@zaip-ai/cli 1.3.2 → 1.3.5

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 (98) hide show
  1. package/LICENSE +6 -3
  2. package/README.md +8 -2
  3. package/dist/services/codex-pre-tool-hook-template.js +1 -1
  4. package/dist/services/runtime-template.test.js +9 -8
  5. package/dist/services/runtime-template.test.js.map +1 -1
  6. package/package.json +2 -2
  7. package/templates/.agents/dashboard/.zaip-managed.json +414 -394
  8. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-ovxurkIk.js → CostCalculationGuidePage-D1mpyYwh.js} +1 -1
  9. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-BXy6nti1.js → DashboardHelpDialog-CKO47D5s.js} +30 -1
  10. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-vSvvOHQp.js → PricingSettingsDialog-BuSlCbyZ.js} +1 -1
  11. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-zH758vfs.js → ProjectSettingsPage-CUHFkAhZ.js} +1 -1
  12. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-CTH2wDs5.js → TraceabilityWorkspace-BmASq2gW.js} +1 -1
  13. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Csj6LiAt.js +68 -0
  14. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-B_lDDHn1.js → icons-vendor-VyTTl_ph.js} +1 -1
  15. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DIwDvCu5.js +2 -0
  16. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-hPPfVGQv.css +1 -0
  17. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-B3TJrD4O.js → react-vendor-CTC6vtln.js} +1 -1
  18. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-7f1j5wx3.js → traceability-9Mtvbwrg.js} +1 -1
  19. package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
  20. package/templates/.agents/dashboard/apps/frontend/src/App.tsx +74 -10
  21. package/templates/.agents/dashboard/apps/frontend/src/AppRequestGuards.tsx +44 -0
  22. package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +103 -2
  23. package/templates/.agents/dashboard/apps/frontend/src/dashboard-capability-nav.css +11 -0
  24. package/templates/.agents/dashboard/apps/frontend/src/operations-period-contract.test.ts +14 -0
  25. package/templates/.agents/dashboard/apps/frontend/src/operations-period.test.ts +3 -6
  26. package/templates/.agents/dashboard/apps/frontend/src/operations-period.ts +3 -2
  27. package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +2 -2
  28. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.test.ts +60 -0
  29. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.tsx +26 -9
  30. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorNavigation.tsx +189 -0
  31. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +122 -2
  32. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +70 -155
  33. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.helpers.tsx +2 -1
  34. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.test.ts +12 -4
  35. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +67 -24
  36. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +19 -4
  37. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.test.ts +59 -0
  38. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.tsx +55 -2
  39. package/templates/.agents/dashboard/modules/operations/src/react/operation-date-range.test.ts +25 -0
  40. package/templates/.agents/dashboard/modules/operations/src/react/operation-date-range.ts +29 -0
  41. package/templates/.agents/dashboard/modules/operations/src/react/operations-coverage-toast.test.ts +3 -2
  42. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +12 -2
  43. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +274 -246
  44. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +255 -227
  45. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +263 -235
  46. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.test.ts +282 -0
  47. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.ts +71 -0
  48. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-launch-progress.ts +18 -0
  49. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-stale-seed.ts +50 -0
  50. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +9 -6
  51. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +45 -73
  52. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-seed.test.ts +79 -2
  53. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-work-item-index.test.ts +56 -0
  54. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.mjs +26 -8
  55. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs +45 -4
  56. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.mjs +58 -0
  57. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.test.mjs +81 -0
  58. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +171 -71
  59. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +152 -11
  60. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +4 -9
  61. package/templates/.agents/dashboard/tests/e2e/playwright.config.ts +9 -1
  62. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-loading.spec.ts +1 -1
  63. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-pagination.spec.ts +4 -0
  64. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-period-transition.spec.ts +238 -0
  65. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-performance-matrix.spec.ts +4 -0
  66. package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +60 -17
  67. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  68. package/templates/.agents/requirements/comparative-audit.md +1 -1
  69. package/templates/.agents/requirements/context-engine.md +1 -1
  70. package/templates/.agents/requirements/cost-observability.md +1 -1
  71. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  72. package/templates/.agents/requirements/data-export.md +1 -1
  73. package/templates/.agents/requirements/design-system.md +1 -1
  74. package/templates/.agents/requirements/execution-engine.md +1 -1
  75. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  76. package/templates/.agents/requirements/model-orchestration.md +1 -1
  77. package/templates/.agents/requirements/public-distribution.md +1 -1
  78. package/templates/.agents/requirements/software-architecture.md +1 -1
  79. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  80. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  81. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  82. package/templates/.agents/runtime/bin/zaip.mjs +7 -7
  83. package/templates/.agents/runtime/checksums.sha256 +3 -3
  84. package/templates/.agents/runtime/provenance.json +3 -3
  85. package/templates/.agents/runtime/sbom.cdx.json +1595 -3491
  86. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +16 -0
  87. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.mjs +19 -20
  88. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.test.mjs +143 -0
  89. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.mjs +14 -0
  90. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.mjs +3 -1
  91. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.test.mjs +21 -0
  92. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/source-size-telemetry/model-identity-reconciliation.mjs +3 -0
  93. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +29 -0
  94. package/templates/template-manifest.json +539 -935
  95. package/templates/.agents/dashboard/.tmp-run-list.json +0 -254
  96. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-DKwP1stV.js +0 -68
  97. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-B31GMG5O.css +0 -1
  98. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-CuMcoUni.js +0 -2
