@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.
Files changed (81) hide show
  1. package/dist/services/install-agents-template.d.ts.map +1 -1
  2. package/dist/services/install-agents-template.js +18 -2
  3. package/dist/services/install-agents-template.js.map +1 -1
  4. package/dist/services/template-manifest.d.ts.map +1 -1
  5. package/dist/services/template-manifest.js +2 -0
  6. package/dist/services/template-manifest.js.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/.agents/architecture/.gitkeep +0 -0
  9. package/templates/.agents/branding/.gitkeep +0 -0
  10. package/templates/.agents/dashboard/.zaip-managed.json +63 -51
  11. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-BZWjXQB-.js → CostCalculationGuidePage-DwVZOmMV.js} +1 -1
  12. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-D8qqeIPE.js → DashboardHelpDialog-3Lrfq645.js} +1 -1
  13. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-Cm12_A0v.js → PricingSettingsDialog-CyjMXkm8.js} +1 -1
  14. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-DxO9F8B4.js → ProjectSettingsPage-B_Oj6Dlf.js} +1 -1
  15. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-_x0twOcS.js → TraceabilityWorkspace-diq9g1L8.js} +1 -1
  16. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-C09KmlSw.js +68 -0
  17. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-CR4Fbf3g.js → icons-vendor-DXBOtWNI.js} +1 -1
  18. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Deo_W81f.js +2 -0
  19. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-hPPfVGQv.css +1 -0
  20. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-DRJQbKnp.js → react-vendor-DaNXQPyk.js} +1 -1
  21. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-CMrDYqjR.js → traceability-I4AKqVDX.js} +1 -1
  22. package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
  23. package/templates/.agents/dashboard/apps/frontend/src/App.tsx +43 -15
  24. package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +18 -3
  25. package/templates/.agents/dashboard/apps/frontend/src/dashboard-capability-nav.css +11 -0
  26. package/templates/.agents/dashboard/apps/frontend/src/operations-period-contract.test.ts +14 -0
  27. package/templates/.agents/dashboard/apps/frontend/src/operations-period.test.ts +22 -7
  28. package/templates/.agents/dashboard/apps/frontend/src/operations-period.ts +11 -2
  29. package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +7 -4
  30. package/templates/.agents/dashboard/modules/operations/src/react/OperationsFilteredStatusDialog.tsx +131 -0
  31. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.helpers.tsx +2 -1
  32. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.test.ts +12 -4
  33. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +69 -157
  34. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +19 -4
  35. package/templates/.agents/dashboard/modules/operations/src/react/operation-date-range.test.ts +25 -0
  36. package/templates/.agents/dashboard/modules/operations/src/react/operation-date-range.ts +29 -0
  37. package/templates/.agents/dashboard/modules/operations/src/react/operations-coverage-toast.test.ts +3 -2
  38. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +12 -2
  39. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview.status-cards.test.ts +13 -7
  40. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +1 -1
  41. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-prerequisites.mjs +40 -0
  42. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-prerequisites.test.mjs +38 -0
  43. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +33 -26
  44. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +5 -8
  45. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-loading.spec.ts +4 -6
  46. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-pagination.spec.ts +4 -0
  47. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-period-transition.spec.ts +43 -17
  48. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-performance-matrix.spec.ts +9 -3
  49. package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +51 -22
  50. package/templates/.agents/growth/.gitkeep +0 -0
  51. package/templates/.agents/marketing/.gitkeep +0 -0
  52. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  53. package/templates/.agents/requirements/comparative-audit.md +1 -1
  54. package/templates/.agents/requirements/context-engine.md +1 -1
  55. package/templates/.agents/requirements/cost-observability.md +1 -1
  56. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  57. package/templates/.agents/requirements/data-export.md +1 -1
  58. package/templates/.agents/requirements/design-system.md +1 -1
  59. package/templates/.agents/requirements/execution-engine.md +1 -1
  60. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  61. package/templates/.agents/requirements/model-orchestration.md +1 -1
  62. package/templates/.agents/requirements/public-distribution.md +1 -1
  63. package/templates/.agents/requirements/software-architecture.md +1 -1
  64. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  65. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  66. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  67. package/templates/.agents/sales/.gitkeep +0 -0
  68. package/templates/README.md +722 -0
  69. package/templates/template-manifest.json +96 -97
  70. package/templates/.agents/architecture/software-architecture.md +0 -131
  71. package/templates/.agents/branding/brandbook.docx +0 -0
  72. package/templates/.agents/branding/brandbook.md +0 -60
  73. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Cpp8xFij.js +0 -68
  74. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-B31GMG5O.css +0 -1
  75. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-ncC1rxap.js +0 -2
  76. package/templates/.agents/growth/estrategia-bootstrap-90-dias.md +0 -169
  77. package/templates/.agents/growth/strategy.md +0 -27
  78. package/templates/.agents/marketing/estrategia-marketing-zaip-90-dias.md +0 -197
  79. package/templates/.agents/marketing/strategy.md +0 -29
  80. package/templates/.agents/sales/playbook.md +0 -32
  81. 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 { defaultOperationsPeriod, requestPeriodForOperations } from "./operations-period.js";
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 This Month with its visible date bounds", () => {
6
- expect(defaultOperationsPeriod.preset).toBe("this-month");
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 on the current Sao Paulo month, like every dashboard tab. */
12
+ /** Operations opens across the full available period for the dashboard overview. */
13
13
  export const defaultOperationsPeriod: OperationsPeriodState = {
14
- preset: "this-month",
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({ preset, range });");
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.indexOf("setOperationsPeriod({ preset, range });"),
187
+ periodChange.lastIndexOf("setOperationsPeriod(nextPeriod);"),
188
188
  );
