fix: scope all git, activity, webhook, and summarize-title routes to correct project store
- Fix 30 git routes to use getScopedStore(req) instead of global store - Fix activity GET/DELETE routes to use scoped store - Fix POST /api/github/webhooks to use scoped store for badge updates - Fix POST /api/ai/summarize-title to use scoped store for settings - Fix GET /api/git/worktrees to use scoped store for task listing - Add projectId parameter to all git and activity frontend API functions - Update useActivityLog hook to pass projectId through Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
@@ -650,8 +650,8 @@ export interface GitRemote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch GitHub remotes from the current git repository */
|
/** Fetch GitHub remotes from the current git repository */
|
||||||
export function fetchGitRemotes(): Promise<GitRemote[]> {
|
export function fetchGitRemotes(projectId?: string): Promise<GitRemote[]> {
|
||||||
return api<GitRemote[]>("/git/remotes");
|
return api<GitRemote[]>(withProjectId("/git/remotes", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Detailed git remote info with fetch and push URLs */
|
/** Detailed git remote info with fetch and push URLs */
|
||||||
@@ -662,36 +662,36 @@ export interface GitRemoteDetailed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch all git remotes with their fetch and push URLs */
|
/** Fetch all git remotes with their fetch and push URLs */
|
||||||
export function fetchGitRemotesDetailed(): Promise<GitRemoteDetailed[]> {
|
export function fetchGitRemotesDetailed(projectId?: string): Promise<GitRemoteDetailed[]> {
|
||||||
return api<GitRemoteDetailed[]>("/git/remotes/detailed");
|
return api<GitRemoteDetailed[]>(withProjectId("/git/remotes/detailed", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a new git remote */
|
/** Add a new git remote */
|
||||||
export function addGitRemote(name: string, url: string): Promise<void> {
|
export function addGitRemote(name: string, url: string, projectId?: string): Promise<void> {
|
||||||
return api<void>("/git/remotes", {
|
return api<void>(withProjectId("/git/remotes", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ name, url }),
|
body: JSON.stringify({ name, url }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove a git remote */
|
/** Remove a git remote */
|
||||||
export function removeGitRemote(name: string): Promise<void> {
|
export function removeGitRemote(name: string, projectId?: string): Promise<void> {
|
||||||
return api<void>(`/git/remotes/${encodeURIComponent(name)}`, {
|
return api<void>(withProjectId(`/git/remotes/${encodeURIComponent(name)}`, projectId), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rename a git remote */
|
/** Rename a git remote */
|
||||||
export function renameGitRemote(name: string, newName: string): Promise<void> {
|
export function renameGitRemote(name: string, newName: string, projectId?: string): Promise<void> {
|
||||||
return api<void>(`/git/remotes/${encodeURIComponent(name)}`, {
|
return api<void>(withProjectId(`/git/remotes/${encodeURIComponent(name)}`, projectId), {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ newName }),
|
body: JSON.stringify({ newName }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update the URL for a git remote */
|
/** Update the URL for a git remote */
|
||||||
export function updateGitRemoteUrl(name: string, url: string): Promise<void> {
|
export function updateGitRemoteUrl(name: string, url: string, projectId?: string): Promise<void> {
|
||||||
return api<void>(`/git/remotes/${encodeURIComponent(name)}/url`, {
|
return api<void>(withProjectId(`/git/remotes/${encodeURIComponent(name)}/url`, projectId), {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({ url }),
|
body: JSON.stringify({ url }),
|
||||||
});
|
});
|
||||||
@@ -939,92 +939,92 @@ export interface GitPushResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch current git status */
|
/** Fetch current git status */
|
||||||
export function fetchGitStatus(): Promise<GitStatus> {
|
export function fetchGitStatus(projectId?: string): Promise<GitStatus> {
|
||||||
return api<GitStatus>("/git/status");
|
return api<GitStatus>(withProjectId("/git/status", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch recent commits */
|
/** Fetch recent commits */
|
||||||
export function fetchGitCommits(limit?: number): Promise<GitCommit[]> {
|
export function fetchGitCommits(limit?: number, projectId?: string): Promise<GitCommit[]> {
|
||||||
const query = limit ? `?limit=${limit}` : "";
|
const query = limit ? `?limit=${limit}` : "";
|
||||||
return api<GitCommit[]>(`/git/commits${query}`);
|
return api<GitCommit[]>(withProjectId(`/git/commits${query}`, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch diff for a specific commit */
|
/** Fetch diff for a specific commit */
|
||||||
export function fetchCommitDiff(hash: string): Promise<{ stat: string; patch: string }> {
|
export function fetchCommitDiff(hash: string, projectId?: string): Promise<{ stat: string; patch: string }> {
|
||||||
return api<{ stat: string; patch: string }>(`/git/commits/${hash}/diff`);
|
return api<{ stat: string; patch: string }>(withProjectId(`/git/commits/${hash}/diff`, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch local commits ahead of the upstream tracking branch (commits to push) */
|
/** Fetch local commits ahead of the upstream tracking branch (commits to push) */
|
||||||
export function fetchAheadCommits(): Promise<GitCommit[]> {
|
export function fetchAheadCommits(projectId?: string): Promise<GitCommit[]> {
|
||||||
return api<GitCommit[]>("/git/commits/ahead");
|
return api<GitCommit[]>(withProjectId("/git/commits/ahead", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch recent commits for a specific remote */
|
/** Fetch recent commits for a specific remote */
|
||||||
export function fetchRemoteCommits(remote: string, ref?: string, limit?: number): Promise<GitCommit[]> {
|
export function fetchRemoteCommits(remote: string, ref?: string, limit?: number, projectId?: string): Promise<GitCommit[]> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (ref) params.set("ref", ref);
|
if (ref) params.set("ref", ref);
|
||||||
if (limit) params.set("limit", String(limit));
|
if (limit) params.set("limit", String(limit));
|
||||||
const query = params.size > 0 ? `?${params.toString()}` : "";
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
||||||
return api<GitCommit[]>(`/git/remotes/${encodeURIComponent(remote)}/commits${query}`);
|
return api<GitCommit[]>(withProjectId(`/git/remotes/${encodeURIComponent(remote)}/commits${query}`, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch all local branches */
|
/** Fetch all local branches */
|
||||||
export function fetchGitBranches(): Promise<GitBranch[]> {
|
export function fetchGitBranches(projectId?: string): Promise<GitBranch[]> {
|
||||||
return api<GitBranch[]>("/git/branches");
|
return api<GitBranch[]>(withProjectId("/git/branches", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch recent commits for a specific branch */
|
/** Fetch recent commits for a specific branch */
|
||||||
export function fetchBranchCommits(branchName: string, limit?: number): Promise<GitCommit[]> {
|
export function fetchBranchCommits(branchName: string, limit?: number, projectId?: string): Promise<GitCommit[]> {
|
||||||
const query = limit ? `?limit=${limit}` : "";
|
const query = limit ? `?limit=${limit}` : "";
|
||||||
return api<GitCommit[]>(`/git/branches/${encodeURIComponent(branchName)}/commits${query}`);
|
return api<GitCommit[]>(withProjectId(`/git/branches/${encodeURIComponent(branchName)}/commits${query}`, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch all worktrees */
|
/** Fetch all worktrees */
|
||||||
export function fetchGitWorktrees(): Promise<GitWorktree[]> {
|
export function fetchGitWorktrees(projectId?: string): Promise<GitWorktree[]> {
|
||||||
return api<GitWorktree[]>("/git/worktrees");
|
return api<GitWorktree[]>(withProjectId("/git/worktrees", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new branch */
|
/** Create a new branch */
|
||||||
export function createBranch(name: string, base?: string): Promise<void> {
|
export function createBranch(name: string, base?: string, projectId?: string): Promise<void> {
|
||||||
return api<void>("/git/branches", {
|
return api<void>(withProjectId("/git/branches", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ name, base }),
|
body: JSON.stringify({ name, base }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checkout an existing branch */
|
/** Checkout an existing branch */
|
||||||
export function checkoutBranch(name: string): Promise<void> {
|
export function checkoutBranch(name: string, projectId?: string): Promise<void> {
|
||||||
return api<void>(`/git/branches/${encodeURIComponent(name)}/checkout`, {
|
return api<void>(withProjectId(`/git/branches/${encodeURIComponent(name)}/checkout`, projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delete a branch */
|
/** Delete a branch */
|
||||||
export function deleteBranch(name: string, force?: boolean): Promise<void> {
|
export function deleteBranch(name: string, force?: boolean, projectId?: string): Promise<void> {
|
||||||
const query = force ? "?force=true" : "";
|
const query = force ? "?force=true" : "";
|
||||||
return api<void>(`/git/branches/${encodeURIComponent(name)}${query}`, {
|
return api<void>(withProjectId(`/git/branches/${encodeURIComponent(name)}${query}`, projectId), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch from remote */
|
/** Fetch from remote */
|
||||||
export function fetchRemote(remote?: string): Promise<GitFetchResult> {
|
export function fetchRemote(remote?: string, projectId?: string): Promise<GitFetchResult> {
|
||||||
return api<GitFetchResult>("/git/fetch", {
|
return api<GitFetchResult>(withProjectId("/git/fetch", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ remote }),
|
body: JSON.stringify({ remote }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pull current branch */
|
/** Pull current branch */
|
||||||
export function pullBranch(): Promise<GitPullResult> {
|
export function pullBranch(projectId?: string): Promise<GitPullResult> {
|
||||||
return api<GitPullResult>("/git/pull", {
|
return api<GitPullResult>(withProjectId("/git/pull", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Push current branch */
|
/** Push current branch */
|
||||||
export function pushBranch(): Promise<GitPushResult> {
|
export function pushBranch(projectId?: string): Promise<GitPushResult> {
|
||||||
return api<GitPushResult>("/git/push", {
|
return api<GitPushResult>(withProjectId("/git/push", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1046,70 +1046,70 @@ export interface GitFileChange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch stash list */
|
/** Fetch stash list */
|
||||||
export function fetchGitStashList(): Promise<GitStash[]> {
|
export function fetchGitStashList(projectId?: string): Promise<GitStash[]> {
|
||||||
return api<GitStash[]>("/git/stashes");
|
return api<GitStash[]>(withProjectId("/git/stashes", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a new stash */
|
/** Create a new stash */
|
||||||
export function createStash(message?: string): Promise<{ message: string }> {
|
export function createStash(message?: string, projectId?: string): Promise<{ message: string }> {
|
||||||
return api<{ message: string }>("/git/stashes", {
|
return api<{ message: string }>(withProjectId("/git/stashes", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ message }),
|
body: JSON.stringify({ message }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply a stash entry */
|
/** Apply a stash entry */
|
||||||
export function applyStash(index: number, drop?: boolean): Promise<{ message: string }> {
|
export function applyStash(index: number, drop?: boolean, projectId?: string): Promise<{ message: string }> {
|
||||||
return api<{ message: string }>(`/git/stashes/${index}/apply`, {
|
return api<{ message: string }>(withProjectId(`/git/stashes/${index}/apply`, projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ drop }),
|
body: JSON.stringify({ drop }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drop a stash entry */
|
/** Drop a stash entry */
|
||||||
export function dropStash(index: number): Promise<{ message: string }> {
|
export function dropStash(index: number, projectId?: string): Promise<{ message: string }> {
|
||||||
return api<{ message: string }>(`/git/stashes/${index}`, {
|
return api<{ message: string }>(withProjectId(`/git/stashes/${index}`, projectId), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch unstaged diff (working directory changes) */
|
/** Fetch unstaged diff (working directory changes) */
|
||||||
export function fetchUnstagedDiff(): Promise<{ stat: string; patch: string }> {
|
export function fetchUnstagedDiff(projectId?: string): Promise<{ stat: string; patch: string }> {
|
||||||
return api<{ stat: string; patch: string }>("/git/diff");
|
return api<{ stat: string; patch: string }>(withProjectId("/git/diff", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch file changes (staged and unstaged) */
|
/** Fetch file changes (staged and unstaged) */
|
||||||
export function fetchFileChanges(): Promise<GitFileChange[]> {
|
export function fetchFileChanges(projectId?: string): Promise<GitFileChange[]> {
|
||||||
return api<GitFileChange[]>("/git/changes");
|
return api<GitFileChange[]>(withProjectId("/git/changes", projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stage specific files */
|
/** Stage specific files */
|
||||||
export function stageFiles(files: string[]): Promise<{ staged: string[] }> {
|
export function stageFiles(files: string[], projectId?: string): Promise<{ staged: string[] }> {
|
||||||
return api<{ staged: string[] }>("/git/stage", {
|
return api<{ staged: string[] }>(withProjectId("/git/stage", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ files }),
|
body: JSON.stringify({ files }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unstage specific files */
|
/** Unstage specific files */
|
||||||
export function unstageFiles(files: string[]): Promise<{ unstaged: string[] }> {
|
export function unstageFiles(files: string[], projectId?: string): Promise<{ unstaged: string[] }> {
|
||||||
return api<{ unstaged: string[] }>("/git/unstage", {
|
return api<{ unstaged: string[] }>(withProjectId("/git/unstage", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ files }),
|
body: JSON.stringify({ files }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a commit */
|
/** Create a commit */
|
||||||
export function createCommit(message: string): Promise<{ hash: string; message: string }> {
|
export function createCommit(message: string, projectId?: string): Promise<{ hash: string; message: string }> {
|
||||||
return api<{ hash: string; message: string }>("/git/commit", {
|
return api<{ hash: string; message: string }>(withProjectId("/git/commit", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ message }),
|
body: JSON.stringify({ message }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Discard changes in working directory for specific files */
|
/** Discard changes in working directory for specific files */
|
||||||
export function discardChanges(files: string[]): Promise<{ discarded: string[] }> {
|
export function discardChanges(files: string[], projectId?: string): Promise<{ discarded: string[] }> {
|
||||||
return api<{ discarded: string[] }>("/git/discard", {
|
return api<{ discarded: string[] }>(withProjectId("/git/discard", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ files }),
|
body: JSON.stringify({ files }),
|
||||||
});
|
});
|
||||||
@@ -1749,18 +1749,20 @@ export function triggerRoutineWebhook(id: string, payload?: Record<string, unkno
|
|||||||
export type { ActivityLogEntry, ActivityEventType } from "@fusion/core";
|
export type { ActivityLogEntry, ActivityEventType } from "@fusion/core";
|
||||||
|
|
||||||
/** Fetch activity log entries */
|
/** Fetch activity log entries */
|
||||||
export function fetchActivityLog(options?: { limit?: number; since?: string; type?: ActivityEventType }): Promise<ActivityLogEntry[]> {
|
export function fetchActivityLog(options?: { limit?: number; since?: string; type?: ActivityEventType; projectId?: string }): Promise<ActivityLogEntry[]> {
|
||||||
const search = new URLSearchParams();
|
const search = new URLSearchParams();
|
||||||
if (options?.limit !== undefined) search.set("limit", String(options.limit));
|
if (options?.limit !== undefined) search.set("limit", String(options.limit));
|
||||||
if (options?.since !== undefined) search.set("since", options.since);
|
if (options?.since !== undefined) search.set("since", options.since);
|
||||||
if (options?.type !== undefined) search.set("type", options.type);
|
if (options?.type !== undefined) search.set("type", options.type);
|
||||||
|
if (options?.projectId) search.set("projectId", options.projectId);
|
||||||
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
||||||
return api<ActivityLogEntry[]>(`/activity${suffix}`);
|
return api<ActivityLogEntry[]>(`/activity${suffix}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear all activity log entries */
|
/** Clear all activity log entries */
|
||||||
export function clearActivityLog(): Promise<{ success: boolean }> {
|
export function clearActivityLog(projectId?: string): Promise<{ success: boolean }> {
|
||||||
return api<{ success: boolean }>("/activity", { method: "DELETE" });
|
const path = withProjectId("/activity", projectId);
|
||||||
|
return api<{ success: boolean }>(path, { method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Workflow Steps ─────────────────────────────────────────────────────
|
// ── Workflow Steps ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { memo, useCallback, useMemo, useState } from "react";
|
import { memo, useCallback, useMemo, useState } from "react";
|
||||||
import { Activity, Server, Settings, Trash2 } from "lucide-react";
|
import { Activity, Server, Settings, Trash2 } from "lucide-react";
|
||||||
import type { NodeInfo, ProjectInfo } from "../api";
|
import type { NodeInfo, ProjectInfo } from "../api";
|
||||||
|
import { getProjectCountForNode } from "../utils/nodeProjectAssignment";
|
||||||
|
|
||||||
export interface NodeCardProps {
|
export interface NodeCardProps {
|
||||||
node: NodeInfo;
|
node: NodeInfo;
|
||||||
@@ -23,10 +24,6 @@ function truncateUrl(url: string, maxLength: number = 42): string {
|
|||||||
return `${url.slice(0, maxLength - 3)}...`;
|
return `${url.slice(0, maxLength - 3)}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAssignedProjectCount(projects: ProjectInfo[], nodeId: string): number {
|
|
||||||
return projects.filter((project) => project.nodeId === nodeId).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function areNodeCardPropsEqual(previous: NodeCardProps, next: NodeCardProps): boolean {
|
function areNodeCardPropsEqual(previous: NodeCardProps, next: NodeCardProps): boolean {
|
||||||
const prevNode = previous.node;
|
const prevNode = previous.node;
|
||||||
const nextNode = next.node;
|
const nextNode = next.node;
|
||||||
@@ -40,8 +37,9 @@ function areNodeCardPropsEqual(previous: NodeCardProps, next: NodeCardProps): bo
|
|||||||
if (prevNode.updatedAt !== nextNode.updatedAt) return false;
|
if (prevNode.updatedAt !== nextNode.updatedAt) return false;
|
||||||
if (previous.isLoading !== next.isLoading) return false;
|
if (previous.isLoading !== next.isLoading) return false;
|
||||||
|
|
||||||
const previousCount = getAssignedProjectCount(previous.projects, prevNode.id);
|
// Compare project counts using the canonical counting function
|
||||||
const nextCount = getAssignedProjectCount(next.projects, nextNode.id);
|
const previousCount = getProjectCountForNode(previous.projects, prevNode);
|
||||||
|
const nextCount = getProjectCountForNode(next.projects, nextNode);
|
||||||
return previousCount === nextCount;
|
return previousCount === nextCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +55,8 @@ function NodeCardInner({
|
|||||||
const statusConfig = STATUS_CONFIG[node.status];
|
const statusConfig = STATUS_CONFIG[node.status];
|
||||||
|
|
||||||
const assignedProjectCount = useMemo(() => {
|
const assignedProjectCount = useMemo(() => {
|
||||||
return getAssignedProjectCount(projects, node.id);
|
return getProjectCountForNode(projects, node);
|
||||||
}, [projects, node.id]);
|
}, [projects, node]);
|
||||||
|
|
||||||
const handleOpenDetails = useCallback(() => {
|
const handleOpenDetails = useCallback(() => {
|
||||||
onEdit(node);
|
onEdit(node);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { Activity, Pencil, Save, X } from "lucide-react";
|
import { Activity, Pencil, Save, X } from "lucide-react";
|
||||||
import type { NodeInfo, NodeUpdateInput, ProjectInfo } from "../api";
|
import type { NodeInfo, NodeUpdateInput, ProjectInfo } from "../api";
|
||||||
import type { ToastType } from "../hooks/useToast";
|
import type { ToastType } from "../hooks/useToast";
|
||||||
|
import { getProjectsForNode } from "../utils/nodeProjectAssignment";
|
||||||
|
|
||||||
interface NodeDetailModalProps {
|
interface NodeDetailModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -65,7 +66,7 @@ export function NodeDetailModal({
|
|||||||
|
|
||||||
const assignedProjects = useMemo(() => {
|
const assignedProjects = useMemo(() => {
|
||||||
if (!node) return [];
|
if (!node) return [];
|
||||||
return projects.filter((project) => project.nodeId === node.id);
|
return getProjectsForNode(projects, node);
|
||||||
}, [node, projects]);
|
}, [node, projects]);
|
||||||
|
|
||||||
const handleHealthCheck = useCallback(async () => {
|
const handleHealthCheck = useCallback(async () => {
|
||||||
@@ -244,9 +245,13 @@ export function NodeDetailModal({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="node-detail-modal__section">
|
<section className="node-detail-modal__section">
|
||||||
<h4>Assigned Projects ({assignedProjects.length})</h4>
|
<h4>{node.type === "local" ? "Projects" : "Assigned Projects"} ({assignedProjects.length})</h4>
|
||||||
{assignedProjects.length === 0 ? (
|
{assignedProjects.length === 0 ? (
|
||||||
<p className="node-detail-modal__empty">No projects are assigned to this node.</p>
|
<p className="node-detail-modal__empty">
|
||||||
|
{node.type === "local"
|
||||||
|
? "No projects are running on this node."
|
||||||
|
: "No projects are assigned to this node."}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="node-detail-modal__project-list">
|
<ul className="node-detail-modal__project-list">
|
||||||
{assignedProjects.map((project) => (
|
{assignedProjects.map((project) => (
|
||||||
|
|||||||
@@ -144,4 +144,48 @@ describe("NodeCard", () => {
|
|||||||
fireEvent.click(screen.getByLabelText("Confirm remove node"));
|
fireEvent.click(screen.getByLabelText("Confirm remove node"));
|
||||||
expect(onRemove).toHaveBeenCalledWith(node.id);
|
expect(onRemove).toHaveBeenCalledWith(node.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("local node counts include unassigned projects", () => {
|
||||||
|
const localNode = makeNode({ id: "local-1", type: "local" });
|
||||||
|
const projects = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "local-1" }), // explicitly assigned
|
||||||
|
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned - runs on local
|
||||||
|
makeProject({ id: "proj-3", nodeId: "remote-1" }), // assigned to remote - not counted
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<NodeCard
|
||||||
|
node={localNode}
|
||||||
|
projects={projects}
|
||||||
|
onHealthCheck={vi.fn()}
|
||||||
|
onEdit={vi.fn()}
|
||||||
|
onRemove={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Local node should show 2 projects (explicitly assigned + unassigned)
|
||||||
|
expect(screen.getByText("2")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remote node counts exclude unassigned projects", () => {
|
||||||
|
const remoteNode = makeNode({ id: "remote-1", type: "remote" });
|
||||||
|
const projects = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "remote-1" }), // explicitly assigned
|
||||||
|
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned - NOT counted for remote
|
||||||
|
makeProject({ id: "proj-3", nodeId: "local-1" }), // assigned to local - not counted
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<NodeCard
|
||||||
|
node={remoteNode}
|
||||||
|
projects={projects}
|
||||||
|
onHealthCheck={vi.fn()}
|
||||||
|
onEdit={vi.fn()}
|
||||||
|
onRemove={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remote node should show only 1 project (explicitly assigned only)
|
||||||
|
expect(screen.getByText("1")).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -148,4 +148,33 @@ describe("NodesView", () => {
|
|||||||
fireEvent.click(nodeCard!);
|
fireEvent.click(nodeCard!);
|
||||||
expect(screen.getByRole("dialog", { name: "Node details for Detail Node" })).toBeDefined();
|
expect(screen.getByRole("dialog", { name: "Node details for Detail Node" })).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("local node project count includes unassigned projects in detail modal", () => {
|
||||||
|
mockUseProjects.mockReturnValue({
|
||||||
|
projects: [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "node-1" }), // explicitly assigned
|
||||||
|
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned - runs on local
|
||||||
|
],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn().mockResolvedValue(undefined),
|
||||||
|
register: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
unregister: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockUseNodes.mockReturnValue(makeUseNodesResult({
|
||||||
|
nodes: [makeNode({ id: "node-1", name: "Local Node", type: "local" })],
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<NodesView addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
// Click on the node card to open detail modal
|
||||||
|
const nodeCard = document.querySelector(".node-card");
|
||||||
|
expect(nodeCard).toBeInTheDocument();
|
||||||
|
fireEvent.click(nodeCard!);
|
||||||
|
|
||||||
|
// Modal should show "Projects (2)" - including the unassigned project
|
||||||
|
expect(screen.getByText("Projects (2)")).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function useActivityLog(options: UseActivityLogOptions = {}): UseActivity
|
|||||||
// Per-project: fetchActivityLog returns ActivityLogEntry[] which is a
|
// Per-project: fetchActivityLog returns ActivityLogEntry[] which is a
|
||||||
// subset of ActivityFeedEntry (missing projectId/projectName). Map to
|
// subset of ActivityFeedEntry (missing projectId/projectName). Map to
|
||||||
// the full shape so downstream consumers see a uniform interface.
|
// the full shape so downstream consumers see a uniform interface.
|
||||||
const logEntries = await fetchActivityLog({ limit, type });
|
const logEntries = await fetchActivityLog({ limit, type, projectId });
|
||||||
data = logEntries.map((entry) => ({
|
data = logEntries.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
projectId: projectId ?? "",
|
projectId: projectId ?? "",
|
||||||
@@ -125,6 +125,7 @@ export function useActivityLog(options: UseActivityLogOptions = {}): UseActivity
|
|||||||
limit,
|
limit,
|
||||||
type,
|
type,
|
||||||
since: lastTimestampRef.current,
|
since: lastTimestampRef.current,
|
||||||
|
projectId,
|
||||||
});
|
});
|
||||||
data = logEntries.map((entry) => ({
|
data = logEntries.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
|
|||||||
183
packages/dashboard/app/utils/nodeProjectAssignment.test.ts
Normal file
183
packages/dashboard/app/utils/nodeProjectAssignment.test.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
isProjectRoutedToNode,
|
||||||
|
getProjectsForNode,
|
||||||
|
getProjectCountForNode,
|
||||||
|
getUnassignedProjectCount,
|
||||||
|
} from "./nodeProjectAssignment";
|
||||||
|
import type { NodeInfo, ProjectInfo } from "../api";
|
||||||
|
|
||||||
|
function makeNode(overrides: Partial<NodeInfo> = {}): NodeInfo {
|
||||||
|
return {
|
||||||
|
id: "node-1",
|
||||||
|
name: "Test Node",
|
||||||
|
type: "local",
|
||||||
|
status: "online",
|
||||||
|
maxConcurrent: 2,
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeProject(overrides: Partial<ProjectInfo> = {}): ProjectInfo {
|
||||||
|
return {
|
||||||
|
id: "proj-1",
|
||||||
|
name: "Project One",
|
||||||
|
path: "/workspace/project-one",
|
||||||
|
status: "active",
|
||||||
|
isolationMode: "in-process",
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("nodeProjectAssignment", () => {
|
||||||
|
describe("isProjectRoutedToNode", () => {
|
||||||
|
describe("local node", () => {
|
||||||
|
const localNode = makeNode({ id: "local-1", type: "local" });
|
||||||
|
|
||||||
|
it("returns true for projects explicitly assigned to this local node", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: "local-1" });
|
||||||
|
expect(isProjectRoutedToNode(project, localNode)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for unassigned projects (nodeId undefined)", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: undefined });
|
||||||
|
expect(isProjectRoutedToNode(project, localNode)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for unassigned projects (nodeId null)", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: null as unknown as string });
|
||||||
|
expect(isProjectRoutedToNode(project, localNode)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for projects assigned to other nodes", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: "other-node" });
|
||||||
|
expect(isProjectRoutedToNode(project, localNode)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for projects assigned to remote nodes", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: "remote-1" });
|
||||||
|
expect(isProjectRoutedToNode(project, localNode)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("remote node", () => {
|
||||||
|
const remoteNode = makeNode({ id: "remote-1", type: "remote" });
|
||||||
|
|
||||||
|
it("returns true for projects explicitly assigned to this remote node", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: "remote-1" });
|
||||||
|
expect(isProjectRoutedToNode(project, remoteNode)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for unassigned projects (nodeId undefined)", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: undefined });
|
||||||
|
expect(isProjectRoutedToNode(project, remoteNode)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for unassigned projects (nodeId null)", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: null as unknown as string });
|
||||||
|
expect(isProjectRoutedToNode(project, remoteNode)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for projects assigned to local nodes", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: "local-1" });
|
||||||
|
expect(isProjectRoutedToNode(project, remoteNode)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for projects assigned to other remote nodes", () => {
|
||||||
|
const project = makeProject({ id: "proj-1", nodeId: "other-remote" });
|
||||||
|
expect(isProjectRoutedToNode(project, remoteNode)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProjectsForNode", () => {
|
||||||
|
it("returns all projects routed to a local node (including unassigned)", () => {
|
||||||
|
const localNode = makeNode({ id: "local-1", type: "local" });
|
||||||
|
const projects: ProjectInfo[] = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "local-1" }), // assigned to this local node
|
||||||
|
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned
|
||||||
|
makeProject({ id: "proj-3", nodeId: "other-local" }), // assigned to different local node
|
||||||
|
makeProject({ id: "proj-4", nodeId: "remote-1" }), // assigned to remote
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getProjectsForNode(projects, localNode);
|
||||||
|
expect(result.map((p) => p.id)).toEqual(["proj-1", "proj-2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns only explicitly assigned projects for a remote node", () => {
|
||||||
|
const remoteNode = makeNode({ id: "remote-1", type: "remote" });
|
||||||
|
const projects: ProjectInfo[] = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "remote-1" }), // assigned to this remote node
|
||||||
|
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned
|
||||||
|
makeProject({ id: "proj-3", nodeId: "local-1" }), // assigned to local
|
||||||
|
makeProject({ id: "proj-4", nodeId: "other-remote" }), // assigned to other remote
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getProjectsForNode(projects, remoteNode);
|
||||||
|
expect(result.map((p) => p.id)).toEqual(["proj-1"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProjectCountForNode", () => {
|
||||||
|
it("returns correct count for local node (includes unassigned)", () => {
|
||||||
|
const localNode = makeNode({ id: "local-1", type: "local" });
|
||||||
|
const projects: ProjectInfo[] = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "local-1" }),
|
||||||
|
makeProject({ id: "proj-2", nodeId: undefined }),
|
||||||
|
makeProject({ id: "proj-3", nodeId: undefined }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getProjectCountForNode(projects, localNode)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct count for remote node (explicit only)", () => {
|
||||||
|
const remoteNode = makeNode({ id: "remote-1", type: "remote" });
|
||||||
|
const projects: ProjectInfo[] = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "remote-1" }),
|
||||||
|
makeProject({ id: "proj-2", nodeId: "remote-1" }),
|
||||||
|
makeProject({ id: "proj-3", nodeId: undefined }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getProjectCountForNode(projects, remoteNode)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 when no projects are routed to the node", () => {
|
||||||
|
const remoteNode = makeNode({ id: "remote-1", type: "remote" });
|
||||||
|
const projects: ProjectInfo[] = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "local-1" }),
|
||||||
|
makeProject({ id: "proj-2", nodeId: undefined }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getProjectCountForNode(projects, remoteNode)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getUnassignedProjectCount", () => {
|
||||||
|
it("counts projects without nodeId", () => {
|
||||||
|
const projects: ProjectInfo[] = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: undefined }),
|
||||||
|
makeProject({ id: "proj-2", nodeId: null as unknown as string }),
|
||||||
|
makeProject({ id: "proj-3", nodeId: "local-1" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getUnassignedProjectCount(projects)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 when all projects are assigned", () => {
|
||||||
|
const projects: ProjectInfo[] = [
|
||||||
|
makeProject({ id: "proj-1", nodeId: "local-1" }),
|
||||||
|
makeProject({ id: "proj-2", nodeId: "remote-1" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getUnassignedProjectCount(projects)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 for empty array", () => {
|
||||||
|
expect(getUnassignedProjectCount([])).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
75
packages/dashboard/app/utils/nodeProjectAssignment.ts
Normal file
75
packages/dashboard/app/utils/nodeProjectAssignment.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Node-Project Assignment Utilities
|
||||||
|
*
|
||||||
|
* Provides canonical counting logic for projects routed to a node.
|
||||||
|
*
|
||||||
|
* **Runtime Behavior:**
|
||||||
|
* - Projects with `nodeId` pointing to a remote node → run on that remote node
|
||||||
|
* - Projects with `nodeId` pointing to a local node → run on that local node
|
||||||
|
* - Projects without `nodeId` (unassigned) → run on local in-process runtime
|
||||||
|
*
|
||||||
|
* **Counting Rules:**
|
||||||
|
* - Local nodes: include both explicitly-assigned projects AND unassigned projects
|
||||||
|
* - Remote nodes: include only explicitly-assigned projects
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NodeInfo, ProjectInfo } from "../api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a project is routed to a specific node based on runtime rules.
|
||||||
|
*
|
||||||
|
* @param project - The project to check
|
||||||
|
* @param node - The node to check against
|
||||||
|
* @returns true if the project runs on this node
|
||||||
|
*/
|
||||||
|
export function isProjectRoutedToNode(project: ProjectInfo, node: NodeInfo): boolean {
|
||||||
|
if (node.type === "remote") {
|
||||||
|
// Remote nodes: only explicit assignment counts
|
||||||
|
return project.nodeId === node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local nodes: explicit assignment OR unassigned (null/undefined)
|
||||||
|
if (project.nodeId === node.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unassigned projects run on local in-process runtime
|
||||||
|
if (project.nodeId === undefined || project.nodeId === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all projects that are routed to a specific node.
|
||||||
|
*
|
||||||
|
* @param projects - All projects
|
||||||
|
* @param node - The node to filter by
|
||||||
|
* @returns Projects routed to this node
|
||||||
|
*/
|
||||||
|
export function getProjectsForNode(projects: ProjectInfo[], node: NodeInfo): ProjectInfo[] {
|
||||||
|
return projects.filter((project) => isProjectRoutedToNode(project, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the count of projects routed to a specific node.
|
||||||
|
*
|
||||||
|
* @param projects - All projects
|
||||||
|
* @param node - The node to count projects for
|
||||||
|
* @returns Number of projects on this node
|
||||||
|
*/
|
||||||
|
export function getProjectCountForNode(projects: ProjectInfo[], node: NodeInfo): number {
|
||||||
|
return getProjectsForNode(projects, node).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the count of unassigned projects (projects without nodeId).
|
||||||
|
* These projects run on the local in-process runtime.
|
||||||
|
*
|
||||||
|
* @param projects - All projects
|
||||||
|
* @returns Number of unassigned projects
|
||||||
|
*/
|
||||||
|
export function getUnassignedProjectCount(projects: ProjectInfo[]): number {
|
||||||
|
return projects.filter((project) => project.nodeId === undefined || project.nodeId === null).length;
|
||||||
|
}
|
||||||
@@ -3745,9 +3745,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns GitHub remotes from the current git repository.
|
* Returns GitHub remotes from the current git repository.
|
||||||
* Response: Array of GitRemote objects [{ name: string, owner: string, repo: string, url: string }]
|
* Response: Array of GitRemote objects [{ name: string, owner: string, repo: string, url: string }]
|
||||||
*/
|
*/
|
||||||
router.get("/git/remotes", (_req, res) => {
|
router.get("/git/remotes", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
const remotes = getGitHubRemotes(rootDir);
|
const remotes = getGitHubRemotes(rootDir);
|
||||||
res.json(remotes);
|
res.json(remotes);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -3763,9 +3764,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns all git remotes with their fetch and push URLs.
|
* Returns all git remotes with their fetch and push URLs.
|
||||||
* Response: Array of GitRemoteDetailed objects [{ name: string, fetchUrl: string, pushUrl: string }]
|
* Response: Array of GitRemoteDetailed objects [{ name: string, fetchUrl: string, pushUrl: string }]
|
||||||
*/
|
*/
|
||||||
router.get("/git/remotes/detailed", (_req, res) => {
|
router.get("/git/remotes/detailed", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -3786,7 +3788,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/remotes", async (req, res) => {
|
router.post("/git/remotes", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
const { name, url } = req.body;
|
const { name, url } = req.body;
|
||||||
if (!name || typeof name !== "string") {
|
if (!name || typeof name !== "string") {
|
||||||
throw badRequest("name is required");
|
throw badRequest("name is required");
|
||||||
@@ -3827,7 +3830,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.delete("/git/remotes/:name", async (req, res) => {
|
router.delete("/git/remotes/:name", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -3855,7 +3859,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.patch("/git/remotes/:name", async (req, res) => {
|
router.patch("/git/remotes/:name", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -3889,7 +3894,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.put("/git/remotes/:name/url", async (req, res) => {
|
router.put("/git/remotes/:name/url", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -3919,9 +3925,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns current git status: branch, commit hash, dirty state, ahead/behind counts.
|
* Returns current git status: branch, commit hash, dirty state, ahead/behind counts.
|
||||||
* Response: { branch: string, commit: string, isDirty: boolean, ahead: number, behind: number }
|
* Response: { branch: string, commit: string, isDirty: boolean, ahead: number, behind: number }
|
||||||
*/
|
*/
|
||||||
router.get("/git/status", (_req, res) => {
|
router.get("/git/status", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -3943,9 +3950,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns recent commits (default 20, configurable via ?limit=).
|
* Returns recent commits (default 20, configurable via ?limit=).
|
||||||
* Response: Array of GitCommit objects
|
* Response: Array of GitCommit objects
|
||||||
*/
|
*/
|
||||||
router.get("/git/commits", (req, res) => {
|
router.get("/git/commits", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -3965,9 +3973,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns diff for a specific commit (stat + patch).
|
* Returns diff for a specific commit (stat + patch).
|
||||||
* Response: { stat: string, patch: string }
|
* Response: { stat: string, patch: string }
|
||||||
*/
|
*/
|
||||||
router.get("/git/commits/:hash/diff", (req, res) => {
|
router.get("/git/commits/:hash/diff", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -3994,9 +4003,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns local commits ahead of the upstream tracking branch (commits that would be pushed).
|
* Returns local commits ahead of the upstream tracking branch (commits that would be pushed).
|
||||||
* Response: Array of GitCommit objects (empty when no upstream is configured)
|
* Response: Array of GitCommit objects (empty when no upstream is configured)
|
||||||
*/
|
*/
|
||||||
router.get("/git/commits/ahead", (_req, res) => {
|
router.get("/git/commits/ahead", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4019,7 +4029,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.get("/git/remotes/:name/commits", async (req, res) => {
|
router.get("/git/remotes/:name/commits", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4085,9 +4096,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns all local branches with current indicator, remote tracking info, and last commit date.
|
* Returns all local branches with current indicator, remote tracking info, and last commit date.
|
||||||
* Response: Array of GitBranch objects
|
* Response: Array of GitBranch objects
|
||||||
*/
|
*/
|
||||||
router.get("/git/branches", (_req, res) => {
|
router.get("/git/branches", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4107,9 +4119,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Query params: limit (default 10, max 100)
|
* Query params: limit (default 10, max 100)
|
||||||
* Response: Array of GitCommit objects
|
* Response: Array of GitCommit objects
|
||||||
*/
|
*/
|
||||||
router.get("/git/branches/:name/commits", (req, res) => {
|
router.get("/git/branches/:name/commits", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4133,14 +4146,15 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Returns all worktrees with path, branch, isMain, and associated task ID.
|
* Returns all worktrees with path, branch, isMain, and associated task ID.
|
||||||
* Response: Array of GitWorktree objects
|
* Response: Array of GitWorktree objects
|
||||||
*/
|
*/
|
||||||
router.get("/git/worktrees", async (_req, res) => {
|
router.get("/git/worktrees", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
// Get tasks to correlate with worktrees
|
// Get tasks to correlate with worktrees
|
||||||
const tasks = await store.listTasks({ slim: true, includeArchived: false });
|
const tasks = await scopedStore.listTasks({ slim: true, includeArchived: false });
|
||||||
const worktrees = getGitWorktrees(tasks, rootDir);
|
const worktrees = getGitWorktrees(tasks, rootDir);
|
||||||
res.json(worktrees);
|
res.json(worktrees);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -4160,7 +4174,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/branches", async (req, res) => {
|
router.post("/git/branches", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4190,7 +4205,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/branches/:name/checkout", async (req, res) => {
|
router.post("/git/branches/:name/checkout", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4218,7 +4234,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.delete("/git/branches/:name", async (req, res) => {
|
router.delete("/git/branches/:name", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4249,7 +4266,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/fetch", async (req, res) => {
|
router.post("/git/fetch", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4274,9 +4292,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* POST /api/git/pull
|
* POST /api/git/pull
|
||||||
* Pull the current branch.
|
* Pull the current branch.
|
||||||
*/
|
*/
|
||||||
router.post("/git/pull", async (_req, res) => {
|
router.post("/git/pull", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4299,9 +4318,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* POST /api/git/push
|
* POST /api/git/push
|
||||||
* Push the current branch.
|
* Push the current branch.
|
||||||
*/
|
*/
|
||||||
router.post("/git/push", async (_req, res) => {
|
router.post("/git/push", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4327,9 +4347,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* GET /api/git/stashes
|
* GET /api/git/stashes
|
||||||
* Returns list of stash entries.
|
* Returns list of stash entries.
|
||||||
*/
|
*/
|
||||||
router.get("/git/stashes", (_req, res) => {
|
router.get("/git/stashes", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4350,7 +4371,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/stashes", async (req, res) => {
|
router.post("/git/stashes", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4376,7 +4398,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/stashes/:index/apply", async (req, res) => {
|
router.post("/git/stashes/:index/apply", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4401,7 +4424,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.delete("/git/stashes/:index", async (req, res) => {
|
router.delete("/git/stashes/:index", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4423,9 +4447,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* GET /api/git/diff
|
* GET /api/git/diff
|
||||||
* Returns working directory diff (unstaged changes).
|
* Returns working directory diff (unstaged changes).
|
||||||
*/
|
*/
|
||||||
router.get("/git/diff", async (_req, res) => {
|
router.get("/git/diff", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4443,9 +4468,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* GET /api/git/changes
|
* GET /api/git/changes
|
||||||
* Returns file changes (staged and unstaged).
|
* Returns file changes (staged and unstaged).
|
||||||
*/
|
*/
|
||||||
router.get("/git/changes", async (_req, res) => {
|
router.get("/git/changes", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4466,7 +4492,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/stage", async (req, res) => {
|
router.post("/git/stage", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4491,7 +4518,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/unstage", async (req, res) => {
|
router.post("/git/unstage", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4516,7 +4544,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/commit", async (req, res) => {
|
router.post("/git/commit", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -4545,7 +4574,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.post("/git/discard", async (req, res) => {
|
router.post("/git/discard", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
if (!isGitRepo(rootDir)) {
|
if (!isGitRepo(rootDir)) {
|
||||||
throw badRequest("Not a git repository");
|
throw badRequest("Not a git repository");
|
||||||
}
|
}
|
||||||
@@ -5235,8 +5265,9 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all matching tasks by badge URL
|
// Find all matching tasks by badge URL (use project-scoped store if projectId is provided)
|
||||||
const tasks = await store.listTasks({ slim: true, includeArchived: false });
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const tasks = await scopedStore.listTasks({ slim: true, includeArchived: false });
|
||||||
const matchingTasks: Array<{ id: string; resourceType: "pr" | "issue"; current: unknown }> = [];
|
const matchingTasks: Array<{ id: string; resourceType: "pr" | "issue"; current: unknown }> = [];
|
||||||
|
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
@@ -5274,7 +5305,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
const next = { ...(badgeData as Omit<PrInfo, "lastCheckedAt">), lastCheckedAt: checkedAt };
|
const next = { ...(badgeData as Omit<PrInfo, "lastCheckedAt">), lastCheckedAt: checkedAt };
|
||||||
const changed = hasPrBadgeFieldsChanged(current, badgeData as Omit<PrInfo, "lastCheckedAt">);
|
const changed = hasPrBadgeFieldsChanged(current, badgeData as Omit<PrInfo, "lastCheckedAt">);
|
||||||
if (changed || current.lastCheckedAt !== checkedAt) {
|
if (changed || current.lastCheckedAt !== checkedAt) {
|
||||||
await store.updatePrInfo(match.id, next);
|
await scopedStore.updatePrInfo(match.id, next);
|
||||||
if (changed) badgeFieldsChanged = true;
|
if (changed) badgeFieldsChanged = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -5282,7 +5313,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
const next = { ...(badgeData as Omit<import("@fusion/core").IssueInfo, "lastCheckedAt">), lastCheckedAt: checkedAt };
|
const next = { ...(badgeData as Omit<import("@fusion/core").IssueInfo, "lastCheckedAt">), lastCheckedAt: checkedAt };
|
||||||
const changed = hasIssueBadgeFieldsChanged(current, badgeData as Omit<import("@fusion/core").IssueInfo, "lastCheckedAt">);
|
const changed = hasIssueBadgeFieldsChanged(current, badgeData as Omit<import("@fusion/core").IssueInfo, "lastCheckedAt">);
|
||||||
if (changed || current.lastCheckedAt !== checkedAt) {
|
if (changed || current.lastCheckedAt !== checkedAt) {
|
||||||
await store.updateIssueInfo(match.id, next);
|
await scopedStore.updateIssueInfo(match.id, next);
|
||||||
if (changed) badgeFieldsChanged = true;
|
if (changed) badgeFieldsChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7912,7 +7943,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
try {
|
try {
|
||||||
const { description, provider, modelId } = req.body;
|
const { description, provider, modelId } = req.body;
|
||||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||||
const rootDir = store.getRootDir();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
const rootDir = scopedStore.getRootDir();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
checkRateLimit,
|
checkRateLimit,
|
||||||
@@ -7956,7 +7988,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
// 3. Settings planningProvider + planningModelId
|
// 3. Settings planningProvider + planningModelId
|
||||||
// 4. Settings defaultProvider + defaultModelId
|
// 4. Settings defaultProvider + defaultModelId
|
||||||
// 5. Automatic model resolution (no explicit model)
|
// 5. Automatic model resolution (no explicit model)
|
||||||
const settings = await store.getSettings();
|
const settings = await scopedStore.getSettings();
|
||||||
|
|
||||||
const resolvedProvider =
|
const resolvedProvider =
|
||||||
(provider && modelId ? provider : undefined) ||
|
(provider && modelId ? provider : undefined) ||
|
||||||
@@ -8562,6 +8594,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
*/
|
*/
|
||||||
router.get("/activity", async (req, res) => {
|
router.get("/activity", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const scopedStore = await getScopedStore(req);
|
||||||
const limitParam = req.query.limit;
|
const limitParam = req.query.limit;
|
||||||
const sinceParam = req.query.since;
|
const sinceParam = req.query.since;
|
||||||
const typeParam = req.query.type;
|
const typeParam = req.query.type;
|
||||||
@@ -8588,7 +8621,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
type: typeParam as ActivityEventType | undefined,
|
type: typeParam as ActivityEventType | undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const entries = await store.getActivityLog(options);
|
const entries = await scopedStore.getActivityLog(options);
|
||||||
res.json(entries);
|
res.json(entries);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
@@ -8603,9 +8636,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
* Clear all activity log entries (maintenance endpoint).
|
* Clear all activity log entries (maintenance endpoint).
|
||||||
* Returns: { success: true }
|
* Returns: { success: true }
|
||||||
*/
|
*/
|
||||||
router.delete("/activity", async (_req, res) => {
|
router.delete("/activity", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await store.clearActivityLog();
|
const scopedStore = await getScopedStore(req);
|
||||||
|
await scopedStore.clearActivityLog();
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user