Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3233f06
feat(task T01): implement via codex
Jul 13, 2026
813adcd
feat(task T04): implement via codex
Jul 13, 2026
6c77cd2
fix(task T01): address qa review via codex
Jul 13, 2026
014aa13
feat(task T02): implement via codex
Jul 13, 2026
d25c5c0
Merge pull request #3049 from codeux-ai/task/feature-codux-22-t04-cod…
numnx Jul 13, 2026
ed6897c
fix(task T02): address qa review via codex
Jul 13, 2026
94a1da6
Merge pull request #3048 from codeux-ai/task/feature-codux-22-t01-cod…
numnx Jul 13, 2026
dd5820e
fix(merge): resolve feature/CODUX-223-complete-automation-credential-…
Jul 13, 2026
83ca336
Merge pull request #3051 from codeux-ai/task/feature-codux-22-t02-cod…
numnx Jul 13, 2026
39dfa1e
feat(task T03): implement via codex
Jul 13, 2026
cb37472
fix(task T03): address qa review via codex
Jul 13, 2026
1073eb8
feat(task T05): implement via codex
Jul 13, 2026
d5f20f6
Merge pull request #3056 from codeux-ai/task/feature-codux-22-t03-cod…
numnx Jul 13, 2026
92976ed
feat(task T06): implement via codex
Jul 13, 2026
9731123
fix(task T05): address qa review via codex
Jul 13, 2026
1d841a5
fix(task T06): address qa review via codex
Jul 13, 2026
69ad168
fix(task T05): address qa review via codex
Jul 13, 2026
57c25a7
feat(task T07): implement via codex
Jul 13, 2026
0f20530
fix(task T05): address qa review via codex
Jul 13, 2026
60bbd2b
Merge pull request #3060 from codeux-ai/task/feature-codux-22-t06-cod…
numnx Jul 13, 2026
d2eb8c2
Merge pull request #3064 from codeux-ai/task/feature-codux-22-t07-cod…
numnx Jul 13, 2026
353ed50
Merge pull request #3058 from codeux-ai/task/feature-codux-22-t05-cod…
numnx Jul 13, 2026
5cb888f
feat(task T08): implement via codex
Jul 13, 2026
49c4e11
Merge pull request #3073 from codeux-ai/task/feature-codux-22-t08-cod…
numnx Jul 13, 2026
fe86a46
feat(task T09): implement via codex
Jul 13, 2026
06f4cf9
fix(task T09): address qa review via codex
Jul 13, 2026
c437321
Merge pull request #3074 from codeux-ai/task/feature-codux-22-t09-cod…
numnx Jul 13, 2026
67f9131
feat(task T10): implement via codex
Jul 14, 2026
34cc800
fix(task T10): address qa review via codex
Jul 14, 2026
55bf8df
fix(task T10): address qa review via codex
Jul 14, 2026
968d55a
fix(task T10): address qa review via codex
Jul 14, 2026
0554a0f
fix(task T10): address qa review via codex
Jul 14, 2026
963d029
fix(merge): resolve feature/CODUX-223-complete-automation-credential-…
Jul 14, 2026
6aa1fb9
Merge pull request #3077 from codeux-ai/task/feature-codux-22-t10-cod…
numnx Jul 14, 2026
3374621
fix(merge): resolve dev into feature/CODUX-223-complete-automation-cr…
Jul 14, 2026
f9da297
fix(merge): resolve dev into feature/CODUX-223-complete-automation-cr…
Jul 14, 2026
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
JULES_API_KEY=<YOUR_JULES_API_KEY>
DASHBOARD_PORT=4444

# The trusted loopback dashboard automatically provisions owner-only root-key custody under ~/.code-ux.
# Headless, server, authenticated, and remote-management deployments must select an external provider.
# Supported explicit values: mounted-key-file, vault, or kms. Never put key material in this file.
# CODE_UX_CREDENTIAL_KEY_PROVIDER=mounted-key-file
# CODE_UX_CREDENTIAL_KEY_FILE=/run/secrets/code-ux-credential-root-key