189
189
  expect(periodChange.indexOf("setDisplayedCosts(null);")).toBeLessThan(
190
- periodChange.indexOf("setOperationsPeriod({ preset, range });"),
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({ preset, range });");
207
+ expect(operationsPage).toContain("setOperationsPeriod(nextPeriod);");
205
208
  });
206
209
 
207
210
  it("keeps detail hydration stable when the quick Operations projection updates", () => {
@@ -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
+ }
@@ -537,7 +537,8 @@ export function formatExecutionAverageHint(
537
537
  totalDays: number,
538
538
  locale: OperationsLocale,
539
539
  ): string {
540
- if (totalDurationMs <= 0 || totalTasks <= 0 || totalDays <= 0) {
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 preserves unavailable coverage", () => {
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("Médias de execução indisponíveis");
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("passes unknown totals through as unavailable instead of coercing them to zero", () => {
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(source).toContain("formatEntityDuration(averageDuration, locale)");
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
- [text.averageTime, formatEntityDuration(averageDuration, locale)],
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
- averageTokens === null
406
- ? formatEntityDuration(null, locale)
407
- : formatCompactNumber(averageTokens, locale),
424
+ formatSummaryMetricValue(
425
+ averageTokens,
426
+ selectedCount,
427
+ (value) => formatCompactNumber(value, locale),
428
+ locale,
429
+ ),
408
430
  ],
409
431
  [
410
432
  text.averageCost,
411
- averageCost === null
412
- ? formatEntityDuration(null, locale)
413
- : formatCurrency(averageCost, currency, locale),
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={formatEntityDuration(totalDurationMetric, locale)}
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
- ? locale === "pt-BR"
589
- ? "Tokens por tarefa e por hora indisponíveis"
590
- : "Tokens per task and hour unavailable"
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 === null
606
- ? formatEntityDuration(null, locale)
607
- : formatCompactNumber(totalTokensMetric, locale)
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
- ? locale === "pt-BR"
614
- ? "Custo por tarefa e por hora indispon\u00edvel"
615
- : "Cost per task and hour unavailable"
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 === null
631
- ? formatEntityDuration(null, locale)
632
- : formatRoundedCurrency(totalCostMetric, currency, locale)
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
- () => initialOperationsDateRange(data.tasks, Object.keys(data.summary.periods?.daily ?? {})),
135
- [data.summary.periods, data.tasks],
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]);