@@ -8,6 +8,50 @@ export function needsAtomicCostSnapshot(dashboardView: unknown, costsLoaded: boo
8
8
  return false;
9
9
  }
10
10
 
11
+ /**
12
+ * State updates from a period picker are asynchronous. Keep the authoritative
13
+ * loaded flag in the decision so a navigation event cannot use the previous
14
+ * render's matching currency and skip the replacement Cost snapshot.
15
+ */
16
+ export function shouldLoadCostSummary({
17
+ costsLoaded,
18
+ currency,
19
+ snapshotCurrency,
20
+ }: {
21
+ costsLoaded: boolean;
22
+ currency: string;
23
+ snapshotCurrency: string | undefined;
24
+ }): boolean {
25
+ return !costsLoaded || snapshotCurrency !== currency;
26
+ }
27
+
28
+ /** A late summary from another period or currency must never restore stale costs. */
29
+ export function isCurrentCostSummaryRequest({
30
+ aborted,
31
+ currentCurrency,
32
+ currentPeriod,
33
+ currentRequestVersion,
34
+ requestVersion,
35
+ requestedCurrency,
36
+ requestedPeriod,
37
+ }: {
38
+ aborted: boolean;
39
+ currentCurrency: string;
40
+ currentPeriod: { from: string | null; to: string | null };
41
+ currentRequestVersion: number;
42
+ requestVersion: number;
43
+ requestedCurrency: string;
44
+ requestedPeriod: { from: string | null; to: string | null };
45
+ }): boolean {
46
+ return (
47
+ !aborted &&
48
+ requestVersion === currentRequestVersion &&
49
+ requestedCurrency === currentCurrency &&
50
+ requestedPeriod.from === currentPeriod.from &&
51
+ requestedPeriod.to === currentPeriod.to
52
+ );
53
+ }
54
+
11
55
  export function isCurrentDashboardRequest(requestVersion: number, currentVersion: number): boolean {
12
56
  return requestVersion === currentVersion;
13
57
  }
@@ -1,6 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { describe, expect, it } from "vitest";
3
- import { isCurrentDashboardRequest, needsAtomicCostSnapshot } from "./App.js";
3
+ import {
4
+ isCurrentCostSummaryRequest,
5
+ isCurrentDashboardRequest,
6
+ needsAtomicCostSnapshot,
7
+ shouldLoadCostSummary,
8
+ } from "./App.js";
4
9
 
