@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.
- package/LICENSE +6 -3
- package/README.md +8 -2
- package/dist/services/codex-pre-tool-hook-template.js +1 -1
- package/dist/services/runtime-template.test.js +9 -8
- package/dist/services/runtime-template.test.js.map +1 -1
- package/package.json +2 -2
- package/templates/.agents/dashboard/.zaip-managed.json +414 -394
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-ovxurkIk.js → CostCalculationGuidePage-D1mpyYwh.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-BXy6nti1.js → DashboardHelpDialog-CKO47D5s.js} +30 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-vSvvOHQp.js → PricingSettingsDialog-BuSlCbyZ.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-zH758vfs.js → ProjectSettingsPage-CUHFkAhZ.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-CTH2wDs5.js → TraceabilityWorkspace-BmASq2gW.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Csj6LiAt.js +68 -0
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-B_lDDHn1.js → icons-vendor-VyTTl_ph.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DIwDvCu5.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-B3TJrD4O.js → react-vendor-CTC6vtln.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-7f1j5wx3.js → traceability-9Mtvbwrg.js} +1 -1
- package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
- package/templates/.agents/dashboard/apps/frontend/src/App.tsx +74 -10
- package/templates/.agents/dashboard/apps/frontend/src/AppRequestGuards.tsx +44 -0
- package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +103 -2
- 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 +3 -6
- package/templates/.agents/dashboard/apps/frontend/src/operations-period.ts +3 -2
- package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +2 -2
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.test.ts +60 -0
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.tsx +26 -9
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorNavigation.tsx +189 -0
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +122 -2
- package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +70 -155
- 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 +67 -24
- package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +19 -4
- package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.test.ts +59 -0
- package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.tsx +55 -2
- 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/packages/local-runtime/dist/server.cjs +274 -246
- package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +255 -227
- package/templates/.agents/dashboard/packages/local-runtime/server.mjs +263 -235
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.test.ts +282 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-dimension-facts.ts +71 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-launch-progress.ts +18 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-stale-seed.ts +50 -0
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +9 -6
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +45 -73
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-seed.test.ts +79 -2
- package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-work-item-index.test.ts +56 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.mjs +26 -8
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs +45 -4
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.mjs +58 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.test.mjs +81 -0
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +171 -71
- package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +152 -11
- package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +4 -9
- package/templates/.agents/dashboard/tests/e2e/playwright.config.ts +9 -1
- package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-loading.spec.ts +1 -1
- 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 +238 -0
- package/templates/.agents/dashboard/tests/e2e/specs/dashboard-performance-matrix.spec.ts +4 -0
- package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +60 -17
- 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/runtime/bin/zaip.mjs +7 -7
- package/templates/.agents/runtime/checksums.sha256 +3 -3
- package/templates/.agents/runtime/provenance.json +3 -3
- package/templates/.agents/runtime/sbom.cdx.json +1595 -3491
- package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +16 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.mjs +19 -20
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.test.mjs +143 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.mjs +14 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.mjs +3 -1
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.test.mjs +21 -0
- package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/source-size-telemetry/model-identity-reconciliation.mjs +3 -0
- package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +29 -0
- package/templates/template-manifest.json +539 -935
- package/templates/.agents/dashboard/.tmp-run-list.json +0 -254
- package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-DKwP1stV.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-CuMcoUni.js +0 -2
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
readDateOnlyCostDimensionPage,
|
|
29
29
|
} from "./sqlite-cost-agent-page.js";
|
|
30
30
|
import { buildCostDimensionAggregates } from "./sqlite-cost-aggregates.js";
|
|
31
|
+
import { rebuildCostInvocationDimensionFactsSql } from "./sqlite-cost-dimension-facts.js";
|
|
31
32
|
import { type CostGroupKind, readCostGroupPage } from "./sqlite-cost-group-page.js";
|
|
32
33
|
import { materializeCostGroupDailyProjection } from "./sqlite-cost-group-projection.js";
|
|
33
34
|
import { filterCostInvocationProjection } from "./sqlite-cost-invocation-page.js";
|
|
@@ -54,7 +55,6 @@ import {
|
|
|
54
55
|
import {
|
|
55
56
|
installLegacyViewAliases,
|
|
56
57
|
materializeSnapshot,
|
|
57
|
-
mostRecentCompatibleSnapshot,
|
|
58
58
|
pruneSnapshotCache,
|
|
59
59
|
readSnapshotFileCount,
|
|
60
60
|
} from "./sqlite-snapshot-cache.js";
|
|
@@ -64,15 +64,14 @@ export {
|
|
|
64
64
|
sourceFingerprint,
|
|
65
65
|
} from "./sqlite-read-model-source-fingerprint.js";
|
|
66
66
|
|
|
67
|
+
import { reportSqliteReadModelLaunchProgress } from "./sqlite-read-model-launch-progress.js";
|
|
67
68
|
import { sourceFingerprint } from "./sqlite-read-model-source-fingerprint.js";
|
|
69
|
+
import { materializePreferredStaleSeed } from "./sqlite-read-model-stale-seed.js";
|
|
68
70
|
|
|
69
71
|
const dbRelativePath = ".local/dashboard-sqlite/read-model.sqlite";
|
|
70
|
-
// Schema
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
// Allocation rows are materialized after attributed-time reconciliation; a
|
|
74
|
-
// prior seed contains the pre-reconciliation payload and is incompatible.
|
|
75
|
-
const readModelSchemaVersion = "70";
|
|
72
|
+
// Schema 71 requires the canonical work-item identity index. Older seeds must
|
|
73
|
+
// rebuild rather than silently retaining the unindexed correlated lookups.
|
|
74
|
+
const readModelSchemaVersion = "71";
|
|
76
75
|
const requiredPersistedTables = [
|
|
77
76
|
"zaip_aios_activities",
|
|
78
77
|
"zaip_dim_llm_model",
|
|
@@ -97,6 +96,7 @@ const requiredIndexes = [
|
|
|
97
96
|
"zaip_activities_period_invocation",
|
|
98
97
|
"zaip_activities_status",
|
|
99
98
|
"zaip_activities_payload",
|
|
99
|
+
"zaip_activities_work_item_identity",
|
|
100
100
|
"cost_dimension_facts_day_model",
|
|
101
101
|
"cost_dimension_facts_day_provider",
|
|
102
102
|
"cost_dimension_facts_invocation",
|
|
@@ -993,6 +993,16 @@ export function validateSqliteReadModelSchema(path: string): {
|
|
|
993
993
|
}
|
|
994
994
|
}
|
|
995
995
|
|
|
996
|
+
function hasReusableSnapshotPayloads(database: DatabaseSync): boolean {
|
|
997
|
+
const indexes = database
|
|
998
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='index'")
|
|
999
|
+
.all() as Array<{
|
|
1000
|
+
name: string;
|
|
1001
|
+
}>;
|
|
1002
|
+
const names = new Set(indexes.map((index) => index.name));
|
|
1003
|
+
return requiredIndexes.every((name) => names.has(name)) && hasRequiredFirstFoldPayloads(database);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
996
1006
|
/** Recreates the disposable comparison read model from ZAIP records and configuration. */
|
|
997
1007
|
export async function rebuildSqliteReadModel(
|
|
998
1008
|
workspaceRoot: string,
|
|
@@ -1005,21 +1015,28 @@ export async function rebuildSqliteReadModel(
|
|
|
1005
1015
|
materialize_ms: number;
|
|
1006
1016
|
}> {
|
|
1007
1017
|
const path = sqliteReadModelPath(workspaceRoot);
|
|
1018
|
+
const reportLaunchProgress = reportSqliteReadModelLaunchProgress;
|
|
1019
|
+
const cacheDirectory = process.env.ZAIP_SQLITE_CACHE_DIR ?? join(dirname(path), "cache");
|
|
1020
|
+
const preferredSeed = await materializePreferredStaleSeed({
|
|
1021
|
+
destination: path,
|
|
1022
|
+
enabled: !options.forceCanonical && process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED === "1",
|
|
1023
|
+
fingerprintMs: 0,
|
|
1024
|
+
hasRequiredPayloads: hasReusableSnapshotPayloads,
|
|
1025
|
+
reportProgress: true,
|
|
1026
|
+
schemaVersion: readModelSchemaVersion,
|
|
1027
|
+
});
|
|
1028
|
+
if (preferredSeed !== null) return preferredSeed;
|
|
1008
1029
|
const fingerprintStartedAt = performance.now();
|
|
1030
|
+
reportLaunchProgress("source_fingerprint_started", 8, "Inventariando fontes canônicas.");
|
|
1009
1031
|
const sourceSnapshot = await sourceFingerprint(workspaceRoot);
|
|
1010
|
-
|
|
1011
|
-
if (process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS !== "1") return;
|
|
1012
|
-
process.stderr.write(
|
|
1013
|
-
`${JSON.stringify({ event: "dashboard_sqlite_rebuild_progress", phase, percent, detail })}\n`,
|
|
1014
|
-
);
|
|
1015
|
-
};
|
|
1032
|
+
reportLaunchProgress("source_fingerprint_ready", 20, "Inventário canônico concluído.");
|
|
1016
1033
|
const { fingerprint } = sourceSnapshot;
|
|
1017
1034
|
const fingerprintMs = Math.round(performance.now() - fingerprintStartedAt);
|
|
1018
1035
|
const existingFiles = readSnapshotFileCount(
|
|
1019
1036
|
path,
|
|
1020
1037
|
fingerprint,
|
|
1021
1038
|
readModelSchemaVersion,
|
|
1022
|
-
|
|
1039
|
+
hasReusableSnapshotPayloads,
|
|
1023
1040
|
);
|
|
1024
1041
|
if (existingFiles !== null) {
|
|
1025
1042
|
validateSqliteReadModelSchema(path);
|
|
@@ -1037,7 +1054,7 @@ export async function rebuildSqliteReadModel(
|
|
|
1037
1054
|
// source fingerprint, then atomically materialize that cache into the
|
|
1038
1055
|
// generation path. A normal fresh-port handoff can therefore be warm.
|
|
1039
1056
|
const cachePath = join(
|
|
1040
|
-
|
|
1057
|
+
cacheDirectory,
|
|
1041
1058
|
// The cache payload is part of the immutable generation contract. Keep
|
|
1042
1059
|
// a separate namespace when that payload changes, so a schema-compatible
|
|
1043
1060
|
// older seed cannot bypass the new all-time precomputation.
|
|
@@ -1049,7 +1066,7 @@ export async function rebuildSqliteReadModel(
|
|
|
1049
1066
|
cachePath,
|
|
1050
1067
|
fingerprint,
|
|
1051
1068
|
readModelSchemaVersion,
|
|
1052
|
-
|
|
1069
|
+
hasReusableSnapshotPayloads,
|
|
1053
1070
|
);
|
|
1054
1071
|
if (cachedFiles !== null) {
|
|
1055
1072
|
const materializeStartedAt = performance.now();
|
|
@@ -1068,24 +1085,14 @@ export async function rebuildSqliteReadModel(
|
|
|
1068
1085
|
// reconstruction merely because a record changed since the last snapshot.
|
|
1069
1086
|
// The launcher labels this state as updating and starts an isolated refresh;
|
|
1070
1087
|
// this process still serves only a schema-validated immutable seed.
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
await materializeSnapshot(fallback.path, path);
|
|
1080
|
-
return {
|
|
1081
|
-
files: fallback.files,
|
|
1082
|
-
path,
|
|
1083
|
-
cache: "stale_seed",
|
|
1084
|
-
fingerprint_ms: fingerprintMs,
|
|
1085
|
-
materialize_ms: Math.round(performance.now() - materializeStartedAt),
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1088
|
+
const fallback = await materializePreferredStaleSeed({
|
|
1089
|
+
destination: path,
|
|
1090
|
+
enabled: !options.forceCanonical && process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED === "1",
|
|
1091
|
+
fingerprintMs,
|
|
1092
|
+
hasRequiredPayloads: hasReusableSnapshotPayloads,
|
|
1093
|
+
schemaVersion: readModelSchemaVersion,
|
|
1094
|
+
});
|
|
1095
|
+
if (fallback !== null) return fallback;
|
|
1089
1096
|
|
|
1090
1097
|
reportLaunchProgress(
|
|
1091
1098
|
"source_inventory",
|
|
@@ -2123,6 +2130,8 @@ export async function rebuildSqliteReadModel(
|
|
|
2123
2130
|
}),
|
|
2124
2131
|
);
|
|
2125
2132
|
}
|
|
2133
|
+
// Keep canonical TEMP references outside the public-view name rewriter.
|
|
2134
|
+
database.exec(rebuildCostInvocationDimensionFactsSql);
|
|
2126
2135
|
const viewSql = `
|
|
2127
2136
|
CREATE INDEX zaip_activities_agent_run ON zaip_aios_activities(activity_source_kind, agent_run_id, occurred_day_sp, occurred_at DESC);
|
|
2128
2137
|
CREATE INDEX zaip_activities_relationships ON zaip_aios_activities(task_id, spec_id, prd_id, execution_id, conversation_id);
|
|
@@ -2140,6 +2149,7 @@ export async function rebuildSqliteReadModel(
|
|
|
2140
2149
|
WHERE agent_invocation_id IS NOT NULL;
|
|
2141
2150
|
CREATE INDEX zaip_activities_status ON zaip_aios_activities(activity_source_kind, status, phase);
|
|
2142
2151
|
CREATE INDEX zaip_activities_payload ON zaip_aios_activities(payload_type, activity_key);
|
|
2152
|
+
CREATE INDEX zaip_activities_work_item_identity ON zaip_aios_activities(json_extract(payload_json, '$.entity_kind'), json_extract(payload_json, '$.id')) WHERE payload_type='work_item';
|
|
2143
2153
|
CREATE VIEW files AS SELECT json_extract(payload_json, '$.path') AS path, json_extract(payload_json, '$.content') AS content, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.content_sha256') AS content_sha256, json_extract(payload_json, '$.artifact_kind') AS artifact_kind, json_extract(payload_json, '$.artifact_id') AS artifact_id, json_extract(payload_json, '$.artifact_occurred_at') AS artifact_occurred_at, json_extract(payload_json, '$.artifact_occurred_day_sp') AS artifact_occurred_day_sp, json_extract(payload_json, '$.artifact_task_id') AS artifact_task_id, json_extract(payload_json, '$.artifact_change_id') AS artifact_change_id, json_extract(payload_json, '$.artifact_run_id') AS artifact_run_id FROM zaip_aios_activities WHERE payload_type='file';
|
|
2144
2154
|
CREATE VIEW read_model_source_state AS SELECT json_extract(payload_json, '$.source_path') AS source_path, json_extract(payload_json, '$.source_hash') AS source_hash, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.source_kind') AS source_kind, json_extract(payload_json, '$.last_processed_at') AS last_processed_at FROM zaip_aios_activities WHERE payload_type='source_state';
|
|
2145
2155
|
CREATE VIEW read_model_metadata AS SELECT json_extract(payload_json, '$.key') AS key, json_extract(payload_json, '$.value') AS value FROM zaip_aios_activities WHERE payload_type='metadata';
|
|
@@ -2182,44 +2192,6 @@ export async function rebuildSqliteReadModel(
|
|
|
2182
2192
|
CREATE VIEW sub_runs AS SELECT * FROM activities WHERE run_role = 'subrun';
|
|
2183
2193
|
CREATE VIEW invocations AS SELECT activity_row_id, activity_key, version_activity_id, source_occurred_at, root_agent_run_id AS root_agent_invocation_id, parent_agent_run_id AS parent_agent_invocation_id, execution_id, task_id, spec_id, prd_id, change_id, conversation_id, run_id, occurred_at, occurred_day_sp, invocation_kind, delegation_depth, zaip_agent, zaip_agent_persona, zaip_agent_job, zaip_agent_role, client, provider, model, phase, purpose, status, source_started_at, source_finished_at, attributed_token_active_ms, attributed_token_active_ms AS duration_ms, CASE WHEN attributed_token_active_ms IS NOT NULL THEN 'observed' ELSE 'unavailable' END AS duration_provenance, CASE WHEN attributed_token_active_ms IS NOT NULL THEN 'attributed_token_active' ELSE NULL END AS duration_fact_provenance, input_tokens, cached_input_tokens, output_tokens, reasoning_tokens, total_tokens, token_source, cost_usd, cost_brl, cost_method, pricing_model, pricing_source, fx_rate, service_tier, reasoning_effort, source_event_count, usage_event_count, llm_call_count, agent_run_id AS agent_invocation_id FROM agent_runs;
|
|
2184
2194
|
CREATE VIEW invocation_usage AS SELECT agent_invocation_id, agent_invocation_id AS usage_index, provider, model, service_tier, reasoning_effort, input_tokens, cached_input_tokens, uncached_input_tokens, output_tokens, reasoning_tokens, visible_output_tokens, total_tokens, input_rate_usd_per_million, cached_input_rate_usd_per_million, output_rate_usd_per_million, reasoning_rate_usd_per_million, context_input_multiplier, context_output_multiplier, speed_cost_multiplier, pricing_scenario, standard_cost_usd, speed_uplift_usd, input_cost_usd, cached_input_cost_usd, output_cost_usd, reasoning_cost_usd, calculated_cost_usd, recorded_cost_usd, cost_variance_usd, cost_usd, cost_brl, cost_method, pricing_model, pricing_source, fx_rate FROM invocation_usage_segments;
|
|
2185
|
-
-- Flatten the small financial dimension fact once during rebuild.
|
|
2186
|
-
-- Models/Providers must never execute JSON extraction for each paint.
|
|
2187
|
-
CREATE TABLE cost_invocation_dimension_facts AS
|
|
2188
|
-
SELECT i.agent_invocation_id, date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') AS occurred_day_sp,
|
|
2189
|
-
CASE WHEN u.model IS NULL OR trim(u.model)='' OR u.model='unavailable' THEN 'unavailable' ELSE u.model END AS model,
|
|
2190
|
-
CASE WHEN u.provider IS NULL OR trim(u.provider)='' OR u.provider='unavailable' THEN 'unavailable' ELSE u.provider END AS provider,
|
|
2191
|
-
u.input_tokens, u.cached_input_tokens, u.output_tokens, u.reasoning_tokens,
|
|
2192
|
-
u.total_tokens, u.cost_usd, u.cost_brl, i.attributed_token_active_ms AS duration_ms, 0 AS residual
|
|
2193
|
-
FROM invocations i JOIN invocation_usage u USING(agent_invocation_id)
|
|
2194
|
-
WHERE u.total_tokens IS NOT NULL OR u.cost_usd IS NOT NULL OR u.cost_brl IS NOT NULL;
|
|
2195
|
-
CREATE INDEX cost_dimension_facts_day_model ON cost_invocation_dimension_facts(occurred_day_sp, model);
|
|
2196
|
-
CREATE INDEX cost_dimension_facts_day_provider ON cost_invocation_dimension_facts(occurred_day_sp, provider);
|
|
2197
|
-
CREATE INDEX cost_dimension_facts_invocation ON cost_invocation_dimension_facts(agent_invocation_id);
|
|
2198
|
-
INSERT INTO cost_invocation_dimension_facts
|
|
2199
|
-
SELECT i.agent_invocation_id, date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') AS occurred_day_sp, 'unavailable', 'unavailable',
|
|
2200
|
-
i.input_tokens-COALESCE(s.input_tokens,0), i.cached_input_tokens-COALESCE(s.cached_input_tokens,0),
|
|
2201
|
-
i.output_tokens-COALESCE(s.output_tokens,0), i.reasoning_tokens-COALESCE(s.reasoning_tokens,0),
|
|
2202
|
-
i.total_tokens-COALESCE(s.total_tokens,0), i.cost_usd-COALESCE(s.cost_usd,0),
|
|
2203
|
-
i.cost_brl-COALESCE(s.cost_brl,0), i.attributed_token_active_ms, 1
|
|
2204
|
-
FROM invocations i LEFT JOIN (
|
|
2205
|
-
SELECT agent_invocation_id, SUM(input_tokens) input_tokens, SUM(cached_input_tokens) cached_input_tokens,
|
|
2206
|
-
SUM(output_tokens) output_tokens, SUM(reasoning_tokens) reasoning_tokens, SUM(total_tokens) total_tokens,
|
|
2207
|
-
SUM(cost_usd) cost_usd, SUM(cost_brl) cost_brl
|
|
2208
|
-
FROM cost_invocation_dimension_facts GROUP BY agent_invocation_id
|
|
2209
|
-
) s USING(agent_invocation_id)
|
|
2210
|
-
WHERE i.total_tokens<>COALESCE(s.total_tokens,0) OR i.cost_usd<>COALESCE(s.cost_usd,0) OR i.cost_brl<>COALESCE(s.cost_brl,0);
|
|
2211
|
-
-- Financial activities outside Agent Invocation telemetry are still
|
|
2212
|
-
-- factual Cost Monitor facts. Give them a stable synthetic key and
|
|
2213
|
-
-- retain them as the explicit no-dimension population.
|
|
2214
|
-
INSERT INTO cost_invocation_dimension_facts
|
|
2215
|
-
SELECT 'activity:' || a.activity_row_id, a.occurred_day_sp, 'unavailable', 'unavailable',
|
|
2216
|
-
a.input_tokens, a.cached_input_tokens, a.output_tokens, a.reasoning_tokens,
|
|
2217
|
-
a.total_tokens, a.cost_usd, a.cost_brl, f.attributed_token_active_ms, 1
|
|
2218
|
-
FROM zaip_aios_activities a
|
|
2219
|
-
LEFT JOIN attributed_token_active_facts f USING(activity_row_id)
|
|
2220
|
-
WHERE a.payload_type IS NULL AND a.activity_source_kind <> 'agent_run'
|
|
2221
|
-
AND (a.total_tokens IS NOT NULL OR a.cost_usd IS NOT NULL OR a.cost_brl IS NOT NULL)
|
|
2222
|
-
AND NOT EXISTS (SELECT 1 FROM cost_invocation_dimension_facts d WHERE d.agent_invocation_id='activity:' || a.activity_row_id);
|
|
2223
2195
|
CREATE VIEW work_items AS SELECT entity_kind, id, title, status, risk, occurred_at, occurred_day_sp FROM canonical_work_items UNION ALL SELECT 'task', task_id, COALESCE(MAX(title), task_id), MAX(work_item_status), MAX(risk), MIN(occurred_at), MIN(occurred_day_sp) FROM activities WHERE task_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM canonical_work_items c WHERE c.entity_kind='task' AND c.id=activities.task_id) GROUP BY task_id UNION ALL SELECT 'spec', spec_id, COALESCE(MAX(title), spec_id), MAX(work_item_status), MAX(risk), MIN(occurred_at), MIN(occurred_day_sp) FROM activities WHERE spec_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM canonical_work_items c WHERE c.entity_kind='spec' AND c.id=activities.spec_id) GROUP BY spec_id UNION ALL SELECT 'prd', prd_id, COALESCE(MAX(title), prd_id), MAX(work_item_status), MAX(risk), MIN(occurred_at), MIN(occurred_day_sp) FROM activities WHERE prd_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM canonical_work_items c WHERE c.entity_kind='prd' AND c.id=activities.prd_id) GROUP BY prd_id;
|
|
2224
2196
|
CREATE VIEW tasks AS SELECT id, title, status, risk, occurred_at, occurred_day_sp FROM work_items WHERE entity_kind = 'task';
|
|
2225
2197
|
CREATE VIEW specs AS SELECT id, title, status, risk, occurred_at, occurred_day_sp FROM work_items WHERE entity_kind = 'spec';
|
|
@@ -3167,7 +3139,7 @@ export async function rebuildSqliteReadModel(
|
|
|
3167
3139
|
cachePath,
|
|
3168
3140
|
fingerprint,
|
|
3169
3141
|
readModelSchemaVersion,
|
|
3170
|
-
|
|
3142
|
+
hasReusableSnapshotPayloads,
|
|
3171
3143
|
) === null
|
|
3172
3144
|
) {
|
|
3173
3145
|
await rm(temporaryPath, { force: true });
|
|
@@ -2,9 +2,13 @@ import { copyFile, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promis
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { hasRequiredFirstFoldPayloads } from "./sqlite-first-fold-persistence.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
rebuildSqliteReadModel,
|
|
9
|
+
SqliteReadModelFileSystem,
|
|
10
|
+
validateSqliteReadModelSchema,
|
|
11
|
+
} from "./sqlite-read-model.js";
|
|
8
12
|
|
|
9
13
|
const roots: string[] = [];
|
|
10
14
|
|
|
@@ -13,6 +17,45 @@ afterEach(async () => {
|
|
|
13
17
|
});
|
|
14
18
|
|
|
15
19
|
describe("SQLite snapshot seed publication", () => {
|
|
20
|
+
it.each([
|
|
21
|
+
"old-schema",
|
|
22
|
+
"missing-index",
|
|
23
|
+
])("rebuilds incompatible %s generations and stale seeds", async (damage) => {
|
|
24
|
+
const root = await mkdtemp(join(tmpdir(), "zaip-snapshot-index-upgrade-"));
|
|
25
|
+
roots.push(root);
|
|
26
|
+
await mkdir(join(root, ".agents", "config"), { recursive: true });
|
|
27
|
+
await writeFile(join(root, ".agents", "config", "project.yaml"), "zaip:\n enabled: true\n");
|
|
28
|
+
const destination = join(root, ".local", "dashboard-sqlite", "read-model.sqlite");
|
|
29
|
+
const cacheDirectory = join(root, ".local", "dashboard-sqlite", "cache");
|
|
30
|
+
vi.stubEnv("ZAIP_SQLITE_DB_PATH", destination);
|
|
31
|
+
vi.stubEnv("ZAIP_SQLITE_CACHE_DIR", cacheDirectory);
|
|
32
|
+
vi.stubEnv("ZAIP_SQLITE_PREFER_LAST_VALID_SEED", "1");
|
|
33
|
+
try {
|
|
34
|
+
await expect(rebuildSqliteReadModel(root)).resolves.toMatchObject({ cache: "rebuilt" });
|
|
35
|
+
const seedName = (await readdir(cacheDirectory)).find((name) => name.endsWith(".sqlite"));
|
|
36
|
+
if (seedName === undefined) throw new Error("Expected the rebuilt seed");
|
|
37
|
+
for (const path of [destination, join(cacheDirectory, seedName)]) {
|
|
38
|
+
const database = new DatabaseSync(path);
|
|
39
|
+
try {
|
|
40
|
+
if (damage === "missing-index")
|
|
41
|
+
database.exec("DROP INDEX zaip_activities_work_item_identity");
|
|
42
|
+
else
|
|
43
|
+
database.exec(
|
|
44
|
+
"UPDATE zaip_aios_activities SET payload_json=json_set(payload_json, '$.value', '70') WHERE payload_type='metadata' AND json_extract(payload_json, '$.key')='schema_version'",
|
|
45
|
+
);
|
|
46
|
+
} finally {
|
|
47
|
+
database.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await expect(rebuildSqliteReadModel(root)).resolves.toMatchObject({ cache: "rebuilt" });
|
|
51
|
+
expect(() => validateSqliteReadModelSchema(destination)).not.toThrow();
|
|
52
|
+
await expect(rebuildSqliteReadModel(root)).resolves.toMatchObject({ cache: "stale_seed" });
|
|
53
|
+
expect(() => validateSqliteReadModelSchema(destination)).not.toThrow();
|
|
54
|
+
} finally {
|
|
55
|
+
vi.unstubAllEnvs();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
16
59
|
it("skips a newer incomplete seed and materializes the prior complete cache", async () => {
|
|
17
60
|
const root = await mkdtemp(join(tmpdir(), "zaip-snapshot-seed-"));
|
|
18
61
|
roots.push(root);
|
|
@@ -58,6 +101,40 @@ describe("SQLite snapshot seed publication", () => {
|
|
|
58
101
|
}
|
|
59
102
|
});
|
|
60
103
|
|
|
104
|
+
it("uses a compatible stale seed before source inventory when launcher policy requests it", async () => {
|
|
105
|
+
const root = await mkdtemp(join(tmpdir(), "zaip-snapshot-seed-early-"));
|
|
106
|
+
roots.push(root);
|
|
107
|
+
await mkdir(join(root, ".agents", "records", "tasks"), { recursive: true });
|
|
108
|
+
await mkdir(join(root, ".agents", "config"), { recursive: true });
|
|
109
|
+
await writeFile(join(root, ".agents", "config", "project.yaml"), "zaip:\n enabled: true\n");
|
|
110
|
+
await writeFile(join(root, ".agents", "records", "tasks", "task.md"), "# initial\n");
|
|
111
|
+
await rebuildSqliteReadModel(root);
|
|
112
|
+
await writeFile(join(root, ".agents", "records", "tasks", "task.md"), "# changed\n");
|
|
113
|
+
const previousPath = process.env.ZAIP_SQLITE_DB_PATH;
|
|
114
|
+
const previousSeed = process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED;
|
|
115
|
+
const previousProgress = process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS;
|
|
116
|
+
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
117
|
+
try {
|
|
118
|
+
process.env.ZAIP_SQLITE_DB_PATH = join(root, ".local", "dashboard-sqlite", "served.sqlite");
|
|
119
|
+
process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED = "1";
|
|
120
|
+
process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS = "1";
|
|
121
|
+
await expect(rebuildSqliteReadModel(root)).resolves.toMatchObject({
|
|
122
|
+
cache: "stale_seed",
|
|
123
|
+
fingerprint_ms: 0,
|
|
124
|
+
});
|
|
125
|
+
expect(stderr.mock.calls.join("\n")).toContain('"phase":"stale_seed_ready"');
|
|
126
|
+
expect(stderr.mock.calls.join("\n")).not.toContain('"phase":"source_fingerprint_started"');
|
|
127
|
+
} finally {
|
|
128
|
+
stderr.mockRestore();
|
|
129
|
+
if (previousPath === undefined) delete process.env.ZAIP_SQLITE_DB_PATH;
|
|
130
|
+
else process.env.ZAIP_SQLITE_DB_PATH = previousPath;
|
|
131
|
+
if (previousSeed === undefined) delete process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED;
|
|
132
|
+
else process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED = previousSeed;
|
|
133
|
+
if (previousProgress === undefined) delete process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS;
|
|
134
|
+
else process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS = previousProgress;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
61
138
|
it("rejects a seed whose board payload is truncated below canonical work", async () => {
|
|
62
139
|
const root = await mkdtemp(join(tmpdir(), "zaip-snapshot-truncated-"));
|
|
63
140
|
roots.push(root);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { rebuildSqliteReadModel, SqliteReadModelFileSystem } from "./sqlite-read-model.js";
|
|
7
|
+
|
|
8
|
+
const roots: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("SQLite work-item identity index", () => {
|
|
15
|
+
it("uses the partial JSON identity index without changing canonical work-list IDs", async () => {
|
|
16
|
+
const root = await mkdtemp(join(tmpdir(), "zaip-work-item-index-"));
|
|
17
|
+
roots.push(root);
|
|
18
|
+
await mkdir(join(root, ".agents", "config"), { recursive: true });
|
|
19
|
+
await mkdir(join(root, ".agents", "records", "tasks"), { recursive: true });
|
|
20
|
+
await writeFile(join(root, ".agents", "config", "project.yaml"), "zaip:\n enabled: true\n");
|
|
21
|
+
await writeFile(
|
|
22
|
+
join(root, ".agents", "records", "tasks", "TASK-INDEXED.md"),
|
|
23
|
+
"---\nid: TASK-INDEXED\ntitle: Indexed canonical task\nstatus: ready\nrisk: standard\ncreated_at: 2026-08-31T12:00:00.000Z\n---\n# Indexed canonical task\n",
|
|
24
|
+
);
|
|
25
|
+
const previousDbPath = process.env.ZAIP_SQLITE_DB_PATH;
|
|
26
|
+
process.env.ZAIP_SQLITE_DB_PATH = join(root, ".local", "dashboard-sqlite", "read-model.sqlite");
|
|
27
|
+
let filesystem: SqliteReadModelFileSystem | undefined;
|
|
28
|
+
try {
|
|
29
|
+
await rebuildSqliteReadModel(root);
|
|
30
|
+
filesystem = new SqliteReadModelFileSystem(root);
|
|
31
|
+
expect(
|
|
32
|
+
filesystem
|
|
33
|
+
.viewPage("operations_work_list", { limit: 10, offset: 0 })
|
|
34
|
+
.rows.map((row) => row.id),
|
|
35
|
+
).toContain("TASK-INDEXED");
|
|
36
|
+
const database = new DatabaseSync(process.env.ZAIP_SQLITE_DB_PATH, { readOnly: true });
|
|
37
|
+
try {
|
|
38
|
+
const plan = database
|
|
39
|
+
.prepare(`EXPLAIN QUERY PLAN
|
|
40
|
+
SELECT a.task_id FROM zaip_vw_activities a WHERE a.task_id IS NOT NULL
|
|
41
|
+
AND NOT EXISTS (SELECT 1 FROM zaip_vw_canonical_work_items c WHERE c.entity_kind='task' AND c.id=a.task_id)
|
|
42
|
+
GROUP BY a.task_id`)
|
|
43
|
+
.all() as Array<{ detail: string }>;
|
|
44
|
+
expect(plan.map((row) => row.detail).join("\n")).toContain(
|
|
45
|
+
"zaip_activities_work_item_identity",
|
|
46
|
+
);
|
|
47
|
+
} finally {
|
|
48
|
+
database.close();
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
filesystem?.close();
|
|
52
|
+
if (previousDbPath === undefined) delete process.env.ZAIP_SQLITE_DB_PATH;
|
|
53
|
+
else process.env.ZAIP_SQLITE_DB_PATH = previousDbPath;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -8,6 +8,8 @@ export async function createDashboardLaunchPage({ port, host, runtimeKind, healt
|
|
|
8
8
|
phase: "Iniciando o launcher local",
|
|
9
9
|
detail: "Abrindo a página de preparação.",
|
|
10
10
|
};
|
|
11
|
+
let progressSteps = [{ phase: progress.phase, detail: progress.detail }];
|
|
12
|
+
progress = { ...progress, steps: progressSteps };
|
|
11
13
|
const connections = new Set();
|
|
12
14
|
let resolveRedirectObserved;
|
|
13
15
|
const redirectObserved = new Promise((resolveObserved) => {
|
|
@@ -49,18 +51,26 @@ export async function createDashboardLaunchPage({ port, host, runtimeKind, healt
|
|
|
49
51
|
<style>
|
|
50
52
|
:root{color-scheme:dark;font-family:Inter,system-ui,sans-serif;background:#010610;color:#f1f5f9}
|
|
51
53
|
*{box-sizing:border-box}body{margin:0;background:#010610}.launch-overlay{position:fixed;inset:0;width:100vw;height:100vh;display:grid;place-items:center;padding:clamp(18px,5vw,72px);background:radial-gradient(circle at 50% 35%,rgba(0,176,240,.11),transparent 42%),#010610}
|
|
52
|
-
.progress{width:min(100%,760px);padding:clamp(24px,4vw,42px);border:1px solid rgba(125,211,252,.2);border-radius:20px;background:rgba(3,9,22,.94);box-shadow:0 24px 80px rgba(0,0,0,.45);backdrop-filter:blur(10px)}.progress strong,.progress span:not(.handoff):not(.failure){display:block}.progress strong{font-size:clamp(1.05rem,2vw,1.35rem)}.progress span{color:#b6c6db;font-size:clamp(.88rem,1.5vw,1rem);margin-top:7px}.track{position:relative;height:9px;margin-top:20px;border-radius:999px;background:#172033;overflow:hidden}.track::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(224,242,254,.14) 35%,rgba(224,242,254,.8) 50%,rgba(224,242,254,.14) 65%,transparent 100%);transform:translateX(-100%);animation:launch-activity 1.35s ease-in-out infinite;pointer-events:none}.progress.terminal-failure .track::after{animation:none;transform:none;background:rgba(254,202,202,.2)}.bar{position:relative;z-index:1;height:100%;width:4%;border-radius:inherit;background:linear-gradient(90deg,#38bdf8,#818cf8);transition:width .35s ease}@keyframes launch-activity{to{transform:translateX(100%)}}@media (prefers-reduced-motion:reduce){.track::after{animation:none;transform:none;background:linear-gradient(90deg,transparent 0%,rgba(224,242,254,.2) 50%,transparent 100%)}}.handoff,.failure{display:none;margin-top:16px;color:#dbeafe}.failure{color:#fecaca}
|
|
54
|
+
.progress{width:min(100%,760px);padding:clamp(24px,4vw,42px);border:1px solid rgba(125,211,252,.2);border-radius:20px;background:rgba(3,9,22,.94);box-shadow:0 24px 80px rgba(0,0,0,.45);backdrop-filter:blur(10px)}.progress strong,.progress span:not(.handoff):not(.failure){display:block}.progress strong{font-size:clamp(1.05rem,2vw,1.35rem)}.progress span{color:#b6c6db;font-size:clamp(.88rem,1.5vw,1rem);margin-top:7px}.track{position:relative;height:9px;margin-top:20px;border-radius:999px;background:#172033;overflow:hidden}.track::after{content:"";position:absolute;z-index:2;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(224,242,254,.14) 35%,rgba(224,242,254,.8) 50%,rgba(224,242,254,.14) 65%,transparent 100%);transform:translateX(-100%);animation:launch-activity 1.35s ease-in-out infinite;pointer-events:none}.progress.terminal-failure .track::after{animation:none;transform:none;background:rgba(254,202,202,.2)}.progress.terminal-failure .bar{background:#f87171}.bar{position:relative;z-index:1;height:100%;width:4%;border-radius:inherit;background:linear-gradient(90deg,#38bdf8,#818cf8);transition:width .35s ease}@keyframes launch-activity{to{transform:translateX(100%)}}@media (prefers-reduced-motion:reduce){.track::after{animation:none;transform:none;background:linear-gradient(90deg,transparent 0%,rgba(224,242,254,.2) 50%,transparent 100%)}}.handoff,.failure{display:none;margin-top:16px;color:#dbeafe}.failure{color:#fecaca}
|
|
55
|
+
.activity{min-height:1.3em}.steps{display:grid;gap:7px;max-height:12.5rem;overflow:auto;margin:18px 0 0;padding:0;list-style:none}.steps li{position:relative;padding-left:20px;color:#94a3b8;font-size:.82rem}.steps li::before{content:"";position:absolute;left:1px;top:.4em;width:8px;height:8px;border-radius:50%;background:#475569}.steps li[data-state="active"]{color:#dbeafe}.steps li[data-state="active"]::before{background:#38bdf8;box-shadow:0 0 0 4px rgba(56,189,248,.16)}.progress.terminal-failure .steps li[data-state="active"]::before{background:#f87171}@media (prefers-reduced-motion:reduce){.bar{transition:none}}
|
|
53
56
|
</style>
|
|
54
57
|
</head>
|
|
55
58
|
<body>
|
|
56
|
-
<main class="launch-overlay" aria-label="Preparando dashboard">
|
|
57
|
-
<section class="progress" aria-live="polite" role="status"><strong id="phase">Iniciando o launcher local</strong><span id="detail">Abrindo a página de preparação.</span><div class="track" id="track" aria-label="Preparação em andamento"><div class="bar" id="bar"></div></div><span class="handoff" id="handoff">O dashboard está pronto. Abrindo a tela principal automaticamente…</span><span class="failure" id="failure" role="alert"></span></section>
|
|
59
|
+
<main class="launch-overlay" aria-label="Preparando dashboard" aria-busy="true">
|
|
60
|
+
<section class="progress" aria-live="polite" role="status"><strong id="phase">Iniciando o launcher local</strong><span id="detail">Abrindo a página de preparação.</span><span class="activity" id="activity" aria-hidden="true">Atividade em andamento.</span><div class="track" id="track" role="progressbar" aria-label="Preparação em andamento" aria-valuemin="0" aria-valuemax="100" aria-valuenow="4" aria-valuetext="Iniciando o launcher local"><div class="bar" id="bar"></div></div><ol class="steps" id="steps" aria-label="Etapas de preparação"><li data-state="active">Iniciando o launcher local</li></ol><span class="handoff" id="handoff">O dashboard está pronto. Abrindo a tela principal automaticamente…</span><span class="failure" id="failure" role="alert"></span></section>
|
|
58
61
|
</main>
|
|
59
62
|
<script>
|
|
60
|
-
let handoffTarget=null,progressRequestInFlight=false,terminalFailure=false;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
let handoffTarget=null,progressRequestInFlight=false,terminalFailure=false,lastProgressPercent=4,compilationStartedAt=null,compilationDetail="",compilationTicker=null,activeStepKey="",activeStepStartedAt=Date.now();
|
|
64
|
+
const reducedMotion=window.matchMedia?.("(prefers-reduced-motion: reduce)").matches===true;
|
|
65
|
+
const recordedStepKeys=new Set(["Iniciando o launcher local\\u0000Abrindo a página de preparação."]);
|
|
66
|
+
function stopCompilationTicker(){if(compilationTicker!==null){clearInterval(compilationTicker);compilationTicker=null}compilationStartedAt=null}
|
|
67
|
+
function renderCompilationElapsed(){if(compilationStartedAt===null||handoffTarget||terminalFailure)return;const seconds=Math.floor((Date.now()-compilationStartedAt)/1000);document.getElementById("detail").textContent=compilationDetail+" · Compilação em andamento · "+seconds+"s"}
|
|
68
|
+
function renderActivityElapsed(){if(handoffTarget||terminalFailure)return;const seconds=Math.floor((Date.now()-activeStepStartedAt)/1000);document.getElementById("activity").textContent="Etapa atual em andamento · "+seconds+"s"}
|
|
69
|
+
function recordStep(step){if(!step||typeof step.phase!=="string")return;const detail=typeof step.detail==="string"?step.detail:"";const key=step.phase+"\\u0000"+detail;if(recordedStepKeys.has(key))return;recordedStepKeys.add(key);const list=document.getElementById("steps");for(const item of list.querySelectorAll("li[data-state=active]"))item.dataset.state="complete";const item=document.createElement("li");item.dataset.state="active";item.textContent=detail?step.phase+" — "+detail:step.phase;list.append(item);while(list.children.length>10)list.removeChild(list.firstElementChild);list.scrollTop=list.scrollHeight}
|
|
70
|
+
function setProgressBar(percent,phase){lastProgressPercent=percent;document.getElementById("bar").style.width=percent+"%";document.getElementById("track").setAttribute("aria-valuenow",String(percent));document.getElementById("track").setAttribute("aria-valuetext",phase)}
|
|
71
|
+
function showHandoff(target){handoffTarget=target;stopCompilationTicker();setProgressBar(100,"Dashboard pronto");document.querySelector(".launch-overlay").setAttribute("aria-busy","false");document.getElementById("handoff").style.display="block";document.getElementById("phase").textContent="Dashboard pronto";document.getElementById("detail").textContent="Concluindo a abertura da tela principal.";document.getElementById("activity").textContent="Preparação concluída."}
|
|
72
|
+
function showFailure(next,percent=lastProgressPercent){terminalFailure=true;clearInterval(progressPoll);stopCompilationTicker();document.querySelector(".launch-overlay").setAttribute("aria-busy","false");document.getElementById("phase").textContent=next.phase;document.getElementById("detail").textContent="A preparação não foi concluída. Consulte a mensagem abaixo para agir.";setProgressBar(percent,next.phase);document.getElementById("handoff").style.display="none";document.getElementById("failure").textContent=next.detail;document.getElementById("failure").style.display="block";document.querySelector(".progress").classList.add("terminal-failure")}
|
|
73
|
+
async function updateProgress(){if(progressRequestInFlight||terminalFailure)return terminalFailure;progressRequestInFlight=true;try{const r=await fetch("/api/v2/progress",{cache:"no-store",signal:AbortSignal.timeout(1000)});const p=await r.json();if(terminalFailure)return true;const percent=Math.max(4,Math.min(100,Number(p.percent)||4));if(p.failure){showFailure(p.failure,percent);return true}if(!handoffTarget){for(const step of Array.isArray(p.steps)?p.steps:[p])recordStep(step);const nextStepKey=p.phase+"\\u0000"+(p.detail??"");if(nextStepKey!==activeStepKey){activeStepKey=nextStepKey;activeStepStartedAt=Date.now()}document.getElementById("phase").textContent=p.phase;const compiling=p.phase.startsWith("Atualizando ");if(compiling){if(compilationStartedAt===null){compilationStartedAt=Date.now();compilationDetail=p.detail;if(!reducedMotion)compilationTicker=setInterval(renderCompilationElapsed,1000)}renderCompilationElapsed()}else{stopCompilationTicker();document.getElementById("detail").textContent=p.detail}renderActivityElapsed()}if(!terminalFailure&&!handoffTarget)setProgressBar(percent,p.phase)}catch{}finally{progressRequestInFlight=false}return terminalFailure}
|
|
64
74
|
const progressPoll=setInterval(updateProgress,250);
|
|
65
75
|
async function waitForDashboard(){try{if(await updateProgress())return;const ready=await fetch("/api/v2/ready",{cache:"no-store",signal:AbortSignal.timeout(1000)});const next=await ready.json();if(next.failure){showFailure(next.failure);return}if(next.target){showHandoff(next.target);clearInterval(progressPoll);setTimeout(()=>location.replace(next.target),0);return}const response=await fetch("${healthPath}",{cache:"no-store",signal:AbortSignal.timeout(1000)});const payload=await response.json();if(response.ok&&payload.kind==="${runtimeKind}"){clearInterval(progressPoll);location.reload();return}}catch{}setTimeout(waitForDashboard,250)}
|
|
66
76
|
updateProgress();setTimeout(waitForDashboard,100)
|
|
@@ -86,10 +96,18 @@ export async function createDashboardLaunchPage({ port, host, runtimeKind, healt
|
|
|
86
96
|
redirectUrl = target;
|
|
87
97
|
},
|
|
88
98
|
setProgress: (next) => {
|
|
99
|
+
const nextStep = {
|
|
100
|
+
phase: next.phase ?? progress.phase,
|
|
101
|
+
detail: next.detail ?? progress.detail,
|
|
102
|
+
};
|
|
103
|
+
const previousStep = progressSteps.at(-1);
|
|
104
|
+
if (previousStep?.phase !== nextStep.phase || previousStep.detail !== nextStep.detail)
|
|
105
|
+
progressSteps = [...progressSteps, nextStep].slice(-10);
|
|
89
106
|
progress = {
|
|
90
107
|
...progress,
|
|
91
108
|
...next,
|
|
92
109
|
percent: Math.max(progress.percent, next.percent ?? progress.percent),
|
|
110
|
+
steps: progressSteps,
|
|
93
111
|
};
|
|
94
112
|
},
|
|
95
113
|
setFailure: (next) => {
|
|
@@ -97,7 +115,7 @@ export async function createDashboardLaunchPage({ port, host, runtimeKind, healt
|
|
|
97
115
|
progress = {
|
|
98
116
|
...progress,
|
|
99
117
|
...next,
|
|
100
|
-
percent:
|
|
118
|
+
percent: progress.percent,
|
|
101
119
|
};
|
|
102
120
|
},
|
|
103
121
|
waitForRedirect: async (timeoutMs = 1_500) =>
|
package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs
CHANGED
|
@@ -16,7 +16,14 @@ describe("dashboard launch page", () => {
|
|
|
16
16
|
expect(html).toContain("position:fixed;inset:0;width:100vw;height:100vh");
|
|
17
17
|
expect(html).toContain("display:grid;place-items:center");
|
|
18
18
|
expect(html).toContain('aria-live="polite" role="status"');
|
|
19
|
-
expect(html).toContain('aria-label="Preparação em andamento"');
|
|
19
|
+
expect(html).toContain('role="progressbar" aria-label="Preparação em andamento"');
|
|
20
|
+
expect(html).toContain('aria-valuemin="0" aria-valuemax="100" aria-valuenow="4"');
|
|
21
|
+
expect(html).toContain('aria-busy="true"');
|
|
22
|
+
expect(html).toContain("z-index:2");
|
|
23
|
+
expect(html).toContain("Compilação em andamento");
|
|
24
|
+
expect(html).toContain('id="steps" aria-label="Etapas de preparação"');
|
|
25
|
+
expect(html).toContain('id="activity" aria-hidden="true"');
|
|
26
|
+
expect(html).toContain("function recordStep(step)");
|
|
20
27
|
expect(html).toContain("animation:launch-activity 1.35s ease-in-out infinite");
|
|
21
28
|
expect(html).toContain("@media (prefers-reduced-motion:reduce)");
|
|
22
29
|
expect(html).toContain("O dashboard está pronto. Abrindo a tela principal automaticamente…");
|
|
@@ -27,6 +34,12 @@ describe("dashboard launch page", () => {
|
|
|
27
34
|
await expect(progress.json()).resolves.toMatchObject({
|
|
28
35
|
phase: "Iniciando o launcher local",
|
|
29
36
|
percent: expect.any(Number),
|
|
37
|
+
steps: [
|
|
38
|
+
{
|
|
39
|
+
phase: "Iniciando o launcher local",
|
|
40
|
+
detail: "Abrindo a página de preparação.",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
30
43
|
});
|
|
31
44
|
expect(health.status).toBe(503);
|
|
32
45
|
await expect(health.json()).resolves.toEqual({
|
|
@@ -62,10 +75,26 @@ describe("dashboard launch page", () => {
|
|
|
62
75
|
page.locator("#handoff").evaluate((element) => getComputedStyle(element).display),
|
|
63
76
|
)
|
|
64
77
|
.toBe("none");
|
|
65
|
-
launchPage.setProgress({
|
|
78
|
+
launchPage.setProgress({
|
|
79
|
+
percent: 64,
|
|
80
|
+
phase: "Atualizando a interface",
|
|
81
|
+
detail: "Uma dependência deste artefato foi modificada.",
|
|
82
|
+
});
|
|
83
|
+
await expect.poll(() => page.locator("#phase").textContent()).toBe("Atualizando a interface");
|
|
84
|
+
await expect.poll(() => page.locator("#track").getAttribute("aria-valuenow")).toBe("64");
|
|
66
85
|
await expect
|
|
67
|
-
.poll(() => page.locator("#
|
|
68
|
-
.toBe("Atualizando
|
|
86
|
+
.poll(() => page.locator("#track").getAttribute("aria-valuetext"))
|
|
87
|
+
.toBe("Atualizando a interface");
|
|
88
|
+
await expect
|
|
89
|
+
.poll(() => page.locator("#detail").textContent())
|
|
90
|
+
.toContain("Compilação em andamento · 0s");
|
|
91
|
+
await expect.poll(() => page.locator("#steps li").count()).toBe(2);
|
|
92
|
+
await expect
|
|
93
|
+
.poll(() => page.locator("#steps li[data-state=active]").textContent())
|
|
94
|
+
.toContain("Atualizando a interface");
|
|
95
|
+
await expect
|
|
96
|
+
.poll(() => page.locator("#activity").textContent())
|
|
97
|
+
.toContain("Etapa atual em andamento");
|
|
69
98
|
|
|
70
99
|
await page.evaluate(() => window.showHandoff("http://127.0.0.1:4417/?token=ready"));
|
|
71
100
|
await expect
|
|
@@ -73,6 +102,9 @@ describe("dashboard launch page", () => {
|
|
|
73
102
|
page.locator("#handoff").evaluate((element) => getComputedStyle(element).display),
|
|
74
103
|
)
|
|
75
104
|
.toBe("block");
|
|
105
|
+
await expect
|
|
106
|
+
.poll(() => page.locator(".launch-overlay").getAttribute("aria-busy"))
|
|
107
|
+
.toBe("false");
|
|
76
108
|
} finally {
|
|
77
109
|
await browser.close();
|
|
78
110
|
await launchPage.close();
|
|
@@ -83,6 +115,11 @@ describe("dashboard launch page", () => {
|
|
|
83
115
|
const launchPage = await startDashboardLaunchPage(0);
|
|
84
116
|
const browser = await chromium.launch();
|
|
85
117
|
try {
|
|
118
|
+
launchPage.setProgress({
|
|
119
|
+
percent: 64,
|
|
120
|
+
phase: "Atualizando o runtime local",
|
|
121
|
+
detail: "Uma dependência deste artefato foi modificada.",
|
|
122
|
+
});
|
|
86
123
|
launchPage.setFailure({
|
|
87
124
|
percent: 100,
|
|
88
125
|
phase: "Não foi possível preparar o dashboard",
|
|
@@ -136,6 +173,10 @@ describe("dashboard launch page", () => {
|
|
|
136
173
|
await expect
|
|
137
174
|
.poll(() => page.locator("#bar").evaluate((element) => getComputedStyle(element).width))
|
|
138
175
|
.not.toBe("4px");
|
|
176
|
+
await expect.poll(() => page.locator("#track").getAttribute("aria-valuenow")).toBe("64");
|
|
177
|
+
await expect
|
|
178
|
+
.poll(() => page.locator(".launch-overlay").getAttribute("aria-busy"))
|
|
179
|
+
.toBe("false");
|
|
139
180
|
|
|
140
181
|
await launchPage.closeAfterFailureObservation(0);
|
|
141
182
|
await expect
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
export function presentLaunchFailure(launchPage, error, formatError) {
|
|
5
|
+
if (launchPage === null) return null;
|
|
6
|
+
launchPage.setFailure({
|
|
7
|
+
percent: 100,
|
|
8
|
+
phase: "Não foi possível preparar o dashboard",
|
|
9
|
+
detail: formatError(error instanceof Error ? error.message : String(error), 900),
|
|
10
|
+
});
|
|
11
|
+
launchPage.closeAfterFailureObservation();
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createPhaseTimer(appendLauncherLog, launcherPid) {
|
|
16
|
+
return (phase, startedAt, details = {}) => {
|
|
17
|
+
appendLauncherLog(
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
event: "dashboard_sqlite_phase",
|
|
20
|
+
phase,
|
|
21
|
+
elapsed_ms: Date.now() - startedAt,
|
|
22
|
+
launcher_pid: launcherPid,
|
|
23
|
+
...details,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function writeLauncherStatus(status, message = "") {
|
|
30
|
+
const path = process.env.ZAIP_LAUNCH_STATUS;
|
|
31
|
+
if (!path) return;
|
|
32
|
+
const temporaryPath = `${path}.${process.pid}.tmp`;
|
|
33
|
+
await writeFile(temporaryPath, `${status}\n${message}`.trimEnd(), "utf8");
|
|
34
|
+
await rename(temporaryPath, path);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function awaitOwnedDashboardExit({
|
|
38
|
+
child,
|
|
39
|
+
currentSession,
|
|
40
|
+
clearOwnedDashboardSession,
|
|
41
|
+
recordPortStatus,
|
|
42
|
+
removeGeneratedReadModel,
|
|
43
|
+
processRef = process,
|
|
44
|
+
}) {
|
|
45
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
46
|
+
processRef.once(signal, () => child.kill(signal));
|
|
47
|
+
}
|
|
48
|
+
const exitCode = await new Promise((resolveExit, rejectExit) => {
|
|
49
|
+
child.once("error", rejectExit);
|
|
50
|
+
child.once("exit", (code) => resolveExit(code ?? 0));
|
|
51
|
+
});
|
|
52
|
+
await clearOwnedDashboardSession();
|
|
53
|
+
if (currentSession !== null) {
|
|
54
|
+
await recordPortStatus(currentSession, "closed");
|
|
55
|
+
await removeGeneratedReadModel(currentSession.dbPath);
|
|
56
|
+
}
|
|
57
|
+
return exitCode;
|
|
58
|
+
}
|