# Unfinished dashboard surfaces are visible by default in dev/test and hidden by default in production builds.
# Set to true in a production build only when the surface is ready to expose.
# VITE_CODEUX_FEATURE_NODES=true
Expand Down
202 changes: 201 additions & 1 deletion dashboard/src/v2/CustomDashboardsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FunctionComponent } from "preact";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { AlertTriangle, ExternalLink, LayoutDashboard, RefreshCw, Save } from "lucide-preact";
import { PageContainer } from "./components/layout/PageContainer.js";
import { PageHeader } from "./components/layout/PageHeader.js";
Expand All @@ -12,18 +12,27 @@ import { useActionFeedback } from "./hooks/use-action-feedback.js";
import { useProjectData } from "./context/project-data.js";
import {
archiveCustomDashboard,
bindCustomDashboardCredential,
createCustomDashboard,
createCustomDashboardRevision,
fetchCustomDashboard,
fetchCustomDashboardCredentialBindings,
fetchCustomDashboardDataCatalog,
fetchCustomDashboardValidationLogs,
fetchCustomDashboardValidationSession,
fetchCustomDashboards,
publishCustomDashboardRevision,
startCustomDashboardValidation,
unbindCustomDashboardCredential,
updateCustomDashboardDraft,
CustomDashboardCredentialBindingApiError,
type CustomDashboardCredentialBindingReview,
type CustomDashboardDataCatalogResponse,
} from "./lib/custom-dashboard-api.js";
import {
fetchAutomationCredentials,
fetchCredentialHealth,
} from "./lib/automation-credential-api.js";
import {
createDefaultCustomDashboardDraft,
hasDraftChanged,
Expand All @@ -38,7 +47,12 @@ import {
type CustomDashboardEditorTab,
} from "./components/custom-dashboards/CustomDashboardEditorPanel.js";
import { CustomDashboardValidationPanel } from "./components/custom-dashboards/CustomDashboardValidationPanel.js";
import { CustomDashboardCredentialSlotsPanel } from "./components/custom-dashboards/CustomDashboardCredentialSlotsPanel.js";
import { CustomDashboardViewer } from "./components/custom-dashboards/CustomDashboardViewer.js";
import type {
AutomationCredentialMetadata,
CredentialBackendHealth,
} from "../../../src/contracts/automation-credential-types.js";
import type {
CreateCustomDashboardRevisionInput,
CustomDashboardDataSourceNodeGraph,
Expand Down Expand Up @@ -88,6 +102,14 @@ export const CustomDashboardsPage: FunctionComponent = () => {
const [revisions, setRevisions] = useState<CustomDashboardRevisionRecord[]>([]);
const [selectedRevisionId, setSelectedRevisionId] = useState<string | null>(null);
const [catalog, setCatalog] = useState<CustomDashboardDataCatalogResponse | null>(null);
const [credentialReview, setCredentialReview] = useState<CustomDashboardCredentialBindingReview | null>(null);
const [credentialMetadata, setCredentialMetadata] = useState<AutomationCredentialMetadata[]>([]);
const [credentialHealth, setCredentialHealth] = useState<CredentialBackendHealth | null>(null);
const [credentialLoading, setCredentialLoading] = useState(false);
const [credentialLoadError, setCredentialLoadError] = useState<string | null>(null);
const [savingCredentialSlotId, setSavingCredentialSlotId] = useState<string | null>(null);
const [credentialSlotErrors, setCredentialSlotErrors] = useState<Record<string, string>>({});
const [credentialSlotAnnouncements, setCredentialSlotAnnouncements] = useState<Record<string, string>>({});
const [draft, setDraft] = useState<CustomDashboardDraftState | null>(null);
const [activeTab, setActiveTab] = useState<CustomDashboardEditorTab>("manifest");
const [selectedFilePath, setSelectedFilePath] = useState("src/dashboard.tsx");
Expand All @@ -109,12 +131,54 @@ export const CustomDashboardsPage: FunctionComponent = () => {
clearError,
} = useActionFeedback();
const archiveConfirm = useConfirmDialog();
const bindingActionControllerRef = useRef<AbortController | null>(null);
const credentialLoadControllerRef = useRef<AbortController | null>(null);

const selectedRevision = useMemo(
() => revisions.find((revision) => revision.id === selectedRevisionId) ?? null,
[revisions, selectedRevisionId],
);
const dirty = useMemo(() => draft ? hasDraftChanged(selectedDashboard, draft) : false, [draft, selectedDashboard]);
const hasCredentialSlots = Boolean(selectedDashboard?.manifest.credentialSlots?.length);

const adoptCredentialReview = useCallback((review: CustomDashboardCredentialBindingReview): void => {
setCredentialReview(review);
setSelectedDashboard((current) => current?.id === review.dashboardId
? {
...current,
credentialBindings: review.slots.flatMap((slotReview) => slotReview.binding ? [slotReview.binding] : []),
credentialBindingRevision: review.credentialBindingRevision ?? current.credentialBindingRevision,
}
: current);
}, []);

const loadCredentialState = useCallback(async (
nextProjectId: string,
dashboardId: string,
signal?: AbortSignal,
): Promise<CustomDashboardCredentialBindingReview | null> => {
setCredentialLoading(true);
setCredentialLoadError(null);
try {
const [review, credentials, health] = await Promise.all([
fetchCustomDashboardCredentialBindings(nextProjectId, dashboardId, signal),
fetchAutomationCredentials(nextProjectId, signal),
fetchCredentialHealth(signal),
]);
if (signal?.aborted) return null;
setCredentialMetadata(credentials);
setCredentialHealth(health);
adoptCredentialReview(review);
return review;
} catch (error) {
if (!signal?.aborted) {
setCredentialLoadError(error instanceof Error ? error.message : "Credential metadata could not be loaded.");
}
return null;
} finally {
if (!signal?.aborted) setCredentialLoading(false);
}
}, [adoptCredentialReview]);

const loadProjectDashboards = useCallback(async (nextProjectId: string, signal?: AbortSignal): Promise<void> => {
setLoading(true);
Expand Down Expand Up @@ -196,13 +260,132 @@ export const CustomDashboardsPage: FunctionComponent = () => {
return () => controller.abort();
}, [loadDashboardDetail, selectedDashboardId]);

useEffect(() => {
bindingActionControllerRef.current?.abort();
credentialLoadControllerRef.current?.abort();
setSavingCredentialSlotId(null);
setCredentialSlotErrors({});
setCredentialSlotAnnouncements({});
if (!projectId || !selectedDashboardId || !hasCredentialSlots) {
setCredentialReview(null);
setCredentialMetadata([]);
setCredentialHealth(null);
setCredentialLoadError(null);
setCredentialLoading(false);
return;
}
const controller = new AbortController();
credentialLoadControllerRef.current = controller;
void loadCredentialState(projectId, selectedDashboardId, controller.signal).finally(() => {
if (credentialLoadControllerRef.current === controller) credentialLoadControllerRef.current = null;
});
return () => {
controller.abort();
if (credentialLoadControllerRef.current === controller) credentialLoadControllerRef.current = null;
};
}, [hasCredentialSlots, loadCredentialState, projectId, selectedDashboardId]);

const refreshSelectedDashboard = useCallback(async (): Promise<void> => {
if (!selectedDashboardId) {
return;
}
await loadDashboardDetail(selectedDashboardId);
}, [loadDashboardDetail, selectedDashboardId]);

const refreshCredentialState = useCallback((): void => {
if (!projectId || !selectedDashboardId || !hasCredentialSlots) return;
credentialLoadControllerRef.current?.abort();
const controller = new AbortController();
credentialLoadControllerRef.current = controller;
void loadCredentialState(projectId, selectedDashboardId, controller.signal).finally(() => {
if (credentialLoadControllerRef.current === controller) credentialLoadControllerRef.current = null;
});
}, [hasCredentialSlots, loadCredentialState, projectId, selectedDashboardId]);

const completeCredentialMutation = useCallback(async (
slotId: string,
announcement: string,
operation: (expectedBindingRevision: number, signal: AbortSignal) => Promise<CustomDashboardCredentialBindingReview>,
): Promise<void> => {
if (!projectId || !selectedDashboardId || credentialReview?.credentialBindingRevision === null || credentialReview?.credentialBindingRevision === undefined) {
setCredentialSlotErrors((current) => ({ ...current, [slotId]: "Refresh credential metadata before changing this binding." }));
return;
}
bindingActionControllerRef.current?.abort();
credentialLoadControllerRef.current?.abort();
credentialLoadControllerRef.current = null;
const controller = new AbortController();
bindingActionControllerRef.current = controller;
const mutationProjectId = projectId;
const mutationDashboardId = selectedDashboardId;
setSavingCredentialSlotId(slotId);
setCredentialSlotErrors((current) => ({ ...current, [slotId]: "" }));
setCredentialSlotAnnouncements((current) => ({ ...current, [slotId]: "" }));
try {
const review = await operation(credentialReview.credentialBindingRevision, controller.signal);
if (controller.signal.aborted) return;
adoptCredentialReview(review);
setCredentialSlotAnnouncements((current) => ({ ...current, [slotId]: announcement }));
setValidationSession(null);
setLogs("");
await loadDashboardDetail(mutationDashboardId, controller.signal);
if (!controller.signal.aborted) {
await loadCredentialState(mutationProjectId, mutationDashboardId, controller.signal);
}
} catch (error) {
if (controller.signal.aborted) return;
if (error instanceof CustomDashboardCredentialBindingApiError && error.status === 409) {
await loadDashboardDetail(mutationDashboardId, controller.signal);
if (!controller.signal.aborted) {
await loadCredentialState(mutationProjectId, mutationDashboardId, controller.signal);
setCredentialSlotErrors((current) => ({
...current,
[slotId]: "Bindings changed in another session. The dashboard was refreshed; review the current binding and explicitly retry your change.",
}));
}
} else {
const policyMessage = error instanceof CustomDashboardCredentialBindingApiError && error.issues.length > 0
? error.issues.map((issue) => issue.message).join(" ")
: error instanceof Error
? error.message
: "The credential binding could not be changed.";
setCredentialSlotErrors((current) => ({ ...current, [slotId]: policyMessage }));
}
} finally {
if (bindingActionControllerRef.current === controller) {
bindingActionControllerRef.current = null;
setSavingCredentialSlotId(null);
}
}
}, [adoptCredentialReview, credentialReview?.credentialBindingRevision, loadCredentialState, loadDashboardDetail, projectId, selectedDashboardId]);

const handleBindCredential = useCallback(async (slotId: string, credentialId: string): Promise<void> => {
await completeCredentialMutation(
slotId,
"Credential binding saved. Validation and publication readiness were refreshed.",
(expectedBindingRevision, signal) => bindCustomDashboardCredential(
projectId ?? "",
selectedDashboardId ?? "",
{ slotId, credentialId, expectedBindingRevision },
signal,
),
);
}, [completeCredentialMutation, projectId, selectedDashboardId]);

const handleUnbindCredential = useCallback(async (slotId: string): Promise<void> => {
await completeCredentialMutation(
slotId,
"Credential unbound. Validation and publication readiness were refreshed.",
(expectedBindingRevision, signal) => unbindCustomDashboardCredential(
projectId ?? "",
selectedDashboardId ?? "",
slotId,
expectedBindingRevision,
signal,
),
);
}, [completeCredentialMutation, projectId, selectedDashboardId]);

const buildDraftInput = useCallback((): UpdateCustomDashboardDraftInput & CreateCustomDashboardRevisionInput => {
if (!draft) {
throw new Error("No dashboard draft is selected.");
Expand Down Expand Up @@ -520,6 +703,23 @@ export const CustomDashboardsPage: FunctionComponent = () => {
selectedFilePath={selectedFilePath}
onSelectedFilePathChange={setSelectedFilePath}
catalog={catalog}
credentialPanel={hasCredentialSlots ? (
<CustomDashboardCredentialSlotsPanel
projectId={projectId}
dashboardId={selectedDashboard.id}
review={credentialReview}
credentials={credentialMetadata}
health={credentialHealth}
loading={credentialLoading}
loadError={credentialLoadError}
savingSlotId={savingCredentialSlotId}
slotErrors={credentialSlotErrors}
slotAnnouncements={credentialSlotAnnouncements}
onBind={handleBindCredential}
onUnbind={handleUnbindCredential}
onRefresh={refreshCredentialState}
/>
) : undefined}
/>
<CustomDashboardValidationPanel
dashboard={selectedDashboard}
Expand Down
Loading
Loading