5
10
  describe("Cost period refresh", () => {
6
11
  it("does not retain a prior Cost Monitor snapshot while Todo o Período is loading", () => {
@@ -16,6 +21,102 @@ describe("Cost period refresh", () => {
16
21
  expect(exportedSnapshot).toBeNull();
17
22
  });
18
23
 
24
+ it("starts a fresh Cost request when a period change invalidates a same-currency snapshot", () => {
25
+ // This reproduces clicking Monitor de Custos before React commits the
26
+ // `setCosts(null)` render: the event closure can still see BRL, but the
27
+ // ref was invalidated synchronously by the period transition.
28
+ expect(
29
+ shouldLoadCostSummary({
30
+ costsLoaded: false,
31
+ currency: "BRL",
32
+ snapshotCurrency: "BRL",
33
+ }),
34
+ ).toBe(true);
35
+ expect(
36
+ shouldLoadCostSummary({
37
+ costsLoaded: true,
38
+ currency: "BRL",
39
+ snapshotCurrency: "BRL",
40
+ }),
41
+ ).toBe(false);
42
+ });
43
+
44
+ it("invalidates and aborts an old Cost summary on a global period transition", () => {
45
+ const app = readFileSync(new URL("./App.tsx", import.meta.url), "utf8");
46
+ const periodChange = app.slice(
47
+ app.indexOf("onDateChange={(preset, range) => {"),
48
+ app.indexOf("settings={"),
49
+ );
50
+
51
+ expect(periodChange).toContain("costSummaryAbortRef.current?.abort();");
52
+ expect(periodChange).toContain("++costSummaryRequestVersion.current;");
53
+ expect(app).toContain("isCurrentCostSummaryRequest({");
54
+ expect(app).toContain('if (failedArea === "costs") void loadCostSummary();');
55
+ });
56
+
57
+ it("reloads the invalidated BRL Cost snapshot for Cost Monitor and All Time presentation", () => {
58
+ const app = readFileSync(new URL("./App.tsx", import.meta.url), "utf8");
59
+ const activeCostReload = app.slice(
60
+ app.indexOf("// A global period change invalidates the Cost snapshot"),
61
+ app.indexOf("const loadMoreTraceTasks"),
62
+ );
63
+
64
+ expect(activeCostReload).toContain('dashboardView !== "cost-monitor" && operationsPeriod.preset !== "all"');
65
+ expect(activeCostReload).toContain("costSummaryAbortRef.current !== null");
66
+ expect(activeCostReload).toContain("costsLoaded: costsLoadedRef.current");
67
+ expect(activeCostReload).toContain("void loadCostSummary();");
68
+ expect(activeCostReload).toContain("operationsPeriod.preset");
69
+ expect(activeCostReload).not.toContain("failedArea");
70
+ });
71
+
72
+ it("does not make the all-time data request depend on its displayed factual range", () => {
73
+ const app = readFileSync(new URL("./App.tsx", import.meta.url), "utf8");
74
+
75
+ expect(app).toContain('operationsPeriod.preset === "all"');
76
+ expect(app).toContain('? "all"');
77
+ expect(app).toContain("const operationsRequestPeriod = useMemo(");
78
+ expect(app).toContain("[api, currency, operationsRequestPeriod]");
79
+ });
80
+
81
+ it("rejects a late all-time summary after a newer period or currency takes ownership", () => {
82
+ const allTime = { from: null, to: null };
83
+ const currentMonth = { from: "2026-08-01", to: "2026-08-31" };
84
+
85
+ expect(
86
+ isCurrentCostSummaryRequest({
87
+ aborted: false,
88
+ currentCurrency: "BRL",
89
+ currentPeriod: currentMonth,
90
+ currentRequestVersion: 3,
91
+ requestVersion: 2,
92
+ requestedCurrency: "BRL",
93
+ requestedPeriod: allTime,
94
+ }),
95
+ ).toBe(false);
96
+ expect(
97
+ isCurrentCostSummaryRequest({
98
+ aborted: false,
99
+ currentCurrency: "USD",
100
+ currentPeriod: allTime,
101
+ currentRequestVersion: 3,
102
+ requestVersion: 3,
103
+ requestedCurrency: "BRL",
104
+ requestedPeriod: allTime,
105
+ }),
106
+ ).toBe(false);
107
+ expect(
108
+ isCurrentCostSummaryRequest({
109
+ aborted: false,
110
+ currentCurrency: "BRL",
111
+ currentPeriod: allTime,
112
+ currentRequestVersion: 3,
113
+ requestVersion: 3,
114
+ requestedCurrency: "BRL",
115
+ requestedPeriod: allTime,
116
+ }),
117
+ ).toBe(true);
118
+ });
119
+
19
120
  it("mounts Cost from a Daily seed without a full summary request", () => {
20
121
  const app = readFileSync(new URL("./App.tsx", import.meta.url), "utf8");
21
122
  const refresh = app.slice(
@@ -35,7 +136,7 @@ describe("Cost period refresh", () => {
35
136
  );
36
137
  expect(refresh).toContain("setDisplayedCosts(null);");
37
138
  expect(refresh).toContain("const activeDashboardView = dashboardViewRef.current");
38
- expect(refresh).toContain("[api, currency, operationsPeriod]");
139
+ expect(refresh).toContain("[api, currency, operationsRequestPeriod]");
39
140
  expect(refresh).not.toContain("[api, currency, dashboardView, operationsPeriod]");
40
141
  });
41
142
 
@@ -10,6 +10,17 @@
10
10
  background: rgba(8, 14, 25, 0.72);
11
11
  }
12
12
 
13
+ .dashboard-capability-shell {
14
+ flex: 0 0 auto;
15
+ }
16
+
17
+ /* The landing and Traceability surfaces have no temporal/search result to filter.
18
+ * Keep the controls available for the two capability views that consume them. */
19
+ .dashboard-capability-shell--initial + .dashboard-global-filters,
20
+ .dashboard-capability-shell--traceability + .dashboard-global-filters {
21
+ display: none;
22
+ }
23
+
13
24
  .dashboard-capability-nav button {
14
25
  display: flex;
15
26
  min-height: 52px;
@@ -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" };
@@ -2,12 +2,9 @@ import { describe, expect, it } from "vitest";
2
2
  import { defaultOperationsPeriod, requestPeriodForOperations } from "./operations-period.js";
3
3
 
4
4
  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
- });
5
+ it("opens in All Time without date bounds", () => {
6
+ expect(defaultOperationsPeriod.preset).toBe("all");
7
+ expect(requestPeriodForOperations(defaultOperationsPeriod)).toEqual({ from: null, to: null });
11
8
  });
12
9
 
13
10
  it("keeps an explicit Sao Paulo This Month selection bounded", () => {
@@ -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
 
@@ -74,11 +74,11 @@ describe("atomic Cost snapshot loading", () => {
74
74
  expect(initialLoad).not.toContain("api.snapshot(currency, period)");
75
75
  });
76
76
 
77
- it("retries a failed Cost snapshot through refresh instead of a summary-only Cost request", () => {
77
+ it("retries a failed Cost snapshot through its summary request", () => {
78
78
  const app = readFileSync(new URL("./App.tsx", import.meta.url), "utf8");
79
79
  const retry = app.slice(app.indexOf("const retryLastLoad"), app.indexOf("const setupRoute"));
80
80
 
81
- expect(retry).toContain('if (failedArea === "costs") void refresh();');
81
+ expect(retry).toContain('if (failedArea === "costs") void loadCostSummary();');
82
82
  expect(retry).not.toContain("loadCosts");
83
83
  });
84
84
 
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { loadCostDeveloperAttribution } from "./CostDeveloperAttributionPanel.js";
3
+
4
+ describe("loadCostDeveloperAttribution", () => {
5
+ it("requests the active period and returns every row supplied by the API", async () => {
6
+ const rows = Array.from({ length: 11 }, (_, index) => ({
7
+ account_id: String(index + 1),
8
+ login_snapshot: `dev-${index + 1}`,
9
+ prd_count: 0,
10
+ spec_count: 0,
11
+ task_count: 1,
12
+ run_count: 1,
13
+ test_count: null,
14
+ evidence_count: null,
15
+ total_duration_ms: null,
16
+ total_tokens: null,
17
+ total_cost_brl: null,
18
+ duration_covered_runs: 0,
19
+ token_covered_runs: 0,
20
+ brl_covered_runs: 0,
21
+ }));
22
+ const fetcher = vi
23
+ .fn<typeof fetch>()
24
+ .mockResolvedValue(new Response(JSON.stringify({ rows }), { status: 200 }));
25
+
26
+ await expect(
27
+ loadCostDeveloperAttribution({
28
+ from: "2026-08-01",
29
+ to: "2026-08-31",
30
+ fetcher,
31
+ }),
32
+ ).resolves.toEqual(rows);
33
+ expect(fetcher).toHaveBeenCalledWith("/api/v2/developers?from=2026-08-01&to=2026-08-31", {});
34
+ });
35
+
36
+ it("keeps the embedded all-time population unbounded and returns all 25 developers", async () => {
37
+ const rows = Array.from({ length: 25 }, (_, index) => ({
38
+ account_id: String(index + 1),
39
+ login_snapshot: `dev-${index + 1}`,
40
+ prd_count: 0,
41
+ spec_count: 0,
42
+ task_count: 1,
43
+ run_count: 1,
44
+ test_count: null,
45
+ evidence_count: null,
46
+ total_duration_ms: null,
47
+ total_tokens: null,
48
+ total_cost_brl: null,
49
+ duration_covered_runs: 0,
50
+ token_covered_runs: 0,
51
+ brl_covered_runs: 0,
52
+ }));
53
+ const fetcher = vi
54
+ .fn<typeof fetch>()
55
+ .mockResolvedValue(new Response(JSON.stringify({ rows }), { status: 200 }));
56
+
57
+ await expect(loadCostDeveloperAttribution({ fetcher })).resolves.toHaveLength(25);
58
+ expect(fetcher).toHaveBeenCalledWith("/api/v2/developers", {});
59
+ });
60
+ });
@@ -2,6 +2,28 @@ import { useEffect, useState } from "react";
2
2
  import { type CostDeveloperAttributionRow, CostDeveloperAttributionTable } from "./CostTables.js";
3
3
  import type { CostLocale } from "./cost-monitor-copy.js";
4
4
 
5
+ export async function loadCostDeveloperAttribution({
6
+ from,
7
+ to,
8
+ signal,
9
+ fetcher = fetch,
10
+ }: {
11
+ from?: string;
12
+ to?: string;
13
+ signal?: AbortSignal;
14
+ fetcher?: typeof fetch;
15
+ }): Promise<readonly CostDeveloperAttributionRow[]> {
16
+ const params = new URLSearchParams();
17
+ if (from) params.set("from", from);
18
+ if (to) params.set("to", to);
19
+ const response = await fetcher(`/api/v2/developers${params.size === 0 ? "" : `?${params}`}`, {
20
+ ...(signal === undefined ? {} : { signal }),
21
+ });
22
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
23
+ const payload = (await response.json()) as { rows?: CostDeveloperAttributionRow[] };
24
+ return payload.rows ?? [];
25
+ }
26
+
5
27
  /** Cost-specific rendering of the factual developer aggregate endpoint. */
6
28
  export function CostDeveloperAttributionPanel({
7
29
  from,
@@ -19,18 +41,13 @@ export function CostDeveloperAttributionPanel({
19
41
  }>({ loading: true, error: null, rows: [] });
20
42
  useEffect(() => {
21
43
  const controller = new AbortController();
22
- const params = new URLSearchParams();
23
- if (from) params.set("from", from);
24
- if (to) params.set("to", to);
25
44
  setState((previous) => ({ ...previous, loading: true, error: null }));
26
- fetch(`/api/v2/developers${params.size === 0 ? "" : `?${params}`}`, {
45
+ loadCostDeveloperAttribution({
46
+ ...(from === undefined ? {} : { from }),
47
+ ...(to === undefined ? {} : { to }),
27
48
  signal: controller.signal,
28
49
  })
29
- .then(async (response) => {
30
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
31
- return response.json() as Promise<{ rows?: CostDeveloperAttributionRow[] }>;
32
- })
33
- .then((payload) => setState({ loading: false, error: null, rows: payload.rows ?? [] }))
50
+ .then((rows) => setState({ loading: false, error: null, rows }))
34
51
  .catch((error: unknown) => {
35
52
  if ((error as { name?: string }).name !== "AbortError")
36
53
  setState({
@@ -0,0 +1,189 @@
1
+ import type { CostMonitorProjection } from "../contracts.js";
2
+ import { type CostFilterState, CostFilters, type CostPeriod } from "./CostFilters.js";
3
+ import { ChevronDownIcon, ListFilterIcon, LocaleFlag, SettingsIcon } from "./CostMonitorIcons.js";
4
+ import type { CostCurrency, CostLocale, costCopy } from "./cost-monitor-copy.js";
5
+
6
+ const zaipLogoUrl = new URL("../../../../../config/src/logo_novo_azul.png", import.meta.url).href;
7
+
8
+ type CostNavigationCopy = (typeof costCopy)[CostLocale];
9
+
10
+ export type CostNavigationPopup = "filters" | "locale" | "settings" | null;
11
+
12
+ export function toggleCostNavigationPopup(
13
+ popup: CostNavigationPopup,
14
+ requestedPopup: Exclude<CostNavigationPopup, null>,
15
+ ): CostNavigationPopup {
16
+ return popup === requestedPopup ? null : requestedPopup;
17
+ }
18
+
19
+ export function CostMonitorNavigation({
20
+ currency,
21
+ filters,
22
+ loading,
23
+ locale,
24
+ onCurrencyChange,
25
+ onFiltersChange,
26
+ onLocaleChange,
27
+ onPopupChange,
28
+ onPeriodChange,
29
+ onRefresh,
30
+ period,
31
+ popup,
32
+ text,
33
+ updatedLabel,
34
+ filtersAvailable,
35
+ }: {
36
+ currency: CostCurrency;
37
+ filters: CostFilterState;
38
+ filtersAvailable: CostMonitorProjection["filters_available"];
39
+ loading: boolean;
40
+ locale: CostLocale;
41
+ onCurrencyChange: (currency: CostCurrency) => void;
42
+ onFiltersChange: (filters: CostFilterState) => void;
43
+ onLocaleChange: (locale: CostLocale) => void;
44
+ onPopupChange: (popup: CostNavigationPopup) => void;
45
+ onPeriodChange: (period: CostPeriod) => void;
46
+ onRefresh: () => void;
47
+ period: CostPeriod;
48
+ popup: CostNavigationPopup;
49
+ text: CostNavigationCopy;
50
+ updatedLabel: string;
51
+ }) {
52
+ const filtersOpen = popup === "filters";
53
+ const localeOpen = popup === "locale";
54
+ const settingsOpen = popup === "settings";
55
+ const changeLocale = (next: CostLocale) => {
56
+ onLocaleChange(next);
57
+ };
58
+
59
+ return (
60
+ <>
61
+ <nav aria-label="Navegação do Cost Monitor" className="cost-monitor-nav">
62
+ <div className="cost-monitor-brand">
63
+ <strong className="cost-monitor-title">{text.title}</strong>
64
+ <small className="cost-last-updated">{updatedLabel}</small>
65
+ </div>
66
+ <img alt="ZAIP" className="cost-monitor-logo" src={zaipLogoUrl} />
67
+ <div className="cost-nav-actions">
68
+ <button
69
+ aria-label={text.refresh}
70
+ className="cost-nav-icon cost-refresh-action"
71
+ disabled={loading}
72
+ onClick={onRefresh}
73
+ type="button"
74
+ >
75
+ ↻
76
+ </button>
77
+ <div className="cost-nav-popup-anchor cost-filter-action">
78
+ <button
79
+ className={filtersOpen ? "cost-nav-button is-active" : "cost-nav-button"}
80
+ onClick={() => onPopupChange(toggleCostNavigationPopup(popup, "filters"))}
81
+ type="button"
82
+ >
83
+ <ListFilterIcon /> {text.filters}
84
+ </button>
85
+ {filtersOpen ? (
86
+ <div className="cost-nav-popup cost-nav-filter-popup">
87
+ <CostFilters
88
+ available={filtersAvailable}
89
+ hideQuery={false}
90
+ locale={locale}
91
+ onChange={onFiltersChange}
92
+ value={filters}
93
+ />
94
+ </div>
95
+ ) : null}
96
+ </div>
97
+ <div className="cost-nav-popup-anchor cost-locale-action">
98
+ <button
99
+ className={localeOpen ? "cost-nav-button is-active" : "cost-nav-button"}
100
+ onClick={() => onPopupChange(toggleCostNavigationPopup(popup, "locale"))}
101
+ type="button"
102
+ >
103
+ <LocaleFlag locale={locale} />
104
+ {locale === "pt-BR" ? "PT-BR" : "EN-US"}
105
+ <ChevronDownIcon />
106
+ </button>
107
+ {localeOpen ? (
108
+ <div className="cost-nav-popup cost-nav-locale-popup">
109
+ <button onClick={() => changeLocale("pt-BR")} type="button">
110
+ <LocaleFlag locale="pt-BR" /> Português do Brasil {locale === "pt-BR" ? "✓" : ""}
111
+ </button>
112
+ <button onClick={() => changeLocale("en-US")} type="button">
113
+ <LocaleFlag locale="en-US" /> English {locale === "en-US" ? "✓" : ""}
114
+ </button>
115
+ </div>
116
+ ) : null}
117
+ </div>
118
+ <div className="cost-nav-popup-anchor cost-settings-action">
119
+ <button
120
+ aria-label={text.settings}
121
+ className={settingsOpen ? "cost-nav-icon is-active" : "cost-nav-icon"}
122
+ onClick={() => onPopupChange(toggleCostNavigationPopup(popup, "settings"))}
123
+ type="button"
124
+ >
125
+ <SettingsIcon />
126
+ </button>
127
+ {settingsOpen ? (
128
+ <div className="cost-nav-popup cost-nav-settings-popup">
129
+ <div className="cost-mobile-settings">
130
+ <small className="cost-mobile-updated">{updatedLabel}</small>
131
+ <button disabled={loading} onClick={onRefresh} type="button">
132
+ ↻{" "}
133
+ {loading ? (locale === "pt-BR" ? "Atualizando…" : "Refreshing…") : text.refresh}
134
+ </button>
135
+ <label>
136
+ <span>{locale === "pt-BR" ? "Idioma" : "Language"}</span>
137
+ <select
138
+ onChange={(event) => changeLocale(event.target.value as CostLocale)}
139
+ value={locale}
140
+ >
141
+ <option value="pt-BR">Português do Brasil</option>
142
+ <option value="en-US">English</option>
143
+ </select>
144
+ </label>
145
+ <div className="cost-mobile-filters">
146
+ <strong>{text.filters}</strong>
147
+ <CostFilters
148
+ available={filtersAvailable}
149
+ hideQuery={false}
150
+ locale={locale}
151
+ onChange={onFiltersChange}
152
+ value={filters}
153
+ />
154
+ </div>
155
+ </div>
156
+ <label>
157
+ <span>{text.currency}</span>
158
+ <select
159
+ onChange={(event) => onCurrencyChange(event.target.value as CostCurrency)}
160
+ value={currency}
161
+ >
162
+ <option value="BRL">BRL · R$</option>
163
+ <option value="USD">USD · $</option>
164
+ </select>
165
+ </label>
166
+ </div>
167
+ ) : null}
168
+ </div>
169
+ </div>
170
+ </nav>
171
+ <header className="cost-hero">
172
+ <p className="cost-hero-description">{text.subtitle}</p>
173
+ <label className="cost-period">
174
+ <span>{text.period}</span>
175
+ <select
176
+ onChange={(event) => onPeriodChange(event.target.value as CostPeriod)}
177
+ value={period}
178
+ >
179
+ <option value="all">{text.allTime}</option>
180
+ <option value="7">{text.last7}</option>
181
+ <option value="30">{text.last30}</option>
182
+ <option value="90">{text.last90}</option>
183
+ <option value="month">{text.thisMonth}</option>
184
+ </select>
185
+ </label>
186
+ </header>
187
+ </>
188
+ );
189
+ }