Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions docs-web/architecture/custom-dashboard-foundation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Custom Dashboard Foundation

Custom dashboards are a persisted domain model for future project-scoped dashboard generation. This foundation stores manifests, generated file bundles, data-source node graphs, validation history, and publication state only. It does not expose HTTP routes, MCP tools, frontend UI, or runtime execution.
Custom dashboards are a persisted domain model for project-scoped dashboard generation. The foundation stores manifests, generated file bundles, data-source node graphs, validation history, and publication state, and the server-side validation runtime can now build and health-check a revision in an isolated Docker session. HTTP routes, MCP tools, and frontend UI are layered separately.

## Contracts

Expand Down Expand Up @@ -35,8 +35,28 @@ All dashboard JSON payloads are stored as text and hydrated through `CustomDashb
- list dashboards by project and load a dashboard by id
- create and update draft metadata, manifests, files, source graphs, styleguides, and runtime metadata
- create immutable revisions from the current draft or explicit payloads
- create/update validation sessions and mark a revision validated
- create/update/delete validation sessions and mark a revision validated
- publish only validated revisions
- archive or delete dashboards

Publishing rejects unvalidated, failed, cancelled, or cross-dashboard revisions. Publishing a new validated revision replaces the prior `custom_dashboard_publications` row for the dashboard, preserving the single-active-publication invariant.

## Validation Runtime

`src/services/custom-dashboard-validation-service.ts` owns server-side validation execution. It consumes `CustomDashboardRepository`, `ProjectManagementRepository`, and `SettingsRepository` through the core dependency factory.

Validation flow:

- `startValidation(projectId, dashboardId, revisionId)` creates a validation session, materializes the immutable revision bundle under `.code-ux/runtime/custom-dashboards/<dashboardId>/<revisionId>/workspace`, and writes a generated Vite/Preact harness.
- The harness injects a read-only Code UX data bridge containing the revision manifest, source node graph, styleguide, runtime metadata, integrations, and declared `external_api` nodes.
- The service runs install/build inside Docker using the resolved `cliWorkflow.containerImage`, then creates and starts a detached serving container on an allocated localhost port.
- A validation session is marked `passed` only after install, build, start, and root URL health checks succeed. Build/start/health failures are recorded as failed validation reports with bounded log excerpts.
- Runtime metadata persists the workspace path, log path, host port, container id/name, image, validation URL path, commands, and latest error/log excerpt so dashboard routes can reuse the detached session later.

Validation does not publish or activate dashboards. A successful run only marks the revision validation status as `passed`; publication remains gated by `publishRevision`.

## Docker and Logs

Docker argument construction lives in `src/services/custom-dashboard-docker-plan.ts`. Validation containers use the configured CLI workflow image, bind-mount only the generated workspace/runtime home plus an optional setup script, and do not mount provider credential directories.

Logs are captured in the validation runtime directory and combined with bounded `docker logs` output through `getValidationLogs(sessionId, tail)`. `stopValidation` removes the detached container while preserving a passed revision report, and `removeValidation` removes the session row after container cleanup.
24 changes: 22 additions & 2 deletions docs/architecture/custom-dashboard-foundation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Custom Dashboard Foundation

Custom dashboards are a persisted domain model for future project-scoped dashboard generation. This foundation stores manifests, generated file bundles, data-source node graphs, validation history, and publication state only. It does not expose HTTP routes, MCP tools, frontend UI, or runtime execution.
Custom dashboards are a persisted domain model for project-scoped dashboard generation. The foundation stores manifests, generated file bundles, data-source node graphs, validation history, and publication state, and the server-side validation runtime can now build and health-check a revision in an isolated Docker session. HTTP routes, MCP tools, and frontend UI are layered separately.

## Contracts

Expand Down Expand Up @@ -35,8 +35,28 @@ All dashboard JSON payloads are stored as text and hydrated through `CustomDashb
- list dashboards by project and load a dashboard by id
- create and update draft metadata, manifests, files, source graphs, styleguides, and runtime metadata
- create immutable revisions from the current draft or explicit payloads
- create/update validation sessions and mark a revision validated
- create/update/delete validation sessions and mark a revision validated
- publish only validated revisions
- archive or delete dashboards

Publishing rejects unvalidated, failed, cancelled, or cross-dashboard revisions. Publishing a new validated revision replaces the prior `custom_dashboard_publications` row for the dashboard, preserving the single-active-publication invariant.

## Validation Runtime

`src/services/custom-dashboard-validation-service.ts` owns server-side validation execution. It consumes `CustomDashboardRepository`, `ProjectManagementRepository`, and `SettingsRepository` through the core dependency factory.

Validation flow:

