@zaip-ai/cli 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/services/install-agents-template.d.ts.map +1 -1
- package/dist/services/install-agents-template.js +18 -2
- package/dist/services/install-agents-template.js.map +1 -1
- package/dist/services/template-manifest.d.ts.map +1 -1
- package/dist/services/template-manifest.js +2 -0
- package/dist/services/template-manifest.js.map +1 -1
- package/package.json +1 -1
- package/templates/.agents/architecture/.gitkeep +0 -0
- package/templates/.agents/branding/.gitkeep +0 -0
- package/templates/.agents/dashboard/.zaip-managed.json +63 -51
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-BZWjXQB-.js → CostCalculationGuidePage-DwVZOmMV.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-D8qqeIPE.js → DashboardHelpDialog-3Lrfq645.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-Cm12_A0v.js → PricingSettingsDialog-CyjMXkm8.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-DxO9F8B4.js → ProjectSettingsPage-B_Oj6Dlf.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-_x0twOcS.js → TraceabilityWorkspace-diq9g1L8.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-C09KmlSw.js +68 -0
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-CR4Fbf3g.js → icons-vendor-DXBOtWNI.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Deo_W81f.js +2 -0
- package/templates/.agents/dashboard/apps/frontend/dist/assets/index-hPPfVGQv.css +1 -0
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-DRJQbKnp.js → react-vendor-DaNXQPyk.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-CMrDYqjR.js → traceability-I4AKqVDX.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
- package/templates/.agents/dashboard/apps/frontend/src/App.tsx +43 -15
- package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +18 -3
- package/templates/.agents/dashboard/apps/frontend/src/dashboard-capability-nav.css +11 -0
- package/templates/.agents/dashboard/apps/frontend/src/operations-period-contract.test.ts +14 -0
- package/templates/.agents/dashboard/apps/frontend/src/operations-period.test.ts +22 -7
- package/templates/.agents/dashboard/apps/frontend/src/operations-period.ts +11 -2
- package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +7 -4
- package/templates/.agents/dashboard/modules/operations/src/react/OperationsFilteredStatusDialog.tsx +131 -0
- package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.helpers.tsx +2 -1
- package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.test.ts +12 -4
- package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +69 -157
- package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +19 -4
- package/templates/.agents/dashboard/modules/operations/src/react/operation-date-range.test.ts +25 -0
- package/templates/.agents/dashboard/modules/operations/src/react/operation-date-range.ts +29 -0
- package/templates/.agents/dashboard/modules/operations/src/react/operations-coverage-toast.test.ts +3 -2
- package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +12 -2
- package/templates/.agents/dashboard/modules/operations/src/react/operations-overview.status-cards.test.ts +13 -7
- package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +1 -1
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-prerequisites.mjs +40 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-prerequisites.test.mjs +38 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +33 -26
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +5 -8
- package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-loading.spec.ts +4 -6
- package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-pagination.spec.ts +4 -0
- package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-period-transition.spec.ts +43 -17
- package/templates/.agents/dashboard/tests/e2e/specs/dashboard-performance-matrix.spec.ts +9 -3
- package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +51 -22
- package/templates/.agents/growth/.gitkeep +0 -0
- package/templates/.agents/marketing/.gitkeep +0 -0
- package/templates/.agents/requirements/autonomous-assurance.md +1 -1
- package/templates/.agents/requirements/comparative-audit.md +1 -1
- package/templates/.agents/requirements/context-engine.md +1 -1
- package/templates/.agents/requirements/cost-observability.md +1 -1
- package/templates/.agents/requirements/dashboard-launch.md +1 -1
- package/templates/.agents/requirements/data-export.md +1 -1
- package/templates/.agents/requirements/design-system.md +1 -1
- package/templates/.agents/requirements/execution-engine.md +1 -1
- package/templates/.agents/requirements/kanban-dashboard.md +1 -1
- package/templates/.agents/requirements/model-orchestration.md +1 -1
- package/templates/.agents/requirements/public-distribution.md +1 -1
- package/templates/.agents/requirements/software-architecture.md +1 -1
- package/templates/.agents/requirements/strategic-improvements.md +1 -1
- package/templates/.agents/requirements/work-lifecycle.md +1 -1
- package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
- package/templates/.agents/sales/.gitkeep +0 -0
- package/templates/README.md +722 -0
- package/templates/template-manifest.json +96 -97
- package/templates/.agents/architecture/software-architecture.md +0 -131
- package/templates/.agents/branding/brandbook.docx +0 -0
- package/templates/.agents/branding/brandbook.md +0 -60
- package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Cpp8xFij.js +0 -68
- package/templates/.agents/dashboard/apps/frontend/dist/assets/index-B31GMG5O.css +0 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/index-ncC1rxap.js +0 -2
- package/templates/.agents/growth/estrategia-bootstrap-90-dias.md +0 -169
- package/templates/.agents/growth/strategy.md +0 -27
- package/templates/.agents/marketing/estrategia-marketing-zaip-90-dias.md +0 -197
- package/templates/.agents/marketing/strategy.md +0 -29
- package/templates/.agents/sales/playbook.md +0 -32
- package/templates/.agents/sales/roteiro-comercial-zaip.md +0 -252
|
@@ -39,6 +39,20 @@ describe("Operations period request contract", () => {
|
|
|
39
39
|
expect(requestPeriodForOperations({ preset: "all", range })).toEqual({ from: null, to: null });
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
it("keeps an All Time request unbounded when its displayed factual range changes", () => {
|
|
43
|
+
const initialDisplay = { start: "2026-09-01", end: "2026-09-01" };
|
|
44
|
+
const factualDisplay = { start: "2026-06-02", end: "2026-09-01" };
|
|
45
|
+
|
|
46
|
+
expect(requestPeriodForOperations({ preset: "all", range: initialDisplay })).toEqual({
|
|
47
|
+
from: null,
|
|
48
|
+
to: null,
|
|
49
|
+
});
|
|
50
|
+
expect(requestPeriodForOperations({ preset: "all", range: factualDisplay })).toEqual({
|
|
51
|
+
from: null,
|
|
52
|
+
to: null,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
42
56
|
it("keeps the latest Today/Yesterday selection when rapid requests resolve out of order", () => {
|
|
43
57
|
const today = { start: "2026-08-08", end: "2026-08-08" };
|
|
44
58
|
const yesterday = { start: "2026-08-07", end: "2026-08-07" };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
defaultOperationsPeriod,
|
|
4
|
+
isAllTimeDisplayRangeChange,
|
|
5
|
+
requestPeriodForOperations,
|
|
6
|
+
} from "./operations-period.js";
|
|
3
7
|
|
|
4
8
|
describe("dashboard default Operations period", () => {
|
|
5
|
-
it("opens in
|
|
6
|
-
expect(defaultOperationsPeriod.preset).toBe("
|
|
7
|
-
expect(requestPeriodForOperations(defaultOperationsPeriod)).toEqual({
|
|
8
|
-
from: defaultOperationsPeriod.range.start,
|
|
9
|
-
to: defaultOperationsPeriod.range.end,
|
|
10
|
-
});
|
|
9
|
+
it("opens in All Time without date bounds", () => {
|
|
10
|
+
expect(defaultOperationsPeriod.preset).toBe("all");
|
|
11
|
+
expect(requestPeriodForOperations(defaultOperationsPeriod)).toEqual({ from: null, to: null });
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
it("keeps an explicit Sao Paulo This Month selection bounded", () => {
|
|
@@ -18,4 +19,18 @@ describe("dashboard default Operations period", () => {
|
|
|
18
19
|
}),
|
|
19
20
|
).toEqual({ from: "2026-07-01", to: "2026-07-31" });
|
|
20
21
|
});
|
|
22
|
+
|
|
23
|
+
it("classifies only an All Time to All Time range synchronization as display-only", () => {
|
|
24
|
+
const allTime = { preset: "all" as const, range: { start: "2026-08-01", end: "2026-09-01" } };
|
|
25
|
+
|
|
26
|
+
expect(
|
|
27
|
+
isAllTimeDisplayRangeChange(allTime, {
|
|
28
|
+
preset: "all",
|
|
29
|
+
range: { start: "2026-06-02", end: "2026-09-01" },
|
|
30
|
+
}),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
expect(
|
|
33
|
+
isAllTimeDisplayRangeChange({ preset: "this-month", range: allTime.range }, allTime),
|
|
34
|
+
).toBe(false);
|
|
35
|
+
});
|
|
21
36
|
});
|
|
@@ -9,9 +9,10 @@ export interface OperationsPeriodState {
|
|
|
9
9
|
range: OperationDateRange;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/** Operations opens
|
|
12
|
+
/** Operations opens across the full available period for the dashboard overview. */
|
|
13
13
|
export const defaultOperationsPeriod: OperationsPeriodState = {
|
|
14
|
-
preset: "
|
|
14
|
+
preset: "all",
|
|
15
|
+
// All Time deliberately ignores these display bounds in requests.
|
|
15
16
|
range: dateRangeForPreset("this-month"),
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -24,3 +25,11 @@ export function requestPeriodForOperations(period: OperationsPeriodState): {
|
|
|
24
25
|
? { from: null, to: null }
|
|
25
26
|
: { from: period.range.start, to: period.range.end };
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
/** Updating All Time's factual display bounds must not invalidate its unbounded request. */
|
|
30
|
+
export function isAllTimeDisplayRangeChange(
|
|
31
|
+
current: OperationsPeriodState,
|
|
32
|
+
next: OperationsPeriodState,
|
|
33
|
+
): boolean {
|
|
34
|
+
return current.preset === "all" && next.preset === "all";
|
|
35
|
+
}
|
|
@@ -92,7 +92,7 @@ describe("atomic Cost snapshot loading", () => {
|
|
|
92
92
|
expect(app).toContain("useAppEffects({");
|
|
93
93
|
expect(effects).toContain("void refresh();");
|
|
94
94
|
expect(effects).toContain("refresh,");
|
|
95
|
-
expect(app).toContain("setOperationsPeriod(
|
|
95
|
+
expect(app).toContain("setOperationsPeriod(nextPeriod);");
|
|
96
96
|
expect(app).toContain("setCosts(null);");
|
|
97
97
|
expect(app).toContain("setDisplayedCosts(null);");
|
|
98
98
|
expect(app).not.toContain('api.costMonitor(currency, period, "detail")');
|
|
@@ -184,10 +184,13 @@ describe("atomic Cost snapshot loading", () => {
|
|
|
184
184
|
expect(periodChange.indexOf("setCosts(null);")).toBeGreaterThan(-1);
|
|
185
185
|
expect(periodChange.indexOf("setDisplayedCosts(null);")).toBeGreaterThan(-1);
|
|
186
186
|
expect(periodChange.indexOf("setCosts(null);")).toBeLessThan(
|
|
187
|
-
periodChange.
|
|
187
|
+
periodChange.lastIndexOf("setOperationsPeriod(nextPeriod);"),
|
|
188
188
|
);
|
|
189
189
|
expect(periodChange.indexOf("setDisplayedCosts(null);")).toBeLessThan(
|
|
190
|
-
periodChange.
|
|
190
|
+
periodChange.lastIndexOf("setOperationsPeriod(nextPeriod);"),
|
|
191
|
+
);
|
|
192
|
+
expect(periodChange).toContain(
|
|
193
|
+
"isAllTimeDisplayRangeChange(operationsPeriodRef.current, nextPeriod)",
|
|
191
194
|
);
|
|
192
195
|
expect(periodChange).toContain("setOperationsLoading(true);");
|
|
193
196
|
expect(periodChange).toContain('setLoadingArea("operations");');
|
|
@@ -201,7 +204,7 @@ describe("atomic Cost snapshot loading", () => {
|
|
|
201
204
|
expect(operationsPage).toContain("datePreset={operationsPeriod.preset}");
|
|
202
205
|
expect(operationsPage).toContain("dateRange={operationsPeriod.range}");
|
|
203
206
|
expect(operationsPage).toContain("onDateChange={(preset, range) => {");
|
|
204
|
-
expect(operationsPage).toContain("setOperationsPeriod(
|
|
207
|
+
expect(operationsPage).toContain("setOperationsPeriod(nextPeriod);");
|
|
205
208
|
});
|
|
206
209
|
|
|
207
210
|
it("keeps detail hydration stable when the quick Operations projection updates", () => {
|
package/templates/.agents/dashboard/modules/operations/src/react/OperationsFilteredStatusDialog.tsx
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { DashboardTaskProjection, MacroProjection } from "../contracts.js";
|
|
2
|
+
import type { OverviewStatusKind, OverviewStatusPage } from "./operations-page-contract.js";
|
|
3
|
+
import { type OperationsLocale, riskLabel } from "./status-labels.js";
|
|
4
|
+
|
|
5
|
+
export function OperationsFilteredStatusDialog({
|
|
6
|
+
eyebrow,
|
|
7
|
+
label,
|
|
8
|
+
locale,
|
|
9
|
+
onClose,
|
|
10
|
+
onOpenTask,
|
|
11
|
+
onOpenDocument,
|
|
12
|
+
items,
|
|
13
|
+
state,
|
|
14
|
+
kind = "task",
|
|
15
|
+
onLoadMore,
|
|
16
|
+
page,
|
|
17
|
+
}: {
|
|
18
|
+
eyebrow: string;
|
|
19
|
+
label: string;
|
|
20
|
+
locale: OperationsLocale;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
onOpenTask: (taskId: string) => void;
|
|
23
|
+
onOpenDocument: (kind: "prd" | "spec", id: string) => void;
|
|
24
|
+
items: readonly (DashboardTaskProjection | MacroProjection)[];
|
|
25
|
+
state?: { loading: boolean; error: string | null } | undefined;
|
|
26
|
+
kind?: OverviewStatusKind;
|
|
27
|
+
onLoadMore?: ((offset: number) => void) | undefined;
|
|
28
|
+
page?: OverviewStatusPage | undefined;
|
|
29
|
+
}) {
|
|
30
|
+
const total = page?.total ?? items.length;
|
|
31
|
+
const taskLabel = statusTypeLabel(kind, total, locale);
|
|
32
|
+
return (
|
|
33
|
+
<dialog
|
|
34
|
+
className="operations-status-dialog-backdrop"
|
|
35
|
+
onMouseDown={(event) => {
|
|
36
|
+
if (event.currentTarget === event.target) onClose();
|
|
37
|
+
}}
|
|
38
|
+
open
|
|
39
|
+
>
|
|
40
|
+
<section
|
|
41
|
+
aria-label={`${label} — ${total} ${taskLabel}`}
|
|
42
|
+
aria-modal="true"
|
|
43
|
+
className="operations-status-dialog"
|
|
44
|
+
role="dialog"
|
|
45
|
+
>
|
|
46
|
+
<header>
|
|
47
|
+
<div>
|
|
48
|
+
<span>{eyebrow}</span>
|
|
49
|
+
<h2>{label}</h2>
|
|
50
|
+
<p>
|
|
51
|
+
{total} {taskLabel}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
<button
|
|
55
|
+
aria-label={locale === "pt-BR" ? "Fechar" : "Close"}
|
|
56
|
+
onClick={onClose}
|
|
57
|
+
type="button"
|
|
58
|
+
>
|
|
59
|
+
×
|
|
60
|
+
</button>
|
|
61
|
+
</header>
|
|
62
|
+
<div className="operations-status-task-list">
|
|
63
|
+
{(page?.loading ?? state?.loading) && items.length === 0 ? (
|
|
64
|
+
<p aria-busy="true" aria-live="polite" className="operations-empty" role="status">
|
|
65
|
+
{locale === "pt-BR" ? "Carregando itens\u2026" : "Loading items\u2026"}
|
|
66
|
+
</p>
|
|
67
|
+
) : null}
|
|
68
|
+
{(page?.error ?? state?.error) ? (
|
|
69
|
+
<p aria-live="assertive" className="operations-empty" role="alert">
|
|
70
|
+
{page?.error ?? state?.error}
|
|
71
|
+
</p>
|
|
72
|
+
) : null}
|
|
73
|
+
{items.map((item) => (
|
|
74
|
+
<button
|
|
75
|
+
key={item.id}
|
|
76
|
+
onClick={() =>
|
|
77
|
+
"change_id" in item ? onOpenTask(item.id) : onOpenDocument(item.kind, item.id)
|
|
78
|
+
}
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
<span className="operations-status-task-main">
|
|
82
|
+
<strong>{item.title}</strong>
|
|
83
|
+
<small>{item.id}</small>
|
|
84
|
+
</span>
|
|
85
|
+
<span
|
|
86
|
+
className={
|
|
87
|
+
"change_id" in item
|
|
88
|
+
? `operations-risk is-${item.risk}`
|
|
89
|
+
: "operations-work-kind is-macro"
|
|
90
|
+
}
|
|
91
|
+
>
|
|
92
|
+
{"change_id" in item ? riskLabel(item.risk, locale) : item.kind.toUpperCase()}
|
|
93
|
+
</span>
|
|
94
|
+
<span className="operations-status-task-action">
|
|
95
|
+
{locale === "pt-BR" ? "Ver detalhes" : "View details"} →
|
|
96
|
+
</span>
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
{items.length === 0 && !(page?.loading ?? state?.loading) ? (
|
|
100
|
+
<p className="operations-empty">
|
|
101
|
+
{locale === "pt-BR"
|
|
102
|
+
? `Nenhum${kind === "task" ? "a" : ""} ${statusTypeLabel(kind, 1, locale)} neste status.`
|
|
103
|
+
: `No ${statusTypeLabel(kind, 2, locale)} in this status.`}
|
|
104
|
+
</p>
|
|
105
|
+
) : null}
|
|
106
|
+
{onLoadMore && items.length < (page?.total ?? 0) ? (
|
|
107
|
+
<button disabled={page?.loading} onClick={() => onLoadMore(items.length)} type="button">
|
|
108
|
+
{page?.loading
|
|
109
|
+
? locale === "pt-BR"
|
|
110
|
+
? "Carregando…"
|
|
111
|
+
: "Loading…"
|
|
112
|
+
: locale === "pt-BR"
|
|
113
|
+
? "Carregar mais"
|
|
114
|
+
: "Load more"}
|
|
115
|
+
</button>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
</section>
|
|
119
|
+
</dialog>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function statusTypeLabel(
|
|
124
|
+
kind: OverviewStatusKind,
|
|
125
|
+
count: number,
|
|
126
|
+
locale: OperationsLocale,
|
|
127
|
+
): string {
|
|
128
|
+
if (kind === "prd") return count === 1 ? "PRD" : "PRDs";
|
|
129
|
+
if (kind === "spec") return count === 1 ? "SPEC" : "SPECs";
|
|
130
|
+
return locale === "pt-BR" ? (count === 1 ? "tarefa" : "tarefas") : count === 1 ? "task" : "tasks";
|
|
131
|
+
}
|
package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.helpers.tsx
CHANGED
|
@@ -537,7 +537,8 @@ export function formatExecutionAverageHint(
|
|
|
537
537
|
totalDays: number,
|
|
538
538
|
locale: OperationsLocale,
|
|
539
539
|
): string {
|
|
540
|
-
if (
|
|
540
|
+
if (totalTasks <= 0) return "0";
|
|
541
|
+
if (totalDurationMs <= 0 || totalDays <= 0) {
|
|
541
542
|
return locale === "pt-BR"
|
|
542
543
|
? "Médias de execução indisponíveis"
|
|
543
544
|
: "Execution averages unavailable";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
findLatestCreatedAt,
|
|
13
13
|
findLatestTaskCreatedAt,
|
|
14
14
|
formatExecutionAverageHint,
|
|
15
|
+
formatSummaryMetricValue,
|
|
15
16
|
formatTokenThroughputHint,
|
|
16
17
|
macroClassificationStats,
|
|
17
18
|
macroStatusDistribution,
|
|
@@ -278,11 +279,12 @@ describe("execution average summary", () => {
|
|
|
278
279
|
);
|
|
279
280
|
});
|
|
280
281
|
|
|
281
|
-
it("localizes English and
|
|
282
|
+
it("localizes English and shows zero for an empty population", () => {
|
|
282
283
|
expect(formatExecutionAverageHint(3_600_000, 3, 1, "en-US")).toBe(
|
|
283
284
|
"20 min / task (1 h / day) (1 day)",
|
|
284
285
|
);
|
|
285
|
-
expect(formatExecutionAverageHint(0, 0, 0, "pt-BR")).toBe("
|
|
286
|
+
expect(formatExecutionAverageHint(0, 0, 0, "pt-BR")).toBe("0");
|
|
287
|
+
expect(formatExecutionAverageHint(0, 0, 0, "en-US")).toBe("0");
|
|
286
288
|
});
|
|
287
289
|
});
|
|
288
290
|
|
|
@@ -351,14 +353,20 @@ describe("overviewMetricAverages", () => {
|
|
|
351
353
|
});
|
|
352
354
|
});
|
|
353
355
|
|
|
354
|
-
it("
|
|
356
|
+
it("keeps unknown totals null internally and renders zero only for an empty population", () => {
|
|
355
357
|
const averages = overviewMetricAverages({ durationMs: null, tokens: null, costBrl: null }, 3);
|
|
356
358
|
expect(averages).toEqual({ durationMs: null, tokens: null, costBrl: null });
|
|
357
359
|
const source = readOverviewSource();
|
|
358
360
|
expect(source).toContain(
|
|
359
361
|
"durationMs: totalDurationMetric, tokens: totalTokensMetric, costBrl: totalCostMetric",
|
|
360
362
|
);
|
|
361
|
-
expect(
|
|
363
|
+
expect(formatSummaryMetricValue(null, 0, (value) => String(value), "pt-BR")).toBe("0");
|
|
364
|
+
expect(formatSummaryMetricValue(null, 0, (value) => String(value), "en-US")).toBe("0");
|
|
365
|
+
expect(formatSummaryMetricValue(null, 1, (value) => String(value), "pt-BR")).toBe(
|
|
366
|
+
"Indisponível",
|
|
367
|
+
);
|
|
368
|
+
expect(formatSummaryMetricValue(42, 0, (value) => String(value), "pt-BR")).toBe("42");
|
|
369
|
+
expect(source).toContain("formatSummaryMetricValue(\n averageDuration,");
|
|
362
370
|
expect(source).not.toContain("executionMetrics.durationMs ?? 0");
|
|
363
371
|
});
|
|
364
372
|
});
|
|
@@ -6,12 +6,9 @@ import {
|
|
|
6
6
|
formatPercent,
|
|
7
7
|
} from "@zaip-ai/dashboard-ui";
|
|
8
8
|
import { type ReactNode, useState } from "react";
|
|
9
|
-
import type {
|
|
10
|
-
DashboardTaskProjection,
|
|
11
|
-
MacroProjection,
|
|
12
|
-
OperationsProjection,
|
|
13
|
-
} from "../contracts.js";
|
|
9
|
+
import type { MacroProjection, OperationsProjection } from "../contracts.js";
|
|
14
10
|
import { allPhaseMetricTotals, executionMetricTotals } from "./execution-metrics.js";
|
|
11
|
+
import { OperationsFilteredStatusDialog as FilteredStatusDialog } from "./OperationsFilteredStatusDialog.js";
|
|
15
12
|
import type { EntityMetric } from "./OperationsOverview.helpers.js";
|
|
16
13
|
import {
|
|
17
14
|
aggregateMacroMetrics,
|
|
@@ -98,6 +95,20 @@ function formatEntityDuration(value: number | null, locale: OperationsLocale): s
|
|
|
98
95
|
if (value !== null) return formatDurationMs(value, locale);
|
|
99
96
|
return locale === "pt-BR" ? "Indisponível" : "Unavailable";
|
|
100
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Keeps missing source facts as null while making a genuinely empty selected
|
|
101
|
+
* population readable as the factual numeric zero in the overview.
|
|
102
|
+
*/
|
|
103
|
+
export function formatSummaryMetricValue(
|
|
104
|
+
value: number | null,
|
|
105
|
+
populationCount: number | null,
|
|
106
|
+
formatKnownValue: (knownValue: number) => string,
|
|
107
|
+
locale: OperationsLocale,
|
|
108
|
+
): string {
|
|
109
|
+
if (value !== null) return formatKnownValue(value);
|
|
110
|
+
return populationCount === 0 ? "0" : formatEntityDuration(null, locale);
|
|
111
|
+
}
|
|
101
112
|
export function OperationsOverview({
|
|
102
113
|
currency,
|
|
103
114
|
data,
|
|
@@ -399,18 +410,32 @@ export function OperationsOverview({
|
|
|
399
410
|
: findLatestCreatedAt(tasks)
|
|
400
411
|
: (selectedMacroAggregate?.latest_created_at ?? findLatestCreatedAt(selectedMacros));
|
|
401
412
|
const metricStats = [
|
|
402
|
-
[
|
|
413
|
+
[
|
|
414
|
+
text.averageTime,
|
|
415
|
+
formatSummaryMetricValue(
|
|
416
|
+
averageDuration,
|
|
417
|
+
selectedCount,
|
|
418
|
+
(value) => formatDurationMs(value, locale),
|
|
419
|
+
locale,
|
|
420
|
+
),
|
|
421
|
+
],
|
|
403
422
|
[
|
|
404
423
|
text.averageTokens,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
424
|
+
formatSummaryMetricValue(
|
|
425
|
+
averageTokens,
|
|
426
|
+
selectedCount,
|
|
427
|
+
(value) => formatCompactNumber(value, locale),
|
|
428
|
+
locale,
|
|
429
|
+
),
|
|
408
430
|
],
|
|
409
431
|
[
|
|
410
432
|
text.averageCost,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
433
|
+
formatSummaryMetricValue(
|
|
434
|
+
averageCost,
|
|
435
|
+
selectedCount,
|
|
436
|
+
(value) => formatCurrency(value, currency, locale),
|
|
437
|
+
locale,
|
|
438
|
+
),
|
|
414
439
|
],
|
|
415
440
|
] as const;
|
|
416
441
|
const aggregateTopTasks: EntityMetric[] = (overviewMetrics?.top_tasks ?? []).map((metric) => ({
|
|
@@ -580,14 +605,23 @@ export function OperationsOverview({
|
|
|
580
605
|
? `${text.totalExecutionTime} de ${copy.shortPlural}`
|
|
581
606
|
: `${copy.shortPlural} ${text.totalExecutionTime.toLowerCase()}`
|
|
582
607
|
}
|
|
583
|
-
value={
|
|
608
|
+
value={formatSummaryMetricValue(
|
|
609
|
+
totalDurationMetric,
|
|
610
|
+
metricPopulationCount,
|
|
611
|
+
(value) => formatDurationMs(value, locale),
|
|
612
|
+
locale,
|
|
613
|
+
)}
|
|
584
614
|
/>
|
|
585
615
|
<Kpi
|
|
586
616
|
hint={
|
|
587
617
|
tokensPerTask === null || tokensPerHour === null
|
|
588
|
-
?
|
|
589
|
-
?
|
|
590
|
-
|
|
618
|
+
? metricPopulationCount === 0
|
|
619
|
+
? locale === "pt-BR"
|
|
620
|
+
? `0 / ${metricUnit} (0 / hora)`
|
|
621
|
+
: `0 / ${metricUnit} (0 / hour)`
|
|
622
|
+
: locale === "pt-BR"
|
|
623
|
+
? "Tokens por tarefa e por hora indisponíveis"
|
|
624
|
+
: "Tokens per task and hour unavailable"
|
|
591
625
|
: locale === "pt-BR"
|
|
592
626
|
? `${formatCompactNumber(tokensPerTask, locale)} / ${metricUnit} (${formatCompactNumber(tokensPerHour, locale, 0)} / hora)`
|
|
593
627
|
: `${formatCompactNumber(tokensPerTask, locale)} / ${metricUnit} (${formatCompactNumber(tokensPerHour, locale, 0)} / hour)`
|
|
@@ -601,18 +635,23 @@ export function OperationsOverview({
|
|
|
601
635
|
? `Tokens gastos por ${copy.shortPlural}`
|
|
602
636
|
: `${copy.shortPlural} tokens spent`
|
|
603
637
|
}
|
|
604
|
-
value={
|
|
605
|
-
totalTokensMetric
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
638
|
+
value={formatSummaryMetricValue(
|
|
639
|
+
totalTokensMetric,
|
|
640
|
+
metricPopulationCount,
|
|
641
|
+
(value) => formatCompactNumber(value, locale),
|
|
642
|
+
locale,
|
|
643
|
+
)}
|
|
609
644
|
/>
|
|
610
645
|
<Kpi
|
|
611
646
|
hint={
|
|
612
647
|
hourlyCost === null || costPerTask === null
|
|
613
|
-
?
|
|
614
|
-
?
|
|
615
|
-
|
|
648
|
+
? metricPopulationCount === 0
|
|
649
|
+
? locale === "pt-BR"
|
|
650
|
+
? `${formatRoundedCurrency(0, currency, locale)} / ${metricUnit} (${formatRoundedCurrency(0, currency, locale)} / hora)`
|
|
651
|
+
: `${formatRoundedCurrency(0, currency, locale)} / ${metricUnit} (${formatRoundedCurrency(0, currency, locale)} / hour)`
|
|
652
|
+
: locale === "pt-BR"
|
|
653
|
+
? "Custo por tarefa e por hora indispon\u00edvel"
|
|
654
|
+
: "Cost per task and hour unavailable"
|
|
616
655
|
: locale === "pt-BR"
|
|
617
656
|
? `${formatRoundedCurrency(costPerTask, currency, locale)} / ${metricUnit} (${formatRoundedCurrency(hourlyCost, currency, locale)} / hora)`
|
|
618
657
|
: `${formatRoundedCurrency(costPerTask, currency, locale)} / ${metricUnit} (${formatRoundedCurrency(hourlyCost, currency, locale)} / hour)`
|
|
@@ -626,11 +665,12 @@ export function OperationsOverview({
|
|
|
626
665
|
? `Custo de ${copy.shortPlural}`
|
|
627
666
|
: `${copy.shortPlural} cost`
|
|
628
667
|
}
|
|
629
|
-
value={
|
|
630
|
-
totalCostMetric
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
668
|
+
value={formatSummaryMetricValue(
|
|
669
|
+
totalCostMetric,
|
|
670
|
+
metricPopulationCount,
|
|
671
|
+
(value) => formatRoundedCurrency(value, currency, locale),
|
|
672
|
+
locale,
|
|
673
|
+
)}
|
|
634
674
|
/>
|
|
635
675
|
</div>
|
|
636
676
|
|
|
@@ -872,134 +912,6 @@ export function OperationsOverview({
|
|
|
872
912
|
);
|
|
873
913
|
}
|
|
874
914
|
|
|
875
|
-
function FilteredStatusDialog({
|
|
876
|
-
eyebrow,
|
|
877
|
-
label,
|
|
878
|
-
locale,
|
|
879
|
-
onClose,
|
|
880
|
-
onOpenTask,
|
|
881
|
-
onOpenDocument,
|
|
882
|
-
items,
|
|
883
|
-
state,
|
|
884
|
-
kind = "task",
|
|
885
|
-
onLoadMore,
|
|
886
|
-
page,
|
|
887
|
-
}: {
|
|
888
|
-
eyebrow: string;
|
|
889
|
-
label: string;
|
|
890
|
-
locale: OperationsLocale;
|
|
891
|
-
onClose: () => void;
|
|
892
|
-
onOpenTask: (taskId: string) => void;
|
|
893
|
-
onOpenDocument: (kind: "prd" | "spec", id: string) => void;
|
|
894
|
-
items: readonly (DashboardTaskProjection | MacroProjection)[];
|
|
895
|
-
state?: { loading: boolean; error: string | null } | undefined;
|
|
896
|
-
kind?: OverviewStatusKind;
|
|
897
|
-
onLoadMore?: ((offset: number) => void) | undefined;
|
|
898
|
-
page?: OverviewStatusPage | undefined;
|
|
899
|
-
}) {
|
|
900
|
-
const total = page?.total ?? items.length;
|
|
901
|
-
const taskLabel = statusTypeLabel(kind, total, locale);
|
|
902
|
-
return (
|
|
903
|
-
<dialog
|
|
904
|
-
className="operations-status-dialog-backdrop"
|
|
905
|
-
onMouseDown={(event) => {
|
|
906
|
-
if (event.currentTarget === event.target) onClose();
|
|
907
|
-
}}
|
|
908
|
-
open
|
|
909
|
-
>
|
|
910
|
-
<section
|
|
911
|
-
aria-label={`${label} — ${total} ${taskLabel}`}
|
|
912
|
-
aria-modal="true"
|
|
913
|
-
className="operations-status-dialog"
|
|
914
|
-
role="dialog"
|
|
915
|
-
>
|
|
916
|
-
<header>
|
|
917
|
-
<div>
|
|
918
|
-
<span>{eyebrow}</span>
|
|
919
|
-
<h2>{label}</h2>
|
|
920
|
-
<p>
|
|
921
|
-
{total} {taskLabel}
|
|
922
|
-
</p>
|
|
923
|
-
</div>
|
|
924
|
-
<button
|
|
925
|
-
aria-label={locale === "pt-BR" ? "Fechar" : "Close"}
|
|
926
|
-
onClick={onClose}
|
|
927
|
-
type="button"
|
|
928
|
-
>
|
|
929
|
-
×
|
|
930
|
-
</button>
|
|
931
|
-
</header>
|
|
932
|
-
<div className="operations-status-task-list">
|
|
933
|
-
{(page?.loading ?? state?.loading) && items.length === 0 ? (
|
|
934
|
-
<p aria-busy="true" aria-live="polite" className="operations-empty" role="status">
|
|
935
|
-
{locale === "pt-BR" ? "Carregando itens\u2026" : "Loading items\u2026"}
|
|
936
|
-
</p>
|
|
937
|
-
) : null}
|
|
938
|
-
{(page?.error ?? state?.error) ? (
|
|
939
|
-
<p aria-live="assertive" className="operations-empty" role="alert">
|
|
940
|
-
{page?.error ?? state?.error}
|
|
941
|
-
</p>
|
|
942
|
-
) : null}
|
|
943
|
-
{items.map((item) => (
|
|
944
|
-
<button
|
|
945
|
-
key={item.id}
|
|
946
|
-
onClick={() =>
|
|
947
|
-
"change_id" in item ? onOpenTask(item.id) : onOpenDocument(item.kind, item.id)
|
|
948
|
-
}
|
|
949
|
-
type="button"
|
|
950
|
-
>
|
|
951
|
-
<span className="operations-status-task-main">
|
|
952
|
-
<strong>{item.title}</strong>
|
|
953
|
-
<small>{item.id}</small>
|
|
954
|
-
</span>
|
|
955
|
-
<span
|
|
956
|
-
className={
|
|
957
|
-
"change_id" in item
|
|
958
|
-
? `operations-risk is-${item.risk}`
|
|
959
|
-
: "operations-work-kind is-macro"
|
|
960
|
-
}
|
|
961
|
-
>
|
|
962
|
-
{"change_id" in item ? riskLabel(item.risk, locale) : item.kind.toUpperCase()}
|
|
963
|
-
</span>
|
|
964
|
-
<span className="operations-status-task-action">
|
|
965
|
-
{locale === "pt-BR" ? "Ver detalhes" : "View details"} →
|
|
966
|
-
</span>
|
|
967
|
-
</button>
|
|
968
|
-
))}
|
|
969
|
-
{items.length === 0 && !(page?.loading ?? state?.loading) ? (
|
|
970
|
-
<p className="operations-empty">
|
|
971
|
-
{locale === "pt-BR"
|
|
972
|
-
? `Nenhum${kind === "task" ? "a" : ""} ${statusTypeLabel(kind, 1, locale)} neste status.`
|
|
973
|
-
: `No ${statusTypeLabel(kind, 2, locale)} in this status.`}
|
|
974
|
-
</p>
|
|
975
|
-
) : null}
|
|
976
|
-
{onLoadMore && items.length < (page?.total ?? 0) ? (
|
|
977
|
-
<button disabled={page?.loading} onClick={() => onLoadMore(items.length)} type="button">
|
|
978
|
-
{page?.loading
|
|
979
|
-
? locale === "pt-BR"
|
|
980
|
-
? "Carregando…"
|
|
981
|
-
: "Loading…"
|
|
982
|
-
: locale === "pt-BR"
|
|
983
|
-
? "Carregar mais"
|
|
984
|
-
: "Load more"}
|
|
985
|
-
</button>
|
|
986
|
-
) : null}
|
|
987
|
-
</div>
|
|
988
|
-
</section>
|
|
989
|
-
</dialog>
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function statusTypeLabel(
|
|
994
|
-
kind: OverviewStatusKind,
|
|
995
|
-
count: number,
|
|
996
|
-
locale: OperationsLocale,
|
|
997
|
-
): string {
|
|
998
|
-
if (kind === "prd") return count === 1 ? "PRD" : "PRDs";
|
|
999
|
-
if (kind === "spec") return count === 1 ? "SPEC" : "SPECs";
|
|
1000
|
-
return locale === "pt-BR" ? (count === 1 ? "tarefa" : "tarefas") : count === 1 ? "task" : "tasks";
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
915
|
function PanelHeader({ title, hint, action }: { title: string; hint: string; action?: ReactNode }) {
|
|
1004
916
|
return (
|
|
1005
917
|
<header className="operations-panel-header">
|
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
|
|
26
26
|
import {
|
|
27
27
|
dateRangeForPreset,
|
|
28
|
+
factualCostDateRange,
|
|
28
29
|
initialOperationsDateRange,
|
|
29
30
|
isDateInOperationPreset,
|
|
30
31
|
isRunInOperationPreset,
|
|
@@ -130,10 +131,13 @@ export function OperationsPage({
|
|
|
130
131
|
const [filterLoading, setFilterLoading] = useState(false);
|
|
131
132
|
const filterLoadingTimer = useRef<number | null>(null);
|
|
132
133
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
133
|
-
const defaultDateRange = useMemo(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
const defaultDateRange = useMemo(() => {
|
|
135
|
+
const operationsRange = initialOperationsDateRange(
|
|
136
|
+
data.tasks,
|
|
137
|
+
Object.keys(data.summary.periods?.daily ?? {}),
|
|
138
|
+
);
|
|
139
|
+
return factualCostDateRange(factualCostPeriods?.day, operationsRange);
|
|
140
|
+
}, [data.summary.periods, data.tasks, factualCostPeriods?.day]);
|
|
137
141
|
const initialDateRange = useMemo(() => dateRangeForPreset("this-month"), []);
|
|
138
142
|
const [localDateRange, setLocalDateRange] = useState(() => initialDateRange);
|
|
139
143
|
const [localDatePreset, setLocalDatePreset] = useState<OperationDatePreset>("all");
|
|
@@ -156,6 +160,17 @@ export function OperationsPage({
|
|
|
156
160
|
},
|
|
157
161
|
[isPeriodControlled, onDateChange, onPeriodChange],
|
|
158
162
|
);
|
|
163
|
+
// A controlled parent initially owns the range selected before the factual
|
|
164
|
+
// Cost summary arrives. Once it does, synchronize only All Time's displayed
|
|
165
|
+
// coverage range; requestPeriodForOperations still omits both bounds.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (
|
|
168
|
+
datePreset !== "all" ||
|
|
169
|
+
(dateRange.start === defaultDateRange.start && dateRange.end === defaultDateRange.end)
|
|
170
|
+
)
|
|
171
|
+
return;
|
|
172
|
+
updatePeriod("all", defaultDateRange);
|
|
173
|
+
}, [datePreset, dateRange.end, dateRange.start, defaultDateRange, updatePeriod]);
|
|
159
174
|
useEffect(() => {
|
|
160
175
|
if (!isPeriodControlled && datePreset === "all") setLocalDateRange(defaultDateRange);
|
|
161
176
|
}, [datePreset, defaultDateRange, isPeriodControlled]);
|