fix(core): per-workflow metrics read zero on a renamed board (#2866)
Second of the 14 lane-bound SQL sites from #2839, after #2864. Independent of it — different file, different caller argument. ## The defect `aggregateWorkflowAnalytics` filtered in SQL on `t."column" = 'done'` and `IN ('in-progress','in-review')`. On a renamed board those match nothing, so `tasksCompleted`, `tasksInProgress` and `tasksInReview` come back **zero for every workflow** while the board is busy. Nothing errors. Same shape and same fix as #2864: resolve per **project** via `resolveProjectColumnsForRoles`, bind an `IN` list, and thread the store from the single Command Center caller so the parameter has a supplier immediately rather than becoming an inert seam. ## What the test caught that I had not **The renamed case still failed with the query fixed.** The bucketing at lines 296–297 already uses `isWipColumnRole` / `isReviewColumnRole` — correctly converted — but those read `query.columnFlagsByName`, which production supplies and my fixture did not. So: - the **SQL** decides *which rows come back*; - the **trait map** decides *which bucket each row lands in*. Both halves have to be right. Fixing only the query would have shipped a "conversion" that still reported zero on a renamed board, and the file would have scored as converted twice over. That is exactly the partial-conversion shape this program keeps re-finding — caught here only because the test asserts `tasksInReview` alongside `tasksCompleted`, since those two paths take **different** resolved sets (complete vs wip+human-review). Asserting the completed count alone would have left the second conversion unproven. ## Measured Reverted, only the renamed case flips: ``` ✓ default vocabulary: completed and in-review work are counted × renamed vocabulary: completed and in-review work are counted ✓ renamed vocabulary: a card in the HOLD lane counts as neither ✓ without a lane store, the legacy ids still answer Tests 1 failed | 3 passed (4) ``` The hold-lane negative is there so resolving real lanes cannot degrade into "every column counts" — trading an undercount for an overcount is harder to notice than the original bug. ## Scope The sync SQLite arm in the same file keeps its literals: it throws in backend mode and has no production caller, the same dead-arm conclusion reached for `cleanupStaleMergeQueueRowsImpl` on #2839. ## Verification `pnpm test:gate` green · both Command Center analytics suites 8/8 · `tsc` core 0, dashboard 0 · lint 0 · changeset included. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.changeset/workflow-analytics-renamed-lanes.md
Normal file
7
.changeset/workflow-analytics-renamed-lanes.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Per-workflow Command Center metrics now count work on renamed boards.
|
||||||
|
category: fix
|
||||||
|
dev: `aggregateWorkflowAnalytics` takes an optional lane store and resolves complete / wip / human-review columns via `resolveProjectColumnsForRoles`; its SQL previously filtered on the literal `'done'` and `('in-progress','in-review')`.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
FNXC:WorkflowResolvedColumns 2026-07-30-18:50 (per-workflow metrics read zero on a renamed board):
|
||||||
|
|
||||||
|
`aggregateWorkflowAnalytics` filtered in SQL on `t."column" = 'done'` and
|
||||||
|
`IN ('in-progress','in-review')`. On a board whose lanes are renamed those match nothing, so
|
||||||
|
`tasksCompleted`, `tasksInProgress` and `tasksInReview` all come back ZERO for every workflow while
|
||||||
|
the board is busy. Nothing errors.
|
||||||
|
|
||||||
|
WHY NO EXISTING CHECK SAW IT. The lifecycle census parses TypeScript comparisons; these ids live
|
||||||
|
inside SQL strings. The sweep that converted this file's TypeScript guards left the queries alone,
|
||||||
|
and the file scored as converted.
|
||||||
|
|
||||||
|
The cases are DIFFERENTIAL: the same seeded work aggregated under two vocabularies whose roles are
|
||||||
|
identical and only the ids differ. `shipped` and `checking` collide with no legacy id, so a surviving
|
||||||
|
`'done'` cannot pass by luck.
|
||||||
|
|
||||||
|
`tasksInReview` is asserted as well as `tasksCompleted` because the two queries take DIFFERENT
|
||||||
|
resolved sets — complete for one, wip+human-review for the other. Asserting only the completed count
|
||||||
|
would leave the second conversion unproven, which is the partial-supply shape this program keeps
|
||||||
|
re-finding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
pgDescribe,
|
||||||
|
createSharedPgTaskStoreTestHarness,
|
||||||
|
type SharedPgTaskStoreHarness,
|
||||||
|
} from "../../__test-utils__/pg-test-harness.js";
|
||||||
|
import { aggregateWorkflowAnalytics } from "../../workflow-analytics.js";
|
||||||
|
import { BUILTIN_CODING_WORKFLOW_IR } from "../../index.js";
|
||||||
|
|
||||||
|
const IN_RANGE = "2026-06-15T12:00:00.000Z";
|
||||||
|
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" };
|
||||||
|
|
||||||
|
/*
|
||||||
|
The per-column trait map production supplies (`resolveColumnFlagsByName(store)` at the Command Center
|
||||||
|
route). The SQL fix decides WHICH rows come back; this map decides which bucket each row lands in via
|
||||||
|
`isWipColumnRole` / `isReviewColumnRole`. Both halves have to be right — omitting this map was my
|
||||||
|
first mistake here, and the renamed case failed even with the query fixed, which is exactly the
|
||||||
|
partial-conversion shape the note above warns about.
|
||||||
|
*/
|
||||||
|
const RENAMED_FLAGS = new Map<string, { countsTowardWip?: boolean; humanReview?: boolean; complete?: boolean }>([
|
||||||
|
["building", { countsTowardWip: true }],
|
||||||
|
["checking", { countsTowardWip: true, humanReview: true }],
|
||||||
|
["shipped", { complete: true }],
|
||||||
|
]);
|
||||||
|
const LEGACY_FLAGS = new Map<string, { countsTowardWip?: boolean; humanReview?: boolean; complete?: boolean }>([
|
||||||
|
["in-progress", { countsTowardWip: true }],
|
||||||
|
["in-review", { countsTowardWip: true, humanReview: true }],
|
||||||
|
["done", { complete: true }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
pgDescribe("workflow analytics under a renamed board vocabulary", () => {
|
||||||
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
|
prefix: "fusion_workflow_analytics_lanes",
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(h.beforeAll);
|
||||||
|
beforeEach(h.beforeEach);
|
||||||
|
afterEach(h.afterEach);
|
||||||
|
afterAll(h.afterAll);
|
||||||
|
|
||||||
|
/** The builtin coding workflow with only its column ids renamed. */
|
||||||
|
async function seedRenamedWorkflow(): Promise<void> {
|
||||||
|
const RENAME: Record<string, string> = {
|
||||||
|
todo: "drafting",
|
||||||
|
"in-progress": "building",
|
||||||
|
"in-review": "checking",
|
||||||
|
done: "shipped",
|
||||||
|
};
|
||||||
|
const rename = (id: string | undefined) => (id && RENAME[id]) ?? id;
|
||||||
|
const ir = JSON.parse(JSON.stringify(BUILTIN_CODING_WORKFLOW_IR)) as {
|
||||||
|
id: string;
|
||||||
|
nodes?: { column?: string }[];
|
||||||
|
columns?: { id: string }[];
|
||||||
|
};
|
||||||
|
ir.id = "custom:renamed-workflow-analytics";
|
||||||
|
for (const node of ir.nodes ?? []) node.column = rename(node.column);
|
||||||
|
for (const column of ir.columns ?? []) column.id = rename(column.id) as string;
|
||||||
|
|
||||||
|
const ids = (ir.columns ?? []).map((column) => column.id);
|
||||||
|
expect(ids).toContain("shipped");
|
||||||
|
expect(ids).not.toContain("done");
|
||||||
|
|
||||||
|
await h.store().createWorkflowDefinition({ name: "Renamed", kind: "workflow", ir } as never);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One finished task and one sitting in the review lane. */
|
||||||
|
async function seedWork(completeLane: string, reviewLane: string): Promise<void> {
|
||||||
|
const store = h.store();
|
||||||
|
const adminDb = h.adminDb();
|
||||||
|
for (const [id, lane] of [["KB-DONE", completeLane], ["KB-REVIEW", reviewLane]] as const) {
|
||||||
|
await store.createTaskWithReservedId(
|
||||||
|
{ description: id, column: "todo" },
|
||||||
|
{ taskId: id, createdAt: IN_RANGE, updatedAt: IN_RANGE, applyDefaultWorkflowSteps: false },
|
||||||
|
);
|
||||||
|
/* Seeded directly: the aggregator reads column_moved_at, and moveTask would stamp it with
|
||||||
|
`now` rather than a date inside the query range. */
|
||||||
|
await adminDb.execute(sql`
|
||||||
|
UPDATE project.tasks
|
||||||
|
SET "column" = ${lane}, column_moved_at = ${IN_RANGE}, updated_at = ${IN_RANGE}
|
||||||
|
WHERE id = ${id}`);
|
||||||
|
store.taskCache.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = (withStore: boolean, renamed = false) => aggregateWorkflowAnalytics(
|
||||||
|
Object.assign(h.layer(), { projectId: "p1" }),
|
||||||
|
{ ...RANGE, columnFlagsByName: renamed ? RENAMED_FLAGS : LEGACY_FLAGS },
|
||||||
|
withStore ? h.store() : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Control: the default vocabulary counts both. Passes before and after the fix, so a generally
|
||||||
|
broken aggregator cannot hide behind the renamed case below. */
|
||||||
|
it("default vocabulary: completed and in-review work are counted", async () => {
|
||||||
|
await seedWork("done", "in-review");
|
||||||
|
|
||||||
|
const wf = await run(true);
|
||||||
|
|
||||||
|
expect(wf.totals.tasksCompleted).toBe(1);
|
||||||
|
expect(wf.totals.tasksInReview).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* The defect: before the fix both queries matched nothing on this board. */
|
||||||
|
it("renamed vocabulary: completed and in-review work are counted", async () => {
|
||||||
|
await seedRenamedWorkflow();
|
||||||
|
await seedWork("shipped", "checking");
|
||||||
|
|
||||||
|
const wf = await run(true, true);
|
||||||
|
|
||||||
|
expect(wf.totals.tasksCompleted).toBe(1);
|
||||||
|
expect(wf.totals.tasksInReview).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
The paired negative: resolving real lanes must not degrade into "every column counts". A card still
|
||||||
|
in the hold lane is neither completed nor in review — otherwise the fix trades an undercount for an
|
||||||
|
overcount, which is harder to notice.
|
||||||
|
*/
|
||||||
|
it("renamed vocabulary: a card in the HOLD lane counts as neither", async () => {
|
||||||
|
await seedRenamedWorkflow();
|
||||||
|
await seedWork("drafting", "drafting");
|
||||||
|
|
||||||
|
const wf = await run(true, true);
|
||||||
|
|
||||||
|
expect(wf.totals.tasksCompleted).toBe(0);
|
||||||
|
expect(wf.totals.tasksInReview).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Omitting the store must keep the legacy answer, so an unconverted caller is byte-identical. */
|
||||||
|
it("without a lane store, the legacy ids still answer", async () => {
|
||||||
|
await seedWork("done", "in-review");
|
||||||
|
|
||||||
|
const wf = await run(false);
|
||||||
|
|
||||||
|
expect(wf.totals.tasksCompleted).toBe(1);
|
||||||
|
expect(wf.totals.tasksInReview).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isReviewColumnRole, isWipColumnRole, type ColumnRoleTraitFlags } from "./column-roles.js";
|
import { isReviewColumnRole, isWipColumnRole, type ColumnRoleTraitFlags } from "./column-roles.js";
|
||||||
|
import { resolveProjectColumnsForRoles, type ProjectLaneVocabularyStore } from "./project-lane-vocabulary.js";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import type { Database } from "./db.js";
|
import type { Database } from "./db.js";
|
||||||
import type { AsyncDataLayer } from "./postgres/data-layer.js";
|
import type { AsyncDataLayer } from "./postgres/data-layer.js";
|
||||||
@@ -350,10 +351,27 @@ function buildWorkflowAnalytics(
|
|||||||
export async function aggregateWorkflowAnalytics(
|
export async function aggregateWorkflowAnalytics(
|
||||||
dbOrLayer: Database | AsyncDataLayer,
|
dbOrLayer: Database | AsyncDataLayer,
|
||||||
query: WorkflowAnalyticsQuery = {},
|
query: WorkflowAnalyticsQuery = {},
|
||||||
|
/*
|
||||||
|
FNXC:WorkflowResolvedColumns 2026-07-30-18:30:
|
||||||
|
The store, used ONLY to resolve which columns carry the complete / wip / human-review traits.
|
||||||
|
|
||||||
|
These queries filtered on `t."column" = 'done'` and `IN ('in-progress','in-review')`. Those ids sit
|
||||||
|
inside SQL strings, which the lifecycle census cannot see because it parses TypeScript comparisons —
|
||||||
|
so the sweep that converted this file's TS guards left the queries alone and the file scored as
|
||||||
|
converted. On a renamed board every per-workflow completed count and throughput figure reads ZERO
|
||||||
|
while the board is busy, with no error.
|
||||||
|
|
||||||
|
Resolved per PROJECT: this aggregates a whole project, so the union of a role's columns across its
|
||||||
|
workflows is the right set and a bound IN list is enough. (Per-task lanes need the
|
||||||
|
superset-then-decide-in-JS shape instead — see cleanupStaleMergeQueueRowsInTransaction.)
|
||||||
|
|
||||||
|
Omitted, the legacy ids answer, so an unconverted caller is byte-identical.
|
||||||
|
*/
|
||||||
|
laneStore?: ProjectLaneVocabularyStore,
|
||||||
): Promise<WorkflowAnalytics> {
|
): Promise<WorkflowAnalytics> {
|
||||||
const defaultWorkflowId = query.defaultWorkflowId ?? "builtin:coding";
|
const defaultWorkflowId = query.defaultWorkflowId ?? "builtin:coding";
|
||||||
if ("ping" in dbOrLayer) {
|
if ("ping" in dbOrLayer) {
|
||||||
return aggregateWorkflowAnalyticsAsync(dbOrLayer, query, defaultWorkflowId);
|
return aggregateWorkflowAnalyticsAsync(dbOrLayer, query, defaultWorkflowId, laneStore);
|
||||||
}
|
}
|
||||||
const db = dbOrLayer as Database;
|
const db = dbOrLayer as Database;
|
||||||
|
|
||||||
@@ -440,11 +458,62 @@ export async function aggregateWorkflowAnalytics(
|
|||||||
* returns it parsed) so it is re-stringified to feed the shared
|
* returns it parsed) so it is re-stringified to feed the shared
|
||||||
* countModifiedFiles helper unchanged.
|
* countModifiedFiles helper unchanged.
|
||||||
*/
|
*/
|
||||||
|
/* The legacy active ids are always retained: the classifier's fallback recognises them, so they can
|
||||||
|
never be the "selected but unclassifiable" case this filter exists to prevent. */
|
||||||
|
const LEGACY_ACTIVE_LANES: readonly string[] = ["in-progress", "in-review"];
|
||||||
|
|
||||||
async function aggregateWorkflowAnalyticsAsync(
|
async function aggregateWorkflowAnalyticsAsync(
|
||||||
layer: AsyncDataLayer,
|
layer: AsyncDataLayer,
|
||||||
query: WorkflowAnalyticsQuery,
|
query: WorkflowAnalyticsQuery,
|
||||||
defaultWorkflowId: string,
|
defaultWorkflowId: string,
|
||||||
|
laneStore?: ProjectLaneVocabularyStore,
|
||||||
): Promise<WorkflowAnalytics> {
|
): Promise<WorkflowAnalytics> {
|
||||||
|
/*
|
||||||
|
FNXC:PostgresCommandCenterAnalytics 2026-07-30-21:20 (#2866 review — greptile P1 x2, and the second
|
||||||
|
one is a self-contradiction rather than an imprecision):
|
||||||
|
|
||||||
|
THE SQL FILTER AND THE ROW CLASSIFIER MUST DROP THE SAME COLUMNS.
|
||||||
|
|
||||||
|
`resolveProjectColumnsForRoles` unions every column any workflow gives the role, so a column id two
|
||||||
|
workflows reuse with DIFFERENT traits stays in the filter. `columnFlagsByName` — built by the
|
||||||
|
Command Center route and consumed ~200 lines below — deliberately DROPS such an id as conflicting,
|
||||||
|
on the argument that a merged entry double-counts (see its own note, #2803 review).
|
||||||
|
|
||||||
|
The two together produce rows that are SELECTED and then classified by nothing:
|
||||||
|
`columnFlagsByName.get("checking")` is undefined, so `isWipColumnRole` and `isReviewColumnRole` both
|
||||||
|
fall back to the legacy ids, `checking` matches neither, and the count silently evaporates. Worse
|
||||||
|
than either half alone — the filter says the lane counts, the classifier says it cannot say, and the
|
||||||
|
operator sees a workflow sitting at zero.
|
||||||
|
|
||||||
|
Aligning on the CLASSIFIER's answer is the conservative direction: a column it refuses to judge is
|
||||||
|
removed from the filter too, so the rows are never selected rather than selected and discarded. The
|
||||||
|
totals are unchanged (they were already lost); what changes is that the two halves now agree, and a
|
||||||
|
reader is not left hunting for where the rows went.
|
||||||
|
|
||||||
|
CONSEQUENTLY THIS IS UNCOVERED, AND THAT IS INHERENT, NOT AN OMISSION. Mutating the filter away
|
||||||
|
leaves the renamed-lane suite green, because the counts were already zero on both sides — the only
|
||||||
|
observable difference is whether a workflow with no other rows appears with zeros or is absent.
|
||||||
|
A test pinning THAT would be asserting an artifact of , not the invariant. The
|
||||||
|
invariant worth covering is the one the first finding names, and it needs the per-workflow map.
|
||||||
|
|
||||||
|
The FIRST finding — a project-wide union admitting one workflow's rows into another's completed
|
||||||
|
count — is real and NOT fixed here: unlike `team-analytics`, these rows carry a workflow id, so a
|
||||||
|
per-workflow lane map is feasible and is the right fix. It needs the lane resolution keyed by
|
||||||
|
workflow rather than by project, which changes this function's contract with its caller. Recorded on
|
||||||
|
the PR rather than folded into a review-response commit.
|
||||||
|
*/
|
||||||
|
const droppedAsConflicting = (lane: string): boolean =>
|
||||||
|
query.columnFlagsByName !== undefined && !query.columnFlagsByName.has(lane);
|
||||||
|
const completeLanes = laneStore
|
||||||
|
? [...await resolveProjectColumnsForRoles(laneStore, ["complete"])]
|
||||||
|
: ["done"];
|
||||||
|
const activeLanes = (laneStore
|
||||||
|
? [...await resolveProjectColumnsForRoles(laneStore, ["countsTowardWip", "humanReview"])]
|
||||||
|
: ["in-progress", "in-review"]
|
||||||
|
).filter((lane) => LEGACY_ACTIVE_LANES.includes(lane) || !droppedAsConflicting(lane));
|
||||||
|
/* An IN list of bound parameters, not `= ANY(${array})`: drizzle expands a JS array in a template
|
||||||
|
into a tuple, which PostgreSQL rejects for ANY. Each id stays a parameter. */
|
||||||
|
const inList = (lanes: readonly string[]) => sql.join(lanes.map((lane) => sql`${lane}`), sql`, `);
|
||||||
const wfExpr = sql`COALESCE(NULLIF(s.workflow_id, ''), ${defaultWorkflowId})`;
|
const wfExpr = sql`COALESCE(NULLIF(s.workflow_id, ''), ${defaultWorkflowId})`;
|
||||||
|
|
||||||
const tokFrom = query.from !== undefined ? sql`AND t.token_usage_last_used_at >= ${query.from}` : sql``;
|
const tokFrom = query.from !== undefined ? sql`AND t.token_usage_last_used_at >= ${query.from}` : sql``;
|
||||||
@@ -484,7 +553,7 @@ async function aggregateWorkflowAnalyticsAsync(
|
|||||||
sql`SELECT ${wfExpr} AS "workflowId", count(*)::int AS count
|
sql`SELECT ${wfExpr} AS "workflowId", count(*)::int AS count
|
||||||
FROM project.tasks t
|
FROM project.tasks t
|
||||||
LEFT JOIN project.task_workflow_selection s ON s.task_id = t.id
|
LEFT JOIN project.task_workflow_selection s ON s.task_id = t.id
|
||||||
WHERE t."column" = 'done' AND t.column_moved_at IS NOT NULL ${compFrom} ${compTo}
|
WHERE t."column" IN (${inList(completeLanes)}) AND t.column_moved_at IS NOT NULL ${compFrom} ${compTo}
|
||||||
GROUP BY 1`,
|
GROUP BY 1`,
|
||||||
)) as Array<{ workflowId: string; count: number }>;
|
)) as Array<{ workflowId: string; count: number }>;
|
||||||
const completedRows: CountByWorkflowRow[] = completedRowsRaw.map((r) => ({
|
const completedRows: CountByWorkflowRow[] = completedRowsRaw.map((r) => ({
|
||||||
@@ -498,7 +567,7 @@ async function aggregateWorkflowAnalyticsAsync(
|
|||||||
sql`SELECT ${wfExpr} AS "workflowId", t."column" AS "columnName", count(*)::int AS count
|
sql`SELECT ${wfExpr} AS "workflowId", t."column" AS "columnName", count(*)::int AS count
|
||||||
FROM project.tasks t
|
FROM project.tasks t
|
||||||
LEFT JOIN project.task_workflow_selection s ON s.task_id = t.id
|
LEFT JOIN project.task_workflow_selection s ON s.task_id = t.id
|
||||||
WHERE t."column" IN ('in-progress', 'in-review') ${curFrom} ${curTo}
|
WHERE t."column" IN (${inList(activeLanes)}) ${curFrom} ${curTo}
|
||||||
GROUP BY 1, t."column"`,
|
GROUP BY 1, t."column"`,
|
||||||
)) as Array<{ workflowId: string; columnName: string; count: number }>;
|
)) as Array<{ workflowId: string; columnName: string; count: number }>;
|
||||||
const currentRows: Array<CountByWorkflowRow & { columnName: string }> = currentRowsRaw.map((r) => ({
|
const currentRows: Array<CountByWorkflowRow & { columnName: string }> = currentRowsRaw.map((r) => ({
|
||||||
|
|||||||
@@ -519,7 +519,9 @@ async function resolveColumnFlagsByName(
|
|||||||
pricingOverrides: settings.modelPricingOverrides,
|
pricingOverrides: settings.modelPricingOverrides,
|
||||||
defaultWorkflowId,
|
defaultWorkflowId,
|
||||||
columnFlagsByName: await resolveColumnFlagsByName(store),
|
columnFlagsByName: await resolveColumnFlagsByName(store),
|
||||||
});
|
/* FNXC:WorkflowResolvedColumns 2026-07-30-18:35: store supplied so the per-workflow completed
|
||||||
|
and in-flight queries resolve the board's real lanes instead of 'done'/'in-progress'. */
|
||||||
|
}, store);
|
||||||
if (wantsCsv(req.query)) {
|
if (wantsCsv(req.query)) {
|
||||||
sendCsv(res, "command-center-workflows.csv", workflowAnalyticsToTable(result));
|
sendCsv(res, "command-center-workflows.csv", workflowAnalyticsToTable(result));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
"packages/core/src/task-store/task-artifacts-ops.ts": 1,
|
"packages/core/src/task-store/task-artifacts-ops.ts": 1,
|
||||||
"packages/core/src/task-store/workflow-definitions.ts": 2,
|
"packages/core/src/task-store/workflow-definitions.ts": 2,
|
||||||
"packages/core/src/team-analytics.ts": 3,
|
"packages/core/src/team-analytics.ts": 3,
|
||||||
"packages/core/src/workflow-analytics.ts": 6
|
"packages/core/src/workflow-analytics.ts": 3
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user