web: retire profile-specific editor
This commit is contained in:
@@ -6,10 +6,6 @@ import {
|
|||||||
SETTINGS_SECTIONS,
|
SETTINGS_SECTIONS,
|
||||||
settingsSectionHref,
|
settingsSectionHref,
|
||||||
} from "./model.ts";
|
} from "./model.ts";
|
||||||
import {
|
|
||||||
profileSourceTreeSettingsHref,
|
|
||||||
virtualProfilePathForCreate,
|
|
||||||
} from "./profile-routes.ts";
|
|
||||||
|
|
||||||
declare const Deno: {
|
declare const Deno: {
|
||||||
test(name: string, fn: () => void): void;
|
test(name: string, fn: () => void): void;
|
||||||
@@ -106,29 +102,3 @@ Deno.test("diagnostic labels preserve severity and code", () => {
|
|||||||
"diagnostic label should be bounded and stable",
|
"diagnostic label should be bounded and stable",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("profile source tree routes use encoded scoped ids", () => {
|
|
||||||
const href = profileSourceTreeSettingsHref("workspace a", "project/tree");
|
|
||||||
assert(
|
|
||||||
href === "/w/workspace%20a/settings/profiles/trees/project%2Ftree",
|
|
||||||
`unexpected href ${href}`,
|
|
||||||
);
|
|
||||||
assert(!href.includes("/home/"), "route must not contain host paths");
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("profile source create paths are normalized to virtual profile paths", () => {
|
|
||||||
assert(
|
|
||||||
virtualProfilePathForCreate("alpha.dcdl") === "profiles/alpha.dcdl",
|
|
||||||
"bare file names are scoped",
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
virtualProfilePathForCreate("profiles/alpha.dcdl") ===
|
|
||||||
"profiles/alpha.dcdl",
|
|
||||||
"virtual paths are preserved",
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
virtualProfilePathForCreate("project:profiles/alpha.dcdl") ===
|
|
||||||
"project:profiles/alpha.dcdl",
|
|
||||||
"safe virtual namespaces are preserved",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
|||||||
bullets: [
|
bullets: [
|
||||||
"Virtual paths and imports resolve inside the committed Workspace tree, never from browser or Server host paths.",
|
"Virtual paths and imports resolve inside the committed Workspace tree, never from browser or Server host paths.",
|
||||||
"Browser analysis is advisory; Server evaluation is required before an atomic revision commit.",
|
"Browser analysis is advisory; Server evaluation is required before an atomic revision commit.",
|
||||||
"Profile, Skill, Prompt, and Plugin consumers remain on their existing authorities until their follow-up cutovers.",
|
"Profile launch data is projected from this active revision; remaining Skill, Prompt, and Plugin consumers migrate in their follow-up cutovers.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,11 +116,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
|||||||
label: "Profile Sources",
|
label: "Profile Sources",
|
||||||
status: "editable",
|
status: "editable",
|
||||||
summary:
|
summary:
|
||||||
"Manage the workspace-scoped Decodal Profile registry and source files used by Backend-published launch profile discovery.",
|
"Inspect the Profile launch projection derived from the active Workspace configuration revision.",
|
||||||
bullets: [
|
bullets: [
|
||||||
"Selectors are source-qualified (builtin:* or project:*); raw profile source paths, archive content, archive digests, resource handles, and runtime tokens are not exposed.",
|
"Selectors are source-qualified (builtin:* or project:*); raw profile source paths, archive content, archive digests, resource handles, and runtime tokens are not exposed.",
|
||||||
"Profile source edits are validated through the Backend ProfileSourceArchive/Decodal boundary before they are persisted.",
|
"Profile declarations and sources are edited only through the shared Workspace configuration editor and evaluate-before-commit contract.",
|
||||||
"Launch profile candidates refresh from the same Backend projection after registry or source updates.",
|
"Launch candidates and Profile archives carry the same active config revision, tree digest, and projection digest.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,166 +1,72 @@
|
|||||||
import { workspaceApiJson, workspaceApiJsonWithBody } from "../api/http";
|
|
||||||
import type {
|
import type {
|
||||||
ProfileSettingsMutationResponse,
|
|
||||||
ProfileSettingsResponse,
|
ProfileSettingsResponse,
|
||||||
WorkspaceMetadataMutationResponse,
|
WorkspaceMetadataMutationResponse,
|
||||||
WorkspaceMetadataSettingsResponse,
|
WorkspaceMetadataSettingsResponse,
|
||||||
WorkspaceProfileSourceDetailResponse,
|
|
||||||
WorkspaceProfileSourceTreeFileResponse,
|
|
||||||
WorkspaceProfileSourceTreeResponse,
|
|
||||||
} from "./profile-types";
|
} from "./profile-types";
|
||||||
|
|
||||||
export function fetchWorkspaceMetadataSettings(
|
export type WorkspaceProfileApi = {
|
||||||
|
getMetadata(workspaceId: string): Promise<WorkspaceMetadataSettingsResponse>;
|
||||||
|
updateMetadata(
|
||||||
|
workspaceId: string,
|
||||||
|
displayName: string,
|
||||||
|
expectedRevision: string,
|
||||||
|
): Promise<WorkspaceMetadataMutationResponse>;
|
||||||
|
getProfiles(workspaceId: string): Promise<ProfileSettingsResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function requestJson<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(input, init);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchWorkspaceMetadataSettings(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<WorkspaceMetadataSettingsResponse> {
|
): Promise<WorkspaceMetadataSettingsResponse> {
|
||||||
return workspaceApiJson(
|
return await requestJson<WorkspaceMetadataSettingsResponse>(
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`,
|
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateWorkspaceMetadataSettings(
|
export async function updateWorkspaceMetadataSettings(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
request: { display_name: string; revision: string },
|
request: { display_name: string; revision: string },
|
||||||
): Promise<WorkspaceMetadataMutationResponse> {
|
): Promise<WorkspaceMetadataMutationResponse> {
|
||||||
return workspaceApiJsonWithBody(
|
return await requestJson<WorkspaceMetadataMutationResponse>(
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`,
|
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchProfileSettings(
|
export async function fetchProfileSettings(workspaceId: string): Promise<ProfileSettingsResponse> {
|
||||||
workspaceId: string,
|
return await requestJson<ProfileSettingsResponse>(
|
||||||
): Promise<ProfileSettingsResponse> {
|
|
||||||
return workspaceApiJson(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
|
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProfileSource(
|
export function createWorkspaceProfileApi(): WorkspaceProfileApi {
|
||||||
workspaceId: string,
|
return {
|
||||||
request: {
|
async getMetadata(workspaceId) {
|
||||||
name: string;
|
return await requestJson<WorkspaceMetadataSettingsResponse>(
|
||||||
description?: string;
|
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
|
||||||
content: string;
|
);
|
||||||
registry_revision: string;
|
|
||||||
},
|
|
||||||
): Promise<ProfileSettingsMutationResponse> {
|
|
||||||
return workspaceApiJsonWithBody(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
},
|
},
|
||||||
);
|
async updateMetadata(workspaceId, displayName, expectedRevision) {
|
||||||
}
|
return await requestJson<WorkspaceMetadataMutationResponse>(
|
||||||
|
`/api/w/${encodeURIComponent(workspaceId)}/settings/metadata`,
|
||||||
export function updateProfileRegistry(
|
{
|
||||||
workspaceId: string,
|
method: "PUT",
|
||||||
request: {
|
headers: { "content-type": "application/json" },
|
||||||
registry_revision: string;
|
body: JSON.stringify({ display_name: displayName, expected_revision: expectedRevision }),
|
||||||
default_profile?: string | null;
|
},
|
||||||
profiles: Array<
|
);
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
description?: string | null;
|
|
||||||
profile_source_id?: string | null;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
},
|
|
||||||
): Promise<ProfileSettingsMutationResponse> {
|
|
||||||
return workspaceApiJsonWithBody(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/registry`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
},
|
},
|
||||||
);
|
getProfiles: fetchProfileSettings,
|
||||||
}
|
};
|
||||||
|
|
||||||
export function fetchProfileSource(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceId: string,
|
|
||||||
): Promise<WorkspaceProfileSourceDetailResponse> {
|
|
||||||
return workspaceApiJson(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/${
|
|
||||||
encodeURIComponent(sourceId)
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateProfileSource(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceId: string,
|
|
||||||
request: { content: string; revision: string },
|
|
||||||
): Promise<ProfileSettingsMutationResponse> {
|
|
||||||
return workspaceApiJsonWithBody(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/${
|
|
||||||
encodeURIComponent(sourceId)
|
|
||||||
}`,
|
|
||||||
{ method: "PUT", body: JSON.stringify(request) },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteProfileSource(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceId: string,
|
|
||||||
request: { registry_revision: string; source_revision: string },
|
|
||||||
): Promise<ProfileSettingsMutationResponse> {
|
|
||||||
return workspaceApiJsonWithBody(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/${
|
|
||||||
encodeURIComponent(sourceId)
|
|
||||||
}`,
|
|
||||||
{ method: "DELETE", body: JSON.stringify(request) },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchProfileSourceTree(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceTreeId: string,
|
|
||||||
): Promise<WorkspaceProfileSourceTreeResponse> {
|
|
||||||
return workspaceApiJson(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
|
||||||
encodeURIComponent(sourceTreeId)
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchProfileTreeFile(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceTreeId: string,
|
|
||||||
path: string,
|
|
||||||
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
|
||||||
return workspaceApiJson(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
|
||||||
encodeURIComponent(sourceTreeId)
|
|
||||||
}/file?path=${encodeURIComponent(path)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeProfileTreeFile(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceTreeId: string,
|
|
||||||
request: { path: string; content: string; revision?: string | null },
|
|
||||||
): Promise<WorkspaceProfileSourceTreeFileResponse> {
|
|
||||||
return workspaceApiJsonWithBody(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
|
||||||
encodeURIComponent(sourceTreeId)
|
|
||||||
}/file`,
|
|
||||||
{ method: "PUT", body: JSON.stringify(request) },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteProfileTreeFile(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceTreeId: string,
|
|
||||||
request: { path: string; revision: string },
|
|
||||||
): Promise<WorkspaceProfileSourceTreeResponse> {
|
|
||||||
return workspaceApiJsonWithBody(
|
|
||||||
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles/trees/${
|
|
||||||
encodeURIComponent(sourceTreeId)
|
|
||||||
}/file`,
|
|
||||||
{ method: "DELETE", body: JSON.stringify(request) },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
export function profileSettingsHref(workspaceId: string): string {
|
|
||||||
return `/w/${encodeURIComponent(workspaceId)}/settings/profiles`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function profileSourceTreeSettingsHref(
|
|
||||||
workspaceId: string,
|
|
||||||
sourceTreeId: string,
|
|
||||||
): string {
|
|
||||||
return `${profileSettingsHref(workspaceId)}/trees/${
|
|
||||||
encodeURIComponent(sourceTreeId)
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function virtualProfilePathForCreate(input: string): string {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
if (!trimmed) return "";
|
|
||||||
if (trimmed.startsWith("project:") || trimmed.startsWith("workspace:")) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
if (trimmed.startsWith("profiles/")) return trimmed;
|
|
||||||
return `profiles/${trimmed}`;
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ export type WorkspaceProfileSummary = {
|
|||||||
export type WorkspaceProfileSourceSummary = {
|
export type WorkspaceProfileSourceSummary = {
|
||||||
profile_source_id: string;
|
profile_source_id: string;
|
||||||
display_path: string;
|
display_path: string;
|
||||||
kind: "decodal" | string;
|
kind: "virtual_config" | string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
content_digest: string;
|
content_digest: string;
|
||||||
provenance: "project_profile_source_tree" | string;
|
provenance: "project_profile_source_tree" | string;
|
||||||
@@ -42,64 +42,11 @@ export type WorkspaceProfileSourceSummary = {
|
|||||||
export type ProfileSettingsResponse = {
|
export type ProfileSettingsResponse = {
|
||||||
workspace_id: string;
|
workspace_id: string;
|
||||||
registry_revision: string;
|
registry_revision: string;
|
||||||
|
config_revision?: number | null;
|
||||||
|
tree_digest?: string | null;
|
||||||
|
projection_digest?: string | null;
|
||||||
default_profile?: string | null;
|
default_profile?: string | null;
|
||||||
profiles: WorkspaceProfileSummary[];
|
profiles: WorkspaceProfileSummary[];
|
||||||
sources: WorkspaceProfileSourceSummary[];
|
sources: WorkspaceProfileSourceSummary[];
|
||||||
source_trees: WorkspaceProfileSourceTreeSummary[];
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceProfileSourceDetailResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
profile: WorkspaceProfileSummary;
|
|
||||||
source: WorkspaceProfileSourceSummary;
|
|
||||||
content: string;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProfileSettingsMutationResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
settings: ProfileSettingsResponse;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceProfileSourceTreeSummary = {
|
|
||||||
source_tree_id: string;
|
|
||||||
label: string;
|
|
||||||
root_path: string;
|
|
||||||
kind: "decodal_source_tree" | string;
|
|
||||||
content_type: string;
|
|
||||||
content_digest: string;
|
|
||||||
provenance: "project_profile_source_tree" | string;
|
|
||||||
editable: boolean;
|
|
||||||
revision: string;
|
|
||||||
file_count: number;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceProfileSourceTreeFileSummary = {
|
|
||||||
path: string;
|
|
||||||
kind: "decodal" | string;
|
|
||||||
content_type: string;
|
|
||||||
content_digest: string;
|
|
||||||
provenance: "project_profile_source_tree" | string;
|
|
||||||
editable: boolean;
|
|
||||||
revision: string;
|
|
||||||
size_bytes: number;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceProfileSourceTreeResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
tree: WorkspaceProfileSourceTreeSummary;
|
|
||||||
files: WorkspaceProfileSourceTreeFileSummary[];
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceProfileSourceTreeFileResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
source_tree_id: string;
|
|
||||||
file: WorkspaceProfileSourceTreeFileSummary;
|
|
||||||
content: string;
|
|
||||||
diagnostics: Diagnostic[];
|
diagnostics: Diagnostic[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,130 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
|
import DiagnosticsList from "$lib/workspace/settings/DiagnosticsList.svelte";
|
||||||
import DecodalSourceEditor from '$lib/workspace/settings/DecodalSourceEditor.svelte';
|
import { fetchProfileSettings } from "$lib/workspace/settings/profile-api";
|
||||||
import {
|
import type { ProfileSettingsResponse } from "$lib/workspace/settings/profile-types";
|
||||||
fetchProfileSettings,
|
import type { PageProps } from "./$types";
|
||||||
fetchProfileSourceTree,
|
|
||||||
deleteProfileTreeFile,
|
|
||||||
fetchProfileTreeFile,
|
|
||||||
writeProfileTreeFile,
|
|
||||||
} from '$lib/workspace/settings/profile-api';
|
|
||||||
import type { Diagnostic } from '$lib/workspace/settings/model';
|
|
||||||
import { profileSourceTreeSettingsHref, virtualProfilePathForCreate } from '$lib/workspace/settings/profile-routes';
|
|
||||||
import type {
|
|
||||||
ProfileSettingsResponse,
|
|
||||||
WorkspaceProfileSourceTreeFileResponse,
|
|
||||||
WorkspaceProfileSourceTreeResponse,
|
|
||||||
} from '$lib/workspace/settings/profile-types';
|
|
||||||
import type { PageProps } from './$types';
|
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
let workspaceId = $derived(data.workspace?.workspace_id ?? '');
|
let workspaceId = $derived(data.workspace?.workspace_id ?? "");
|
||||||
|
|
||||||
let profileSettings = $state<ProfileSettingsResponse | null>(null);
|
let profileSettings = $state<ProfileSettingsResponse | null>(null);
|
||||||
let sourceTree = $state<WorkspaceProfileSourceTreeResponse | null>(null);
|
|
||||||
let selectedFile = $state<WorkspaceProfileSourceTreeFileResponse | null>(null);
|
|
||||||
let draftContent = $state('');
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let saving = $state(false);
|
|
||||||
let creating = $state(false);
|
|
||||||
let deleting = $state(false);
|
|
||||||
let newFilePath = $state('new-profile.dcdl');
|
|
||||||
let message = $state<string | null>(null);
|
let message = $state<string | null>(null);
|
||||||
let diagnostics = $state<Diagnostic[]>([]);
|
|
||||||
|
|
||||||
async function loadSettings() {
|
|
||||||
if (!workspaceId) return;
|
|
||||||
loading = true;
|
|
||||||
message = null;
|
|
||||||
try {
|
|
||||||
const response = await fetchProfileSettings(workspaceId);
|
|
||||||
profileSettings = response;
|
|
||||||
diagnostics = response.diagnostics;
|
|
||||||
const treeId = response.source_trees[0]?.source_tree_id;
|
|
||||||
if (treeId) {
|
|
||||||
sourceTree = await fetchProfileSourceTree(workspaceId, treeId);
|
|
||||||
const firstPath = sourceTree.files[0]?.path;
|
|
||||||
if (firstPath) await selectTreeFile(treeId, firstPath);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
message = err instanceof Error ? err.message : 'profile settings request failed';
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectTreeFile(sourceTreeId: string, path: string) {
|
|
||||||
selectedFile = await fetchProfileTreeFile(workspaceId, sourceTreeId, path);
|
|
||||||
draftContent = selectedFile.content;
|
|
||||||
diagnostics = selectedFile.diagnostics;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createTreeFile(sourceTreeId: string) {
|
|
||||||
creating = true;
|
|
||||||
message = null;
|
|
||||||
try {
|
|
||||||
const path = virtualProfilePathForCreate(newFilePath);
|
|
||||||
selectedFile = await writeProfileTreeFile(workspaceId, sourceTreeId, {
|
|
||||||
path,
|
|
||||||
content: '{\n slug = "new-profile";\n description = "New profile";\n scope = "workspace_read";\n}',
|
|
||||||
});
|
|
||||||
draftContent = selectedFile.content;
|
|
||||||
sourceTree = await fetchProfileSourceTree(workspaceId, sourceTreeId);
|
|
||||||
diagnostics = selectedFile.diagnostics;
|
|
||||||
message = 'Created Decodal profile source.';
|
|
||||||
} catch (err) {
|
|
||||||
message = err instanceof Error ? err.message : 'profile source create failed';
|
|
||||||
} finally {
|
|
||||||
creating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteSelectedFile() {
|
|
||||||
if (!selectedFile) return;
|
|
||||||
deleting = true;
|
|
||||||
message = null;
|
|
||||||
try {
|
|
||||||
sourceTree = await deleteProfileTreeFile(workspaceId, selectedFile.source_tree_id, {
|
|
||||||
path: selectedFile.file.path,
|
|
||||||
revision: selectedFile.file.revision,
|
|
||||||
});
|
|
||||||
selectedFile = null;
|
|
||||||
draftContent = '';
|
|
||||||
diagnostics = sourceTree.diagnostics;
|
|
||||||
message = 'Deleted Decodal profile source.';
|
|
||||||
} catch (err) {
|
|
||||||
message = err instanceof Error ? err.message : 'profile source delete failed';
|
|
||||||
} finally {
|
|
||||||
deleting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSelectedFile() {
|
|
||||||
if (!selectedFile) return;
|
|
||||||
saving = true;
|
|
||||||
message = null;
|
|
||||||
try {
|
|
||||||
selectedFile = await writeProfileTreeFile(workspaceId, selectedFile.source_tree_id, {
|
|
||||||
path: selectedFile.file.path,
|
|
||||||
revision: selectedFile.file.revision,
|
|
||||||
content: draftContent,
|
|
||||||
});
|
|
||||||
draftContent = selectedFile.content;
|
|
||||||
sourceTree = await fetchProfileSourceTree(workspaceId, selectedFile.source_tree_id);
|
|
||||||
diagnostics = selectedFile.diagnostics;
|
|
||||||
message = 'Saved Decodal profile source.';
|
|
||||||
} catch (err) {
|
|
||||||
message = err instanceof Error ? err.message : 'profile source save failed';
|
|
||||||
} finally {
|
|
||||||
saving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
async function load() {
|
async function load() {
|
||||||
await loadSettings();
|
loading = true;
|
||||||
if (cancelled) return;
|
message = null;
|
||||||
|
try {
|
||||||
|
const response = await fetchProfileSettings(workspaceId);
|
||||||
|
if (!cancelled) profileSettings = response;
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
message = error instanceof Error ? error.message : "profile settings request failed";
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (workspaceId) load();
|
if (workspaceId) load();
|
||||||
else loading = false;
|
else loading = false;
|
||||||
@@ -138,90 +38,39 @@
|
|||||||
<title>Profiles · Yoi Workspace</title>
|
<title>Profiles · Yoi Workspace</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="card settings-section" aria-labelledby="profile-sources-title">
|
<section class="card settings-section" aria-labelledby="profiles-title">
|
||||||
<header class="settings-section-header">
|
<header class="settings-section-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Backend-owned source tree</p>
|
<p class="eyebrow">Workspace configuration projection</p>
|
||||||
<h2 id="profile-sources-title">Profiles</h2>
|
<h2 id="profiles-title">Profiles</h2>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge">Decodal editor</span>
|
<a class="badge" href={`/w/${encodeURIComponent(workspaceId)}/settings/configuration-sources`}>
|
||||||
|
Edit configuration
|
||||||
|
</a>
|
||||||
</header>
|
</header>
|
||||||
<p>
|
<p>
|
||||||
Review effective launch profiles and edit Decodal source files through virtual profile-tree paths. The browser receives only safe relative paths and revision tokens.
|
These launch profiles are derived from the active Workspace configuration revision. Edit Profile declarations and Decodal sources in the shared configuration editor.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="status-message">Loading profiles…</p>
|
<p class="status-message">Loading profiles…</p>
|
||||||
|
{:else if message}
|
||||||
|
<p class="status-message error">{message}</p>
|
||||||
{:else if profileSettings}
|
{:else if profileSettings}
|
||||||
<div class="settings-profile-grid">
|
<p class="settings-note">
|
||||||
<article>
|
Revision {profileSettings.config_revision ?? "unknown"} · tree {profileSettings.tree_digest ?? "unknown"} · projection {profileSettings.projection_digest ?? "unknown"}
|
||||||
<h3>Available profiles</h3>
|
</p>
|
||||||
<p class="settings-note">Profiles that can be selected when launching a Worker.</p>
|
<ul class="settings-profile-list">
|
||||||
<ul class="settings-profile-list">
|
{#each profileSettings.profiles as profile (profile.profile_id)}
|
||||||
{#each profileSettings.profiles as profile (profile.profile_id)}
|
<li>
|
||||||
<li>
|
<strong>{profile.selector}</strong>
|
||||||
<strong>{profile.selector}</strong>
|
<span>{profile.label}</span>
|
||||||
<span>{profile.label}</span>
|
<small>{profile.source_kind}{profile.is_default ? " · default" : ""}</small>
|
||||||
<small>{profile.source_kind}{profile.is_default ? ' · default' : ''}</small>
|
{#if profile.description}<p>{profile.description}</p>{/if}
|
||||||
{#if profile.description}<p>{profile.description}</p>{/if}
|
<DiagnosticsList diagnostics={profile.diagnostics} />
|
||||||
<DiagnosticsList diagnostics={profile.diagnostics} />
|
</li>
|
||||||
</li>
|
{/each}
|
||||||
{/each}
|
</ul>
|
||||||
</ul>
|
<DiagnosticsList diagnostics={profileSettings.diagnostics} />
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h3>Profile source tree</h3>
|
|
||||||
<p class="settings-note">Virtual Decodal paths exposed by the Backend-owned source tree.</p>
|
|
||||||
{#if sourceTree}
|
|
||||||
<small>{sourceTree.tree.root_path} · {sourceTree.tree.file_count} files · {sourceTree.tree.content_type} · rev {sourceTree.tree.revision}</small>
|
|
||||||
<p><a href={profileSourceTreeSettingsHref(workspaceId, sourceTree.tree.source_tree_id)}>Open tree route</a></p>
|
|
||||||
<div class="settings-inline-form">
|
|
||||||
<input bind:value={newFilePath} aria-label="New profile source virtual path" placeholder="profiles/new-profile.dcdl" />
|
|
||||||
<button type="button" disabled={creating} onclick={() => createTreeFile(sourceTree!.tree.source_tree_id)}>
|
|
||||||
{creating ? 'Creating…' : 'Create source'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul class="settings-profile-list">
|
|
||||||
{#each sourceTree.files as file (file.path)}
|
|
||||||
<li>
|
|
||||||
<button class="link-button" type="button" onclick={() => selectTreeFile(sourceTree!.tree.source_tree_id, file.path)}>
|
|
||||||
<strong>{file.path}</strong>
|
|
||||||
</button>
|
|
||||||
<span>{file.kind} · {file.content_type} · {file.size_bytes} bytes</span>
|
|
||||||
<small>{file.editable ? 'editable' : 'read-only'} · rev {file.revision}</small>
|
|
||||||
<DiagnosticsList diagnostics={file.diagnostics} />
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{:else}
|
|
||||||
<p class="settings-note">No project profile source tree is available.</p>
|
|
||||||
{/if}
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if selectedFile}
|
|
||||||
<article class="settings-editor-panel">
|
|
||||||
<header class="settings-section-header">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">{selectedFile.source_tree_id}</p>
|
|
||||||
<h3>{selectedFile.file.path}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-editor-actions">
|
|
||||||
<button type="button" disabled={saving || draftContent === selectedFile.content} onclick={saveSelectedFile}>
|
|
||||||
{saving ? 'Saving…' : 'Save'}
|
|
||||||
</button>
|
|
||||||
<button type="button" disabled={deleting} onclick={deleteSelectedFile}>
|
|
||||||
{deleting ? 'Deleting…' : 'Delete'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<DecodalSourceEditor value={draftContent} onChange={(value) => (draftContent = value)} ariaLabel={`Decodal source ${selectedFile.file.path}`} />
|
|
||||||
</article>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message}
|
|
||||||
<p class="status-message" class:error={message.includes('failed') || message.includes('error')}>{message}</p>
|
|
||||||
{/if}
|
|
||||||
<DiagnosticsList {diagnostics} />
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
-173
@@ -1,173 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
|
|
||||||
import DecodalSourceEditor from '$lib/workspace/settings/DecodalSourceEditor.svelte';
|
|
||||||
import {
|
|
||||||
deleteProfileTreeFile,
|
|
||||||
fetchProfileSourceTree,
|
|
||||||
fetchProfileTreeFile,
|
|
||||||
writeProfileTreeFile,
|
|
||||||
} from '$lib/workspace/settings/profile-api';
|
|
||||||
import { virtualProfilePathForCreate } from '$lib/workspace/settings/profile-routes';
|
|
||||||
import type { Diagnostic } from '$lib/workspace/settings/model';
|
|
||||||
import type {
|
|
||||||
WorkspaceProfileSourceTreeFileResponse,
|
|
||||||
WorkspaceProfileSourceTreeResponse,
|
|
||||||
} from '$lib/workspace/settings/profile-types';
|
|
||||||
import type { PageProps } from './$types';
|
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
|
||||||
let workspaceId = $derived(data.workspace?.workspace_id ?? '');
|
|
||||||
let sourceTreeId = $derived(data.sourceTreeId);
|
|
||||||
|
|
||||||
let tree = $state<WorkspaceProfileSourceTreeResponse | null>(null);
|
|
||||||
let selectedFile = $state<WorkspaceProfileSourceTreeFileResponse | null>(null);
|
|
||||||
let draftContent = $state('');
|
|
||||||
let newFilePath = $state('new-profile.dcdl');
|
|
||||||
let loading = $state(true);
|
|
||||||
let saving = $state(false);
|
|
||||||
let creating = $state(false);
|
|
||||||
let deleting = $state(false);
|
|
||||||
let message = $state<string | null>(null);
|
|
||||||
let diagnostics = $state<Diagnostic[]>([]);
|
|
||||||
|
|
||||||
async function reloadTree() {
|
|
||||||
tree = await fetchProfileSourceTree(workspaceId, sourceTreeId);
|
|
||||||
diagnostics = tree.diagnostics;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectFile(path: string) {
|
|
||||||
selectedFile = await fetchProfileTreeFile(workspaceId, sourceTreeId, path);
|
|
||||||
draftContent = selectedFile.content;
|
|
||||||
diagnostics = selectedFile.diagnostics;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createFile() {
|
|
||||||
creating = true;
|
|
||||||
message = null;
|
|
||||||
try {
|
|
||||||
selectedFile = await writeProfileTreeFile(workspaceId, sourceTreeId, {
|
|
||||||
path: virtualProfilePathForCreate(newFilePath),
|
|
||||||
content: '{\n slug = "new-profile";\n description = "New profile";\n scope = "workspace_read";\n}',
|
|
||||||
});
|
|
||||||
draftContent = selectedFile.content;
|
|
||||||
await reloadTree();
|
|
||||||
message = 'Created Decodal profile source.';
|
|
||||||
} catch (err) {
|
|
||||||
message = err instanceof Error ? err.message : 'profile source create failed';
|
|
||||||
} finally {
|
|
||||||
creating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveFile() {
|
|
||||||
if (!selectedFile) return;
|
|
||||||
saving = true;
|
|
||||||
message = null;
|
|
||||||
try {
|
|
||||||
selectedFile = await writeProfileTreeFile(workspaceId, sourceTreeId, {
|
|
||||||
path: selectedFile.file.path,
|
|
||||||
revision: selectedFile.file.revision,
|
|
||||||
content: draftContent,
|
|
||||||
});
|
|
||||||
draftContent = selectedFile.content;
|
|
||||||
await reloadTree();
|
|
||||||
message = 'Saved Decodal profile source.';
|
|
||||||
} catch (err) {
|
|
||||||
message = err instanceof Error ? err.message : 'profile source save failed';
|
|
||||||
} finally {
|
|
||||||
saving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFile() {
|
|
||||||
if (!selectedFile) return;
|
|
||||||
deleting = true;
|
|
||||||
message = null;
|
|
||||||
try {
|
|
||||||
tree = await deleteProfileTreeFile(workspaceId, sourceTreeId, {
|
|
||||||
path: selectedFile.file.path,
|
|
||||||
revision: selectedFile.file.revision,
|
|
||||||
});
|
|
||||||
selectedFile = null;
|
|
||||||
draftContent = '';
|
|
||||||
diagnostics = tree.diagnostics;
|
|
||||||
message = 'Deleted Decodal profile source.';
|
|
||||||
} catch (err) {
|
|
||||||
message = err instanceof Error ? err.message : 'profile source delete failed';
|
|
||||||
} finally {
|
|
||||||
deleting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!workspaceId || !sourceTreeId) {
|
|
||||||
loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading = true;
|
|
||||||
reloadTree()
|
|
||||||
.catch((err) => {
|
|
||||||
message = err instanceof Error ? err.message : 'profile source tree load failed';
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Profile source tree · Yoi Workspace</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<section class="card settings-section" aria-labelledby="profile-tree-title">
|
|
||||||
<header class="settings-section-header">
|
|
||||||
<div>
|
|
||||||
<h2 id="profile-tree-title">Profile source tree</h2>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{#if loading}
|
|
||||||
<p class="status-message">Loading source tree…</p>
|
|
||||||
{:else if tree}
|
|
||||||
<p class="settings-note">
|
|
||||||
{tree.tree.root_path} · {tree.tree.file_count} files · {tree.tree.content_type} · {tree.tree.content_digest}
|
|
||||||
</p>
|
|
||||||
<div class="settings-inline-form">
|
|
||||||
<input bind:value={newFilePath} aria-label="New profile source virtual path" placeholder="profiles/new-profile.dcdl" />
|
|
||||||
<button type="button" disabled={creating} onclick={createFile}>{creating ? 'Creating…' : 'Create source'}</button>
|
|
||||||
</div>
|
|
||||||
<div class="settings-profile-grid">
|
|
||||||
<article>
|
|
||||||
<h3>Files</h3>
|
|
||||||
<ul class="settings-profile-list">
|
|
||||||
{#each tree.files as file (file.path)}
|
|
||||||
<li>
|
|
||||||
<button class="link-button" type="button" onclick={() => selectFile(file.path)}><strong>{file.path}</strong></button>
|
|
||||||
<span>{file.content_type} · {file.content_digest}</span>
|
|
||||||
<small>{file.size_bytes} bytes · rev {file.revision}</small>
|
|
||||||
<DiagnosticsList diagnostics={file.diagnostics} />
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
{#if selectedFile}
|
|
||||||
<article>
|
|
||||||
<header class="settings-section-header">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">{selectedFile.file.content_type}</p>
|
|
||||||
<h3>{selectedFile.file.path}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="settings-editor-actions">
|
|
||||||
<button type="button" disabled={saving || draftContent === selectedFile.content} onclick={saveFile}>{saving ? 'Saving…' : 'Save'}</button>
|
|
||||||
<button type="button" disabled={deleting} onclick={deleteFile}>{deleting ? 'Deleting…' : 'Delete'}</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<DecodalSourceEditor value={draftContent} onChange={(value) => (draftContent = value)} ariaLabel={`Decodal source ${selectedFile.file.path}`} />
|
|
||||||
</article>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if message}<p class="status-message">{message}</p>{/if}
|
|
||||||
<DiagnosticsList {diagnostics} />
|
|
||||||
</section>
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
import type { PageLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageLoad = ({ params }) => ({
|
|
||||||
sourceTreeId: params.sourceTreeId,
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user