- `startValidation(projectId, dashboardId, revisionId)` creates a validation session, materializes the immutable revision bundle under `.code-ux/runtime/custom-dashboards/<dashboardId>/<revisionId>/workspace`, and writes a generated Vite/Preact harness.
- The harness injects a read-only Code UX data bridge containing the revision manifest, source node graph, styleguide, runtime metadata, integrations, and declared `external_api` nodes.
- The service runs install/build inside Docker using the resolved `cliWorkflow.containerImage`, then creates and starts a detached serving container on an allocated localhost port.
- A validation session is marked `passed` only after install, build, start, and root URL health checks succeed. Build/start/health failures are recorded as failed validation reports with bounded log excerpts.
- Runtime metadata persists the workspace path, log path, host port, container id/name, image, validation URL path, commands, and latest error/log excerpt so dashboard routes can reuse the detached session later.

Validation does not publish or activate dashboards. A successful run only marks the revision validation status as `passed`; publication remains gated by `publishRevision`.

## Docker and Logs

Docker argument construction lives in `src/services/custom-dashboard-docker-plan.ts`. Validation containers use the configured CLI workflow image, bind-mount only the generated workspace/runtime home plus an optional setup script, and do not mount provider credential directories.

Logs are captured in the validation runtime directory and combined with bounded `docker logs` output through `getValidationLogs(sessionId, tail)`. `stopValidation` removes the detached container while preserving a passed revision report, and `removeValidation` removes the session row after container cleanup.
9 changes: 9 additions & 0 deletions src/app/dependency-factory/core-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { SprintFileBrowserService } from "../../services/sprint-file-browser-ser
import { SprintFileBrowserRepository } from "../../repositories/sprint-file-browser-repository.js";
import { DockerService } from "../../services/docker-service.js";
import { CustomDashboardRepository } from "../../repositories/custom-dashboard-repository.js";
import { CustomDashboardValidationService } from "../../services/custom-dashboard-validation-service.js";

