Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
This commit is contained in:
parent
742bd61e7b
commit
b1ee0da1b8
29
app/api/activity/route.ts
Normal file
29
app/api/activity/route.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { fetchGanttApi } from "@/lib/data/gantt-api";
|
||||||
|
|
||||||
|
interface ActivityPayload {
|
||||||
|
tasks?: unknown[];
|
||||||
|
projects?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const payload = await fetchGanttApi<ActivityPayload>("/tasks?scope=all");
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
tasks: Array.isArray(payload.tasks) ? payload.tasks : [],
|
||||||
|
projects: Array.isArray(payload.projects) ? payload.projects : [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Failed to fetch activity data";
|
||||||
|
console.error("[api/activity] Request failed", error);
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { supabaseClient } from "@/lib/supabase/client";
|
|
||||||
import type { Database, ActivityItem } from "@/lib/supabase/database.types";
|
import type { Database, ActivityItem } from "@/lib/supabase/database.types";
|
||||||
|
|
||||||
type Task = Database['public']['Tables']['tasks']['Row'];
|
type Project = Pick<Database["public"]["Tables"]["projects"]["Row"], "id" | "name" | "color">;
|
||||||
type Project = Database['public']['Tables']['projects']['Row'];
|
type TaskStatus = Database["public"]["Tables"]["tasks"]["Row"]["status"];
|
||||||
|
|
||||||
export type ActivityFilterType = 'all' | 'task_created' | 'task_completed' | 'task_updated' | 'comment_added' | 'task_assigned';
|
export type ActivityFilterType =
|
||||||
|
| "all"
|
||||||
|
| "task_created"
|
||||||
|
| "task_completed"
|
||||||
|
| "task_updated"
|
||||||
|
| "comment_added"
|
||||||
|
| "task_assigned";
|
||||||
|
|
||||||
interface UseActivityFeedOptions {
|
interface UseActivityFeedOptions {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@ -15,10 +20,8 @@ interface UseActivityFeedOptions {
|
|||||||
filterType?: ActivityFilterType;
|
filterType?: ActivityFilterType;
|
||||||
}
|
}
|
||||||
|
|
||||||
let canUseLeanTasksSelect = true;
|
|
||||||
|
|
||||||
interface UserDirectoryEntry {
|
interface UserDirectoryEntry {
|
||||||
name: string;
|
name?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,10 +35,35 @@ interface NormalizedTaskComment {
|
|||||||
replies: NormalizedTaskComment[];
|
replies: NormalizedTaskComment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDirectoryRow {
|
interface ActivityApiTask {
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
title: string;
|
||||||
avatar_url: string | null;
|
status: TaskStatus;
|
||||||
|
projectId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdById?: string;
|
||||||
|
createdByName?: string;
|
||||||
|
createdByAvatarUrl?: string;
|
||||||
|
updatedById?: string;
|
||||||
|
updatedByName?: string;
|
||||||
|
updatedByAvatarUrl?: string;
|
||||||
|
assigneeId?: string;
|
||||||
|
assigneeName?: string;
|
||||||
|
assigneeAvatarUrl?: string;
|
||||||
|
comments?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityApiProject {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityApiResponse {
|
||||||
|
tasks?: ActivityApiTask[];
|
||||||
|
projects?: ActivityApiProject[];
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUserId(value: string | null | undefined): string | null {
|
function normalizeUserId(value: string | null | undefined): string | null {
|
||||||
@ -50,9 +78,9 @@ function resolveUserName(
|
|||||||
fallbackName?: string | null
|
fallbackName?: string | null
|
||||||
): string {
|
): string {
|
||||||
const normalizedUserId = normalizeUserId(userId);
|
const normalizedUserId = normalizeUserId(userId);
|
||||||
if (normalizedUserId && users[normalizedUserId]?.name) return users[normalizedUserId].name;
|
if (normalizedUserId && users[normalizedUserId]?.name) return users[normalizedUserId].name as string;
|
||||||
if (fallbackName && fallbackName.trim().length > 0) return fallbackName;
|
if (fallbackName && fallbackName.trim().length > 0) return fallbackName;
|
||||||
if (!userId) return 'Unknown';
|
if (!userId) return "Unknown";
|
||||||
|
|
||||||
const normalizedId = userId.trim().toLowerCase();
|
const normalizedId = userId.trim().toLowerCase();
|
||||||
if (normalizedId === "assistant") return "Assistant";
|
if (normalizedId === "assistant") return "Assistant";
|
||||||
@ -126,146 +154,117 @@ function flattenTaskComments(
|
|||||||
return flattened;
|
return flattened;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUserDirectory(rows: Array<{ id: string; name: string | null; avatar_url: string | null }>): UserDirectory {
|
function buildUserDirectoryFromTasks(tasks: ActivityApiTask[]): UserDirectory {
|
||||||
const directory: UserDirectory = {};
|
const directory: UserDirectory = {};
|
||||||
|
|
||||||
rows.forEach((row) => {
|
const upsert = (userId?: string, name?: string, avatarUrl?: string) => {
|
||||||
const rawId = toNonEmptyString(row.id);
|
const normalizedUserId = normalizeUserId(userId);
|
||||||
const id = normalizeUserId(rawId);
|
if (!normalizedUserId) return;
|
||||||
const name = toNonEmptyString(row.name);
|
|
||||||
if (!id || !name) return;
|
|
||||||
|
|
||||||
directory[id] = {
|
const existing = directory[normalizedUserId] ?? {};
|
||||||
name,
|
const nextName = toNonEmptyString(name) ?? existing.name;
|
||||||
avatarUrl: toNonEmptyString(row.avatar_url) ?? undefined,
|
const nextAvatarUrl = toNonEmptyString(avatarUrl) ?? existing.avatarUrl;
|
||||||
|
|
||||||
|
if (!nextName && !nextAvatarUrl) return;
|
||||||
|
|
||||||
|
directory[normalizedUserId] = {
|
||||||
|
...(nextName ? { name: nextName } : {}),
|
||||||
|
...(nextAvatarUrl ? { avatarUrl: nextAvatarUrl } : {}),
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
upsert(task.createdById, task.createdByName, task.createdByAvatarUrl);
|
||||||
|
upsert(task.updatedById, task.updatedByName, task.updatedByAvatarUrl);
|
||||||
|
upsert(task.assigneeId, task.assigneeName, task.assigneeAvatarUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||||
const { limit = 50, projectId, filterType = 'all' } = options;
|
const { limit = 50, projectId, filterType = "all" } = options;
|
||||||
const isDebug = process.env.NODE_ENV !== "production";
|
const isDebug = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchProjects = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabaseClient
|
|
||||||
.from('projects')
|
|
||||||
.select('id, name, color')
|
|
||||||
.order('name');
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
setProjects(data || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching projects:', err);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchActivities = useCallback(async () => {
|
const fetchActivities = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let userRows: UserDirectoryRow[] = [];
|
const response = await fetch("/api/activity", { cache: "no-store" });
|
||||||
try {
|
const payload = (await response.json().catch(() => null)) as ActivityApiResponse | null;
|
||||||
const response = await fetch("/api/users/directory", { cache: "no-store" });
|
|
||||||
if (response.ok) {
|
if (!response.ok) {
|
||||||
const payload = (await response.json()) as { users?: UserDirectoryRow[] };
|
const message =
|
||||||
userRows = Array.isArray(payload.users) ? payload.users : [];
|
payload?.error ||
|
||||||
} else {
|
`Activity API request failed with status ${response.status}`;
|
||||||
if (isDebug) {
|
throw new Error(message);
|
||||||
console.warn("[activity-feed] users directory API returned non-OK status", { status: response.status });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (isDebug) {
|
|
||||||
console.warn("[activity-feed] users directory API request failed, falling back to client query", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userRows.length === 0) {
|
const apiTasks = Array.isArray(payload?.tasks) ? payload.tasks : [];
|
||||||
const { data: fallbackUserRows, error: usersError } = await supabaseClient
|
const apiProjects = (Array.isArray(payload?.projects) ? payload.projects : []).filter(
|
||||||
.from("users")
|
(project) =>
|
||||||
.select("id, name, avatar_url");
|
typeof project?.id === "string" &&
|
||||||
if (usersError) throw usersError;
|
typeof project?.name === "string" &&
|
||||||
userRows = (fallbackUserRows || []) as UserDirectoryRow[];
|
typeof project?.color === "string"
|
||||||
}
|
);
|
||||||
|
|
||||||
const userDirectory = buildUserDirectory(userRows);
|
const visibleProjects = apiProjects.map((project) => ({
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
color: project.color,
|
||||||
|
}));
|
||||||
|
setProjects(visibleProjects);
|
||||||
|
|
||||||
|
const userDirectory = buildUserDirectoryFromTasks(apiTasks);
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
console.log("[activity-feed] user directory loaded", {
|
console.log("[activity-feed] user directory derived from tasks", {
|
||||||
userCount: Object.keys(userDirectory).length,
|
userCount: Object.keys(userDirectory).length,
|
||||||
sampleUserIds: Object.keys(userDirectory).slice(0, 5),
|
sampleUserIds: Object.keys(userDirectory).slice(0, 5),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const runTasksQuery = async (selectClause: string) => {
|
const projectById = new Map(visibleProjects.map((project) => [project.id, project]));
|
||||||
let query = supabaseClient
|
|
||||||
.from('tasks')
|
|
||||||
.select(selectClause)
|
|
||||||
.order('updated_at', { ascending: false })
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
if (projectId) {
|
const tasks = apiTasks
|
||||||
query = query.eq('project_id', projectId);
|
.filter((task) => (projectId ? task.projectId === projectId : true))
|
||||||
}
|
.map((task) => ({
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
status: task.status,
|
||||||
|
project_id: task.projectId,
|
||||||
|
created_at: task.createdAt,
|
||||||
|
updated_at: task.updatedAt,
|
||||||
|
created_by_id: task.createdById ?? null,
|
||||||
|
created_by_name: task.createdByName ?? null,
|
||||||
|
updated_by_id: task.updatedById ?? null,
|
||||||
|
updated_by_name: task.updatedByName ?? null,
|
||||||
|
assignee_id: task.assigneeId ?? null,
|
||||||
|
assignee_name: task.assigneeName ?? null,
|
||||||
|
comments: task.comments,
|
||||||
|
projects: projectById.get(task.projectId)
|
||||||
|
? {
|
||||||
|
name: projectById.get(task.projectId)?.name,
|
||||||
|
color: projectById.get(task.projectId)?.color,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
assignee: task.assigneeId
|
||||||
|
? {
|
||||||
|
id: task.assigneeId,
|
||||||
|
name: task.assigneeName ?? null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
let tasks: unknown[] | null = null;
|
|
||||||
let tasksError: unknown = null;
|
|
||||||
|
|
||||||
if (canUseLeanTasksSelect) {
|
|
||||||
const leanResult = await runTasksQuery(`
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
status,
|
|
||||||
project_id,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
created_by_id,
|
|
||||||
created_by_name,
|
|
||||||
updated_by_id,
|
|
||||||
updated_by_name,
|
|
||||||
assignee_id,
|
|
||||||
assignee_name,
|
|
||||||
comments,
|
|
||||||
projects:project_id (name, color)
|
|
||||||
`);
|
|
||||||
tasks = leanResult.data;
|
|
||||||
tasksError = leanResult.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tasksError || !canUseLeanTasksSelect) {
|
|
||||||
if (tasksError) canUseLeanTasksSelect = false;
|
|
||||||
const fallback = await runTasksQuery(`
|
|
||||||
*,
|
|
||||||
projects:project_id (*),
|
|
||||||
assignee:assignee_id (id, name)
|
|
||||||
`);
|
|
||||||
tasks = fallback.data;
|
|
||||||
tasksError = fallback.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tasksError) throw tasksError;
|
|
||||||
|
|
||||||
// Convert tasks to activity items
|
|
||||||
const activityItems: ActivityItem[] = [];
|
const activityItems: ActivityItem[] = [];
|
||||||
|
|
||||||
const typedTasks = (tasks || []) as Array<Task & {
|
|
||||||
projects: Pick<Project, "name" | "color"> | null;
|
|
||||||
assignee?: { id?: string; name?: string | null } | null;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const globalTaskUserNameById: Record<string, string> = {};
|
const globalTaskUserNameById: Record<string, string> = {};
|
||||||
typedTasks.forEach((task) => {
|
tasks.forEach((task) => {
|
||||||
const createdById = normalizeUserId(task.created_by_id);
|
const createdById = normalizeUserId(task.created_by_id);
|
||||||
const updatedById = normalizeUserId(task.updated_by_id);
|
const updatedById = normalizeUserId(task.updated_by_id);
|
||||||
const assigneeId = normalizeUserId(task.assignee_id);
|
const assigneeId = normalizeUserId(task.assignee_id);
|
||||||
@ -277,7 +276,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
typedTasks.forEach((task) => {
|
tasks.forEach((task) => {
|
||||||
const project = task.projects;
|
const project = task.projects;
|
||||||
const taskUserNameById: Record<string, string> = {};
|
const taskUserNameById: Record<string, string> = {};
|
||||||
|
|
||||||
@ -294,7 +293,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
if (assigneeId && (task.assignee_name || task.assignee?.name)) {
|
if (assigneeId && (task.assignee_name || task.assignee?.name)) {
|
||||||
taskUserNameById[assigneeId] = task.assignee_name || task.assignee?.name || "";
|
taskUserNameById[assigneeId] = task.assignee_name || task.assignee?.name || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdByName = resolveUserName(
|
const createdByName = resolveUserName(
|
||||||
task.created_by_id,
|
task.created_by_id,
|
||||||
userDirectory,
|
userDirectory,
|
||||||
@ -312,6 +311,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
);
|
);
|
||||||
const createdByAvatarUrl = resolveUserAvatarUrl(task.created_by_id, userDirectory);
|
const createdByAvatarUrl = resolveUserAvatarUrl(task.created_by_id, userDirectory);
|
||||||
const updatedByAvatarUrl = resolveUserAvatarUrl(task.updated_by_id, userDirectory);
|
const updatedByAvatarUrl = resolveUserAvatarUrl(task.updated_by_id, userDirectory);
|
||||||
|
|
||||||
if (isDebug && (!task.created_by_id || createdByName === "User")) {
|
if (isDebug && (!task.created_by_id || createdByName === "User")) {
|
||||||
console.log("[activity-feed] created_by fallback", {
|
console.log("[activity-feed] created_by fallback", {
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
@ -320,81 +320,78 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
resolvedName: createdByName,
|
resolvedName: createdByName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task creation activity
|
if (filterType === "all" || filterType === "task_created") {
|
||||||
if (filterType === 'all' || filterType === 'task_created') {
|
|
||||||
activityItems.push({
|
activityItems.push({
|
||||||
id: `${task.id}-created`,
|
id: `${task.id}-created`,
|
||||||
type: 'task_created',
|
type: "task_created",
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
task_title: task.title,
|
task_title: task.title,
|
||||||
project_id: task.project_id,
|
project_id: task.project_id,
|
||||||
project_name: project?.name || 'Unknown Project',
|
project_name: project?.name || "Unknown Project",
|
||||||
project_color: project?.color || '#6B7280',
|
project_color: project?.color || "#6B7280",
|
||||||
user_id: task.created_by_id || '',
|
user_id: task.created_by_id || "",
|
||||||
user_name: createdByName,
|
user_name: createdByName,
|
||||||
user_avatar_url: createdByAvatarUrl,
|
user_avatar_url: createdByAvatarUrl,
|
||||||
timestamp: pickTimestamp(task.created_at, task.updated_at),
|
timestamp: pickTimestamp(task.created_at, task.updated_at),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task completion activity
|
if (task.status === "done" && (filterType === "all" || filterType === "task_completed")) {
|
||||||
if (task.status === 'done' && (filterType === 'all' || filterType === 'task_completed')) {
|
|
||||||
activityItems.push({
|
activityItems.push({
|
||||||
id: `${task.id}-completed`,
|
id: `${task.id}-completed`,
|
||||||
type: 'task_completed',
|
type: "task_completed",
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
task_title: task.title,
|
task_title: task.title,
|
||||||
project_id: task.project_id,
|
project_id: task.project_id,
|
||||||
project_name: project?.name || 'Unknown Project',
|
project_name: project?.name || "Unknown Project",
|
||||||
project_color: project?.color || '#6B7280',
|
project_color: project?.color || "#6B7280",
|
||||||
user_id: task.updated_by_id || task.created_by_id || '',
|
user_id: task.updated_by_id || task.created_by_id || "",
|
||||||
user_name: updatedByName || createdByName,
|
user_name: updatedByName || createdByName,
|
||||||
user_avatar_url: updatedByAvatarUrl || createdByAvatarUrl,
|
user_avatar_url: updatedByAvatarUrl || createdByAvatarUrl,
|
||||||
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assignment activity
|
if (task.assignee_id && (filterType === "all" || filterType === "task_assigned")) {
|
||||||
if (task.assignee_id && (filterType === 'all' || filterType === 'task_assigned')) {
|
|
||||||
activityItems.push({
|
activityItems.push({
|
||||||
id: `${task.id}-assigned`,
|
id: `${task.id}-assigned`,
|
||||||
type: 'task_assigned',
|
type: "task_assigned",
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
task_title: task.title,
|
task_title: task.title,
|
||||||
project_id: task.project_id,
|
project_id: task.project_id,
|
||||||
project_name: project?.name || 'Unknown Project',
|
project_name: project?.name || "Unknown Project",
|
||||||
project_color: project?.color || '#6B7280',
|
project_color: project?.color || "#6B7280",
|
||||||
user_id: task.updated_by_id || task.created_by_id || '',
|
user_id: task.updated_by_id || task.created_by_id || "",
|
||||||
user_name: updatedByName || createdByName,
|
user_name: updatedByName || createdByName,
|
||||||
user_avatar_url: updatedByAvatarUrl || createdByAvatarUrl,
|
user_avatar_url: updatedByAvatarUrl || createdByAvatarUrl,
|
||||||
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
||||||
details: `Assigned to ${assigneeName}`,
|
details: `Assigned to ${assigneeName}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task update activity (if updated after creation and not completed)
|
if (
|
||||||
if (task.updated_at !== task.created_at &&
|
task.updated_at !== task.created_at &&
|
||||||
task.status !== 'done' &&
|
task.status !== "done" &&
|
||||||
(filterType === 'all' || filterType === 'task_updated')) {
|
(filterType === "all" || filterType === "task_updated")
|
||||||
|
) {
|
||||||
activityItems.push({
|
activityItems.push({
|
||||||
id: `${task.id}-updated`,
|
id: `${task.id}-updated`,
|
||||||
type: 'task_updated',
|
type: "task_updated",
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
task_title: task.title,
|
task_title: task.title,
|
||||||
project_id: task.project_id,
|
project_id: task.project_id,
|
||||||
project_name: project?.name || 'Unknown Project',
|
project_name: project?.name || "Unknown Project",
|
||||||
project_color: project?.color || '#6B7280',
|
project_color: project?.color || "#6B7280",
|
||||||
user_id: task.updated_by_id || '',
|
user_id: task.updated_by_id || "",
|
||||||
user_name: updatedByName,
|
user_name: updatedByName,
|
||||||
user_avatar_url: updatedByAvatarUrl,
|
user_avatar_url: updatedByAvatarUrl,
|
||||||
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
||||||
details: `Status: ${task.status}`,
|
details: `Status: ${task.status}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment activities
|
if (task.comments && Array.isArray(task.comments) && (filterType === "all" || filterType === "comment_added")) {
|
||||||
if (task.comments && Array.isArray(task.comments) && (filterType === 'all' || filterType === 'comment_added')) {
|
|
||||||
const comments = normalizeTaskComments(task.comments);
|
const comments = normalizeTaskComments(task.comments);
|
||||||
const flattenedComments = flattenTaskComments(comments);
|
const flattenedComments = flattenTaskComments(comments);
|
||||||
|
|
||||||
@ -409,6 +406,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
: null) ?? null
|
: null) ?? null
|
||||||
);
|
);
|
||||||
const commentAuthorAvatarUrl = resolveUserAvatarUrl(commentAuthorId, userDirectory);
|
const commentAuthorAvatarUrl = resolveUserAvatarUrl(commentAuthorId, userDirectory);
|
||||||
|
|
||||||
if (isDebug && (commentAuthorName === "User" || commentAuthorName === "Unknown")) {
|
if (isDebug && (commentAuthorName === "User" || commentAuthorName === "Unknown")) {
|
||||||
console.log("[activity-feed] comment author fallback", {
|
console.log("[activity-feed] comment author fallback", {
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
@ -422,12 +420,12 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
|
|
||||||
activityItems.push({
|
activityItems.push({
|
||||||
id: `${task.id}-comment-${comment.id}-${path}`,
|
id: `${task.id}-comment-${comment.id}-${path}`,
|
||||||
type: 'comment_added',
|
type: "comment_added",
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
task_title: task.title,
|
task_title: task.title,
|
||||||
project_id: task.project_id,
|
project_id: task.project_id,
|
||||||
project_name: project?.name || 'Unknown Project',
|
project_name: project?.name || "Unknown Project",
|
||||||
project_color: project?.color || '#6B7280',
|
project_color: project?.color || "#6B7280",
|
||||||
user_id: commentAuthorId,
|
user_id: commentAuthorId,
|
||||||
user_name: commentAuthorName,
|
user_name: commentAuthorName,
|
||||||
user_avatar_url: commentAuthorAvatarUrl,
|
user_avatar_url: commentAuthorAvatarUrl,
|
||||||
@ -437,13 +435,9 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort by timestamp descending
|
activityItems.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||||
activityItems.sort((a, b) =>
|
|
||||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply limit after all activities are collected
|
|
||||||
setActivities(activityItems.slice(0, limit));
|
setActivities(activityItems.slice(0, limit));
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
console.log("[activity-feed] activities prepared", {
|
console.log("[activity-feed] activities prepared", {
|
||||||
@ -454,31 +448,25 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching activities:', err);
|
console.error("Error fetching activities:", err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch activities');
|
setError(err instanceof Error ? err.message : "Failed to fetch activities");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [limit, projectId, filterType]);
|
}, [limit, projectId, filterType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProjects();
|
|
||||||
fetchActivities();
|
fetchActivities();
|
||||||
}, [fetchProjects, fetchActivities]);
|
}, [fetchActivities]);
|
||||||
|
|
||||||
// Poll for updates every 30 seconds (since realtime WebSocket is disabled)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchActivities();
|
fetchActivities();
|
||||||
}, 30000); // 30 seconds
|
}, 30000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [fetchActivities]);
|
}, [fetchActivities]);
|
||||||
|
|
||||||
// Note: Real-time subscription disabled due to WebSocket connection issues
|
|
||||||
// The activity feed uses regular HTTP polling instead (30s interval)
|
|
||||||
// To re-enable realtime, configure Supabase Realtime in your project settings
|
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
fetchActivities();
|
fetchActivities();
|
||||||
}, [fetchActivities]);
|
}, [fetchActivities]);
|
||||||
|
|||||||
@ -64,15 +64,25 @@ export async function fetchGanttApi<T>(endpoint: string): Promise<T> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = (await response.json().catch(() => null)) as
|
const contentType = response.headers.get("content-type") || "";
|
||||||
|
const isJson = contentType.includes("application/json");
|
||||||
|
const payload = (isJson ? await response.json().catch(() => null) : null) as
|
||||||
| { error?: string; message?: string }
|
| { error?: string; message?: string }
|
||||||
| null;
|
| null;
|
||||||
|
const nonJsonBody = !isJson ? await response.text().catch(() => "") : "";
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const details = payload?.error || payload?.message || response.statusText;
|
const details =
|
||||||
|
payload?.error ||
|
||||||
|
payload?.message ||
|
||||||
|
(nonJsonBody ? nonJsonBody.replace(/\s+/g, " ").slice(0, 200) : response.statusText);
|
||||||
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): ${details}`);
|
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): ${details}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isJson) {
|
||||||
|
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): expected JSON response`);
|
||||||
|
}
|
||||||
|
|
||||||
return payload as T;
|
return payload as T;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
240
scripts/README.md
Normal file
240
scripts/README.md
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
# Mission Control CLI
|
||||||
|
|
||||||
|
A Next.js API-based CLI for managing Mission Control. Follows the same architecture principles as the Gantt Board CLI.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The Mission Control CLI follows a **clean API passthrough architecture**:
|
||||||
|
|
||||||
|
1. **API is the source of truth** - All business logic lives in the Mission Control API endpoints
|
||||||
|
2. **CLI is thin** - CLI scripts parse arguments and call API endpoints, no direct database access
|
||||||
|
3. **Shared code via delegation** - Task/project/sprint operations delegate to Gantt Board CLI
|
||||||
|
4. **Mission Control specific features** call Mission Control API directly
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Authenticate
|
||||||
|
./scripts/mc.sh auth login user@example.com password
|
||||||
|
|
||||||
|
# Search across tasks, projects, documents
|
||||||
|
./scripts/mc.sh search "api design"
|
||||||
|
|
||||||
|
# List tasks with due dates
|
||||||
|
./scripts/mc.sh due-dates
|
||||||
|
|
||||||
|
# Task operations (delegates to gantt-board)
|
||||||
|
./scripts/mc.sh task list --status open
|
||||||
|
./scripts/mc.sh task create --title "New task" --project "Mission Control"
|
||||||
|
|
||||||
|
# Project operations (delegates to gantt-board)
|
||||||
|
./scripts/mc.sh project list
|
||||||
|
|
||||||
|
# Sprint operations (delegates to gantt-board)
|
||||||
|
./scripts/mc.sh sprint list --active
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### Main CLI
|
||||||
|
|
||||||
|
- **`mc.sh`** - Main entry point for all Mission Control CLI operations
|
||||||
|
|
||||||
|
### Wrapper Scripts (Delegate to Gantt Board)
|
||||||
|
|
||||||
|
- **`task.sh`** - Task operations (delegates to gantt-board/scripts/task.sh)
|
||||||
|
- **`project.sh`** - Project operations (delegates to gantt-board/scripts/project.sh)
|
||||||
|
- **`sprint.sh`** - Sprint operations (delegates to gantt-board/scripts/sprint.sh)
|
||||||
|
|
||||||
|
### Library
|
||||||
|
|
||||||
|
- **`lib/api_client.sh`** - Shared HTTP client for Mission Control API calls
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
- **`update-task-status.js`** - Update task status (delegates to gantt-board)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mission Control API URL
|
||||||
|
export MC_API_URL="http://localhost:3001/api"
|
||||||
|
|
||||||
|
# Path to gantt-board (auto-detected if not set)
|
||||||
|
export GANTT_BOARD_DIR="/path/to/gantt-board"
|
||||||
|
|
||||||
|
# Cookie file for authentication
|
||||||
|
export MC_COOKIE_FILE="$HOME/.config/mission-control/cookies.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command Reference
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mc.sh auth login <email> <password>
|
||||||
|
./mc.sh auth logout
|
||||||
|
./mc.sh auth session
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mc.sh search "query string"
|
||||||
|
```
|
||||||
|
|
||||||
|
Searches across:
|
||||||
|
- Tasks (title, description)
|
||||||
|
- Projects (name, description)
|
||||||
|
- Sprints (name, goal)
|
||||||
|
- Documents (title, content)
|
||||||
|
|
||||||
|
### Tasks with Due Dates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mc.sh due-dates
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns tasks with due dates, ordered by due date.
|
||||||
|
|
||||||
|
### Documents
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mc.sh documents list
|
||||||
|
./mc.sh documents get <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task Operations (via Gantt Board)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mc.sh task list [--status <status>] [--priority <priority>]
|
||||||
|
./mc.sh task get <task-id>
|
||||||
|
./mc.sh task create --title "..." [--description "..."] [--project <name>]
|
||||||
|
./mc.sh task update <task-id> [--status <status>] [--priority <priority>]
|
||||||
|
./mc.sh task delete <task-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
See Gantt Board CLI documentation for full task command reference.
|
||||||
|
|
||||||
|
### Project Operations (via Gantt Board)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mc.sh project list
|
||||||
|
./mc.sh project get <project-id-or-name>
|
||||||
|
./mc.sh project create --name "..." [--description "..."]
|
||||||
|
./mc.sh project update <project-id> [--name "..."] [--description "..."]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sprint Operations (via Gantt Board)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mc.sh sprint list [--active]
|
||||||
|
./mc.sh sprint get <sprint-id-or-name>
|
||||||
|
./mc.sh sprint create --name "..." [--goal "..."]
|
||||||
|
./mc.sh sprint close <sprint-id-or-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mission Control CLI │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ mc.sh │ │ task.sh │ │ project.sh │ │
|
||||||
|
│ │ (main) │ │ (wrapper) │ │ (wrapper) │ │
|
||||||
|
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ └──────────┬──────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌──────────────▼──────────────┐ │
|
||||||
|
│ │ │ Gantt Board CLI │ │
|
||||||
|
│ │ │ (task.sh, project.sh) │ │
|
||||||
|
│ │ └──────────────┬──────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ▼ │
|
||||||
|
│ │ ┌─────────────────────┐ │
|
||||||
|
│ │ │ Gantt Board API │ │
|
||||||
|
│ │ └─────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────┐ │
|
||||||
|
│ │ Mission Control API │ │
|
||||||
|
│ │ /api/search, /api/tasks/with-due-dates, etc │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the CLI contract test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:cli-contract
|
||||||
|
```
|
||||||
|
|
||||||
|
This verifies:
|
||||||
|
1. Mission Control CLI wrappers delegate to gantt-board CLI
|
||||||
|
2. No direct database references in scripts/
|
||||||
|
3. update-task-status.js delegates to gantt-board task.sh
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **No Direct Database Access** - CLI scripts never call the database directly
|
||||||
|
2. **API Passthrough** - All operations go through API endpoints
|
||||||
|
3. **Shared Functionality** - Common operations (tasks, projects, sprints) use Gantt Board
|
||||||
|
4. **Clean Separation** - Mission Control specific features use Mission Control API
|
||||||
|
|
||||||
|
## Adding New Commands
|
||||||
|
|
||||||
|
To add a new Mission Control specific command:
|
||||||
|
|
||||||
|
1. Create the API endpoint in `app/api/<feature>/route.ts`
|
||||||
|
2. Add the command handler in `scripts/mc.sh`
|
||||||
|
3. Use `lib/api_client.sh` functions for HTTP calls
|
||||||
|
4. Document the command in this README
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In mc.sh
|
||||||
|
handle_feature() {
|
||||||
|
mc_get "/feature" | jq .
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "GANTT_BOARD_DIR not set"
|
||||||
|
|
||||||
|
Set the environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GANTT_BOARD_DIR=/path/to/gantt-board
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the auto-detection by placing gantt-board in a standard location:
|
||||||
|
- `../../../gantt-board` (relative to mission-control)
|
||||||
|
- `$HOME/Documents/Projects/OpenClaw/Web/gantt-board`
|
||||||
|
|
||||||
|
### "Not authenticated"
|
||||||
|
|
||||||
|
Login first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/mc.sh auth login user@example.com password
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Connection Errors
|
||||||
|
|
||||||
|
Verify Mission Control is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/auth/session
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Gantt Board CLI](../../gantt-board/scripts/README.md)
|
||||||
|
- [Mission Control API](../app/api/)
|
||||||
111
scripts/lib/api_client.sh
Executable file
111
scripts/lib/api_client.sh
Executable file
@ -0,0 +1,111 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Mission Control API Client Library
|
||||||
|
# Shared HTTP client for Mission Control CLI scripts
|
||||||
|
# Follows the same pattern as gantt-board/scripts/lib/api_client.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
MC_API_URL="${MC_API_URL:-http://localhost:3001/api}"
|
||||||
|
MC_COOKIE_FILE="${MC_COOKIE_FILE:-$HOME/.config/mission-control/cookies.txt}"
|
||||||
|
|
||||||
|
# Ensure cookie directory exists
|
||||||
|
mkdir -p "$(dirname "$MC_COOKIE_FILE")"
|
||||||
|
|
||||||
|
# Make authenticated API call to Mission Control
|
||||||
|
# Usage: mc_api_call <method> <endpoint> [data]
|
||||||
|
mc_api_call() {
|
||||||
|
local method="$1"
|
||||||
|
local endpoint="$2"
|
||||||
|
local data="${3:-}"
|
||||||
|
|
||||||
|
local url="${MC_API_URL}${endpoint}"
|
||||||
|
local curl_opts=(
|
||||||
|
-s
|
||||||
|
-b "$MC_COOKIE_FILE"
|
||||||
|
-c "$MC_COOKIE_FILE"
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "$data" ]]; then
|
||||||
|
curl_opts+=(-d "$data")
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl "${curl_opts[@]}" -X "$method" "$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
# GET request helper
|
||||||
|
# Usage: mc_get <endpoint>
|
||||||
|
mc_get() {
|
||||||
|
mc_api_call "GET" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# POST request helper
|
||||||
|
# Usage: mc_post <endpoint> [data]
|
||||||
|
mc_post() {
|
||||||
|
local endpoint="$1"
|
||||||
|
local data="${2:-}"
|
||||||
|
mc_api_call "POST" "$endpoint" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
# DELETE request helper
|
||||||
|
# Usage: mc_delete <endpoint>
|
||||||
|
mc_delete() {
|
||||||
|
mc_api_call "DELETE" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# URL encode a string
|
||||||
|
# Usage: url_encode <string>
|
||||||
|
url_encode() {
|
||||||
|
local str="$1"
|
||||||
|
printf '%s' "$str" | jq -sRr @uri
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if user is authenticated (cookie exists and is valid)
|
||||||
|
mc_is_authenticated() {
|
||||||
|
if [[ ! -f "$MC_COOKIE_FILE" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to get session - if it fails, not authenticated
|
||||||
|
local response
|
||||||
|
response=$(mc_get "/auth/session" 2>/dev/null || echo '{"user":null}')
|
||||||
|
|
||||||
|
# Check if we got a valid user back
|
||||||
|
echo "$response" | jq -e '.user != null' >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Login to Mission Control
|
||||||
|
# Usage: mc_login <email> <password>
|
||||||
|
mc_login() {
|
||||||
|
local email="$1"
|
||||||
|
local password="$2"
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(mc_post "/auth/login" "$(jq -n --arg email "$email" --arg password "$password" '{email:$email,password:$password}')")
|
||||||
|
|
||||||
|
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
|
||||||
|
echo "Login failed: $(echo "$response" | jq -r '.error')" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Login successful"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logout from Mission Control
|
||||||
|
mc_logout() {
|
||||||
|
mc_post "/auth/logout"
|
||||||
|
rm -f "$MC_COOKIE_FILE"
|
||||||
|
echo "Logged out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export functions for use in other scripts
|
||||||
|
export -f mc_api_call
|
||||||
|
export -f mc_get
|
||||||
|
export -f mc_post
|
||||||
|
export -f mc_delete
|
||||||
|
export -f url_encode
|
||||||
|
export -f mc_is_authenticated
|
||||||
|
export -f mc_login
|
||||||
|
export -f mc_logout
|
||||||
265
scripts/mc.sh
Executable file
265
scripts/mc.sh
Executable file
@ -0,0 +1,265 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Mission Control CLI - Main Entry Point
|
||||||
|
# Usage: ./mc.sh <command> [args]
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# auth Authentication (login, logout, session)
|
||||||
|
# task Task operations (delegates to gantt-board)
|
||||||
|
# project Project operations (delegates to gantt-board)
|
||||||
|
# sprint Sprint operations (delegates to gantt-board)
|
||||||
|
# search Search across tasks, projects, documents
|
||||||
|
# document Document management
|
||||||
|
# due-dates Tasks with due dates
|
||||||
|
# dashboard Dashboard data
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# MC_API_URL Mission Control API URL (default: http://localhost:3001/api)
|
||||||
|
# GANTT_BOARD_DIR Path to gantt-board directory (auto-detected)
|
||||||
|
# MC_COOKIE_FILE Path to cookie file (default: ~/.config/mission-control/cookies.txt)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
export SCRIPT_DIR
|
||||||
|
|
||||||
|
# Source API client library
|
||||||
|
source "$SCRIPT_DIR/lib/api_client.sh"
|
||||||
|
|
||||||
|
# Auto-detect gantt-board directory
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
# Try common locations
|
||||||
|
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||||
|
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show usage
|
||||||
|
usage() {
|
||||||
|
cat << 'EOF'
|
||||||
|
Mission Control CLI
|
||||||
|
|
||||||
|
Usage: ./mc.sh <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
auth <subcommand> Authentication operations
|
||||||
|
login <email> <password> Login to Mission Control
|
||||||
|
logout Logout
|
||||||
|
session Show current session
|
||||||
|
|
||||||
|
task <args> Task operations (delegates to gantt-board)
|
||||||
|
See: ./task.sh --help
|
||||||
|
|
||||||
|
project <args> Project operations (delegates to gantt-board)
|
||||||
|
See: ./project.sh --help
|
||||||
|
|
||||||
|
sprint <args> Sprint operations (delegates to gantt-board)
|
||||||
|
See: ./sprint.sh --help
|
||||||
|
|
||||||
|
search <query> Search across tasks, projects, documents
|
||||||
|
|
||||||
|
due-dates List tasks with due dates
|
||||||
|
|
||||||
|
documents Document management
|
||||||
|
list List all documents
|
||||||
|
get <id> Get document by ID
|
||||||
|
|
||||||
|
dashboard Get dashboard data
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./mc.sh auth login user@example.com password
|
||||||
|
./mc.sh search "api design"
|
||||||
|
./mc.sh due-dates
|
||||||
|
./mc.sh task list --status open
|
||||||
|
./mc.sh project list
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
MC_API_URL Mission Control API URL (default: http://localhost:3001/api)
|
||||||
|
GANTT_BOARD_DIR Path to gantt-board directory
|
||||||
|
MC_COOKIE_FILE Path to cookie file
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auth commands
|
||||||
|
handle_auth() {
|
||||||
|
local subcmd="${1:-}"
|
||||||
|
|
||||||
|
case "$subcmd" in
|
||||||
|
login)
|
||||||
|
local email="${2:-}"
|
||||||
|
local password="${3:-}"
|
||||||
|
if [[ -z "$email" || -z "$password" ]]; then
|
||||||
|
echo "Usage: ./mc.sh auth login <email> <password>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mc_login "$email" "$password"
|
||||||
|
;;
|
||||||
|
logout)
|
||||||
|
mc_logout
|
||||||
|
;;
|
||||||
|
session)
|
||||||
|
mc_get "/auth/session" | jq .
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown auth subcommand: $subcmd" >&2
|
||||||
|
echo "Usage: ./mc.sh auth {login|logout|session}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search command
|
||||||
|
handle_search() {
|
||||||
|
local query="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$query" ]]; then
|
||||||
|
echo "Usage: ./mc.sh search <query>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local encoded_query
|
||||||
|
encoded_query=$(url_encode "$query")
|
||||||
|
mc_get "/search?q=$encoded_query" | jq .
|
||||||
|
}
|
||||||
|
|
||||||
|
# Due dates command
|
||||||
|
handle_due_dates() {
|
||||||
|
mc_get "/tasks/with-due-dates" | jq .
|
||||||
|
}
|
||||||
|
|
||||||
|
# Documents command
|
||||||
|
handle_documents() {
|
||||||
|
local subcmd="${1:-list}"
|
||||||
|
|
||||||
|
case "$subcmd" in
|
||||||
|
list)
|
||||||
|
mc_get "/documents" | jq .
|
||||||
|
;;
|
||||||
|
get)
|
||||||
|
local id="${2:-}"
|
||||||
|
if [[ -z "$id" ]]; then
|
||||||
|
echo "Usage: ./mc.sh documents get <id>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mc_get "/documents?id=$id" | jq .
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown documents subcommand: $subcmd" >&2
|
||||||
|
echo "Usage: ./mc.sh documents {list|get <id>}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Task command - delegate to gantt-board
|
||||||
|
handle_task() {
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||||
|
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$GANTT_BOARD_DIR/scripts/task.sh" ]]; then
|
||||||
|
echo "Error: gantt-board task.sh not found at $GANTT_BOARD_DIR/scripts/task.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delegate to gantt-board task.sh
|
||||||
|
exec "$GANTT_BOARD_DIR/scripts/task.sh" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Project command - delegate to gantt-board
|
||||||
|
handle_project() {
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$GANTT_BOARD_DIR/scripts/project.sh" ]]; then
|
||||||
|
echo "Error: gantt-board project.sh not found at $GANTT_BOARD_DIR/scripts/project.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$GANTT_BOARD_DIR/scripts/project.sh" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sprint command - delegate to gantt-board
|
||||||
|
handle_sprint() {
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$GANTT_BOARD_DIR/scripts/sprint.sh" ]]; then
|
||||||
|
echo "Error: gantt-board sprint.sh not found at $GANTT_BOARD_DIR/scripts/sprint.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$GANTT_BOARD_DIR/scripts/sprint.sh" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dashboard command
|
||||||
|
handle_dashboard() {
|
||||||
|
# For now, combine multiple API calls to build dashboard view
|
||||||
|
echo "Fetching dashboard data..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== Tasks with Due Dates ==="
|
||||||
|
handle_due_dates | jq -r '.[] | "\(.due_date) | \(.priority) | \(.title)"' 2>/dev/null || echo "No tasks with due dates"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Recent Activity ==="
|
||||||
|
# This would need a dedicated API endpoint
|
||||||
|
echo "(Recent activity endpoint not yet implemented)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main command dispatcher
|
||||||
|
main() {
|
||||||
|
local cmd="${1:-}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
auth)
|
||||||
|
handle_auth "$@"
|
||||||
|
;;
|
||||||
|
search)
|
||||||
|
handle_search "$@"
|
||||||
|
;;
|
||||||
|
due-dates)
|
||||||
|
handle_due_dates
|
||||||
|
;;
|
||||||
|
documents|document)
|
||||||
|
handle_documents "$@"
|
||||||
|
;;
|
||||||
|
task)
|
||||||
|
handle_task "$@"
|
||||||
|
;;
|
||||||
|
project)
|
||||||
|
handle_project "$@"
|
||||||
|
;;
|
||||||
|
sprint)
|
||||||
|
handle_sprint "$@"
|
||||||
|
;;
|
||||||
|
dashboard)
|
||||||
|
handle_dashboard
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$cmd" ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Unknown command: $cmd" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@ -1,9 +1,39 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Mission Control Project CLI Wrapper
|
||||||
|
# Delegates to gantt-board project.sh for project operations
|
||||||
|
# This maintains the architecture principle: CLI is passthrough to API
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
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" "$@"
|
# Auto-detect gantt-board directory if not set
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
# Try common locations
|
||||||
|
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||||
|
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify gantt-board is available
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation:" >&2
|
||||||
|
echo " export GANTT_BOARD_DIR=/path/to/gantt-board" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$GANTT_BOARD_DIR/scripts/project.sh" ]]; then
|
||||||
|
echo "Error: gantt-board project.sh not found at $GANTT_BOARD_DIR/scripts/project.sh" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Please ensure gantt-board is installed correctly." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delegate all calls to gantt-board project.sh
|
||||||
|
exec "$GANTT_BOARD_DIR/scripts/project.sh" "$@"
|
||||||
|
|||||||
39
scripts/sprint.sh
Executable file
39
scripts/sprint.sh
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Mission Control Sprint CLI Wrapper
|
||||||
|
# Delegates to gantt-board sprint.sh for sprint operations
|
||||||
|
# This maintains the architecture principle: CLI is passthrough to API
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Auto-detect gantt-board directory if not set
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
# Try common locations
|
||||||
|
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||||
|
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify gantt-board is available
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation:" >&2
|
||||||
|
echo " export GANTT_BOARD_DIR=/path/to/gantt-board" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$GANTT_BOARD_DIR/scripts/sprint.sh" ]]; then
|
||||||
|
echo "Error: gantt-board sprint.sh not found at $GANTT_BOARD_DIR/scripts/sprint.sh" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Please ensure gantt-board is installed correctly." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delegate all calls to gantt-board sprint.sh
|
||||||
|
exec "$GANTT_BOARD_DIR/scripts/sprint.sh" "$@"
|
||||||
@ -1,9 +1,39 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Mission Control Task CLI Wrapper
|
||||||
|
# Delegates to gantt-board task.sh for task operations
|
||||||
|
# This maintains the architecture principle: CLI is passthrough to API
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
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" "$@"
|
# Auto-detect gantt-board directory if not set
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
# Try common locations
|
||||||
|
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||||
|
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||||
|
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify gantt-board is available
|
||||||
|
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||||
|
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation:" >&2
|
||||||
|
echo " export GANTT_BOARD_DIR=/path/to/gantt-board" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$GANTT_BOARD_DIR/scripts/task.sh" ]]; then
|
||||||
|
echo "Error: gantt-board task.sh not found at $GANTT_BOARD_DIR/scripts/task.sh" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Please ensure gantt-board is installed correctly." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delegate all calls to gantt-board task.sh
|
||||||
|
exec "$GANTT_BOARD_DIR/scripts/task.sh" "$@"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user