feat: add live config profile refresh
- Standardize built-in tool users for shared writable profile mounts - Mount canonical non-Git profile sources across compatible instances - Report restart-required outcomes and guard active profile deletion - Surface restart feedback in config profile editing Quality gates: frontend build passed; backend py_compile and LSP passed. Skipped: backend pytest/Ruff unavailable; Docker/manual checks not approved.
This commit is contained in:
@@ -34,17 +34,21 @@ export const useConfigProfiles = () => {
|
||||
const [profiles, setProfiles] = useState<ConfigProfile[]>([]);
|
||||
const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
|
||||
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(null);
|
||||
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [previewData, setPreviewData] = useState<ResolvedProfile | null>(null);
|
||||
const [previewingId, setPreviewingId] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<CreateConfigProfileRequest>(defaultForm);
|
||||
const [formData, setFormData] =
|
||||
useState<CreateConfigProfileRequest>(defaultForm);
|
||||
const [includedProfileIds, setIncludedProfileIds] = useState<string[]>([]);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
|
||||
const selectedProfile = profiles.find((p) => p.id === selectedProfileId) || null;
|
||||
const selectedProfile =
|
||||
profiles.find((p) => p.id === selectedProfileId) || null;
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setStatus("loading");
|
||||
@@ -89,7 +93,9 @@ export const useConfigProfiles = () => {
|
||||
is_default: profile.is_default,
|
||||
});
|
||||
setIncludedProfileIds(
|
||||
profile.includes.map((inc: { included_profile_id: string }) => inc.included_profile_id),
|
||||
profile.includes.map(
|
||||
(inc: { included_profile_id: string }) => inc.included_profile_id,
|
||||
),
|
||||
);
|
||||
setError(null);
|
||||
setSaveStatus("idle");
|
||||
@@ -208,20 +214,39 @@ export const useConfigProfiles = () => {
|
||||
if (isCreating) {
|
||||
const newProfile = await createConfigProfile(formData);
|
||||
if (includedProfileIds.length > 0) {
|
||||
await updateProfileIncludes(newProfile.id, { includes: includedProfileIds });
|
||||
await updateProfileIncludes(newProfile.id, {
|
||||
includes: includedProfileIds,
|
||||
});
|
||||
}
|
||||
setIsCreating(false);
|
||||
setSelectedProfileId(newProfile.id);
|
||||
setSaveStatus("saved");
|
||||
await loadData();
|
||||
const refreshed = (await listConfigProfiles()).find((p) => p.id === newProfile.id);
|
||||
const refreshed = (await listConfigProfiles()).find(
|
||||
(p) => p.id === newProfile.id,
|
||||
);
|
||||
if (refreshed) populateForm(refreshed);
|
||||
} else if (selectedProfile) {
|
||||
await updateConfigProfile(selectedProfile.id, formData);
|
||||
await updateProfileIncludes(selectedProfile.id, { includes: includedProfileIds });
|
||||
const updatedProfile = await updateConfigProfile(
|
||||
selectedProfile.id,
|
||||
formData,
|
||||
);
|
||||
await updateProfileIncludes(selectedProfile.id, {
|
||||
includes: includedProfileIds,
|
||||
});
|
||||
const restartOutcomes = updatedProfile.refresh_outcomes?.filter(
|
||||
(outcome) => outcome.status === "restart_required",
|
||||
);
|
||||
if (restartOutcomes?.length) {
|
||||
setError(
|
||||
`Profile saved. ${restartOutcomes.length} running instance${restartOutcomes.length === 1 ? "" : "s"} must restart to adopt these changes.`,
|
||||
);
|
||||
}
|
||||
setSaveStatus("saved");
|
||||
await loadData();
|
||||
const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id);
|
||||
const refreshed = (await listConfigProfiles()).find(
|
||||
(p) => p.id === selectedProfile.id,
|
||||
);
|
||||
if (refreshed) populateForm(refreshed);
|
||||
}
|
||||
return true;
|
||||
@@ -233,7 +258,8 @@ export const useConfigProfiles = () => {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!window.confirm("Are you sure you want to delete this config profile?")) return;
|
||||
if (!window.confirm("Are you sure you want to delete this config profile?"))
|
||||
return;
|
||||
try {
|
||||
await deleteConfigProfile(id);
|
||||
if (selectedProfileId === id) {
|
||||
@@ -242,8 +268,8 @@ export const useConfigProfiles = () => {
|
||||
resetForm();
|
||||
}
|
||||
await loadData();
|
||||
} catch {
|
||||
alert("Failed to delete config profile");
|
||||
} catch (err) {
|
||||
setError(extractErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -268,7 +294,10 @@ export const useConfigProfiles = () => {
|
||||
};
|
||||
|
||||
const addEnvVar = () => {
|
||||
setFormData((prev) => ({ ...prev, env_vars: { ...prev.env_vars, "": "" } }));
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
env_vars: { ...prev.env_vars, "": "" },
|
||||
}));
|
||||
setSaveStatus("idle");
|
||||
};
|
||||
|
||||
@@ -323,7 +352,10 @@ export const useConfigProfiles = () => {
|
||||
setSaveStatus("idle");
|
||||
};
|
||||
|
||||
const updateMount = (index: number, updates: Partial<ConfigProfile["mounts"][0]>) => {
|
||||
const updateMount = (
|
||||
index: number,
|
||||
updates: Partial<ConfigProfile["mounts"][0]>,
|
||||
) => {
|
||||
setFormData((prev) => {
|
||||
const mounts = [...(prev.mounts || [])];
|
||||
mounts[index] = { ...mounts[index], ...updates };
|
||||
|
||||
Reference in New Issue
Block a user