export interface CoreDependencies {
providerRunner: IProviderRunner;
Expand Down Expand Up @@ -123,6 +124,7 @@ export interface CoreDependencies {
sprintFileBrowserService: SprintFileBrowserService;
sprintFileBrowserRepository: SprintFileBrowserRepository;
customDashboardRepository: CustomDashboardRepository;
customDashboardValidationService: CustomDashboardValidationService;
}

export function createCoreDependencies(
Expand Down Expand Up @@ -241,6 +243,12 @@ export function createCoreDependencies(
logger: logger.child({ component: "sprint-file-browser-service" }),
});
const customDashboardRepository = new CustomDashboardRepository(appDbStorage);
const customDashboardValidationService = new CustomDashboardValidationService({
customDashboardRepository,
projectManagementRepository,
settingsRepository,
logger: logger.child({ component: "custom-dashboard-validation-service" }),
});
const sprintMarkdownService = new SprintMarkdownService(projectManagementRepository);
const sprintIssueService = new SprintIssueService({
projectManagementRepository,
Expand Down Expand Up @@ -379,5 +387,6 @@ export function createCoreDependencies(
sprintFileBrowserService,
sprintFileBrowserRepository,
customDashboardRepository,
customDashboardValidationService,
};
}
5 changes: 5 additions & 0 deletions src/repositories/custom-dashboard-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ export class CustomDashboardRepository {
return row ? this.mapValidationSessionRow(row) : null;
}

deleteValidationSession(sessionId: string): void {
this.requireValidationSession(sessionId);
this.db.prepare(`DELETE FROM custom_dashboard_validation_sessions WHERE id = ?`).run(sessionId);
}

markRevisionValidated(
revisionId: string,
validationReport: CustomDashboardValidationReport,
Expand Down
150 changes: 150 additions & 0 deletions src/services/custom-dashboard-docker-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { CONTAINER_SETUP_SCRIPT } from "./cli-workflow-utils.js";
import {
DOCKER_BRIDGE_NETWORK_ARGS,
DOCKER_NO_NEW_PRIVILEGES_ARGS,
toDockerMountArg,
} from "./cli-docker-utils.js";

export const CUSTOM_DASHBOARD_VALIDATION_CONTAINER_PORT = 4173;
export const CUSTOM_DASHBOARD_VALIDATION_LOG_DRIVER = "local";
export const CUSTOM_DASHBOARD_VALIDATION_CONTAINER_WORKSPACE = "/code-ux-custom-dashboard/workspace";
export const CUSTOM_DASHBOARD_VALIDATION_CONTAINER_HOME = "/code-ux-custom-dashboard/home";
export const CUSTOM_DASHBOARD_VALIDATION_CONTAINER_NPM_PREFIX = "/code-ux-custom-dashboard/npm-global";
export const CUSTOM_DASHBOARD_VALIDATION_CONTAINER_NPM_CACHE = "/code-ux-custom-dashboard/npm-cache";

interface CustomDashboardValidationDockerBaseArgs {
projectId: string;
dashboardId: string;
revisionId: string;
sessionId: string;
workspacePath: string;
runtimeHomePath: string;
hostPort?: number | null;
containerName?: string;
userSpec: string | null;
setupScriptSource?: string | null;
shouldRunSetupScriptAtRuntime: boolean;
resolvedImage: string;
bootstrapScript: string;
}

export interface CustomDashboardValidationDockerRunArgs extends CustomDashboardValidationDockerBaseArgs {
command: string;
}

export interface CustomDashboardValidationDockerCreateArgs extends CustomDashboardValidationDockerBaseArgs {
hostPort: number;
containerName: string;
startCommand: string;
}

export function buildCustomDashboardValidationDockerRunArgs(
args: CustomDashboardValidationDockerRunArgs,
): string[] {
const dockerArgs = [
"run",
"--rm",
"--log-driver", CUSTOM_DASHBOARD_VALIDATION_LOG_DRIVER,
...DOCKER_BRIDGE_NETWORK_ARGS,
...DOCKER_NO_NEW_PRIVILEGES_ARGS,
"--workdir", CUSTOM_DASHBOARD_VALIDATION_CONTAINER_WORKSPACE,
"--label", "code-ux.managed=true",
"--label", "code-ux.custom-dashboard-validation-build=true",
"--label", `code-ux.project-id=${args.projectId}`,
"--label", `code-ux.dashboard-id=${args.dashboardId}`,
"--label", `code-ux.revision-id=${args.revisionId}`,
"--label", `code-ux.session-id=${args.sessionId}`,
"--mount", toDockerMountArg({
source: args.workspacePath,
destination: CUSTOM_DASHBOARD_VALIDATION_CONTAINER_WORKSPACE,
readonly: false,
}),
"--mount", toDockerMountArg({
source: args.runtimeHomePath,
destination: CUSTOM_DASHBOARD_VALIDATION_CONTAINER_HOME,
readonly: false,
}),
"-e", `HOME=${CUSTOM_DASHBOARD_VALIDATION_CONTAINER_HOME}`,
"-e", `NPM_CONFIG_PREFIX=${CUSTOM_DASHBOARD_VALIDATION_CONTAINER_NPM_PREFIX}`,
"-e", `NPM_CONFIG_CACHE=${CUSTOM_DASHBOARD_VALIDATION_CONTAINER_NPM_CACHE}`,
];

appendCommonDockerArgs(dockerArgs, args);
dockerArgs.push(
args.resolvedImage,
"bash",
"-c",
args.bootstrapScript,
"dashboard-validator",
"bash",
"-lc",
args.command,
);
return dockerArgs;
}

export function buildCustomDashboardValidationDockerCreateArgs(
args: CustomDashboardValidationDockerCreateArgs,
): string[] {
const dockerArgs = [
"create",
"--name", args.containerName,
"--log-driver", CUSTOM_DASHBOARD_VALIDATION_LOG_DRIVER,
...DOCKER_BRIDGE_NETWORK_ARGS,
...DOCKER_NO_NEW_PRIVILEGES_ARGS,
"-p", `127.0.0.1:${args.hostPort}:${CUSTOM_DASHBOARD_VALIDATION_CONTAINER_PORT}`,
"--workdir", CUSTOM_DASHBOARD_VALIDATION_CONTAINER_WORKSPACE,
"--label", "code-ux.managed=true",
"--label", "code-ux.custom-dashboard-validation=true",
"--label", `code-ux.project-id=${args.projectId}`,
"--label", `code-ux.dashboard-id=${args.dashboardId}`,
"--label", `code-ux.revision-id=${args.revisionId}`,
"--label", `code-ux.session-id=${args.sessionId}`,
"--label", `code-ux.host-port=${args.hostPort}`,
"--mount", toDockerMountArg({
source: args.workspacePath,
destination: CUSTOM_DASHBOARD_VALIDATION_CONTAINER_WORKSPACE,
readonly: false,
}),
"--mount", toDockerMountArg({
source: args.runtimeHomePath,
destination: CUSTOM_DASHBOARD_VALIDATION_CONTAINER_HOME,
readonly: false,
}),
"-e", `HOME=${CUSTOM_DASHBOARD_VALIDATION_CONTAINER_HOME}`,
"-e", "HOST=0.0.0.0",
"-e", `PORT=${CUSTOM_DASHBOARD_VALIDATION_CONTAINER_PORT}`,
"-e", `DASHBOARD_HOST=0.0.0.0`,
"-e", `DASHBOARD_PORT=${CUSTOM_DASHBOARD_VALIDATION_CONTAINER_PORT}`,
];

appendCommonDockerArgs(dockerArgs, args);
dockerArgs.push(
args.resolvedImage,
"bash",
"-c",
args.bootstrapScript,
"dashboard-validator",
"bash",
"-lc",
args.startCommand,
);
return dockerArgs;
}

function appendCommonDockerArgs(
dockerArgs: string[],
args: CustomDashboardValidationDockerBaseArgs,
): void {
if (args.userSpec) {
dockerArgs.push("--user", args.userSpec);
}

if (args.setupScriptSource && args.shouldRunSetupScriptAtRuntime) {
dockerArgs.push("--mount", toDockerMountArg({
source: args.setupScriptSource,
destination: CONTAINER_SETUP_SCRIPT,
readonly: true,
}));
}
}
Loading