feat: refresh shared Git config mounts live
- Use profile-scoped canonical Git clone sources with locked refreshes - Mount shared Git configuration read-only and isolate profile content - Add API and desktop/mobile actions for live Git mount refresh Quality gates: frontend build and backend py_compile passed. Skipped: backend pytest/Ruff unavailable; Docker/manual checks not approved.
This commit is contained in:
@@ -2,7 +2,7 @@ import { apiClient } from "./client";
|
||||
|
||||
export interface ConfigProfileRefreshOutcome {
|
||||
instance_id: string;
|
||||
status: "compatible" | "restart_required" | "incompatible_permissions";
|
||||
status: "compatible" | "refreshed" | "restart_required" | "incompatible_permissions";
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,15 @@ export const deleteConfigProfile = async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/config-profiles/${id}`);
|
||||
};
|
||||
|
||||
export const refreshConfigProfileGitMounts = async (
|
||||
id: string,
|
||||
): Promise<{ refresh_outcomes: ConfigProfileRefreshOutcome[] }> => {
|
||||
const response = await apiClient.post<{
|
||||
refresh_outcomes: ConfigProfileRefreshOutcome[];
|
||||
}>(`/config-profiles/${id}/refresh-git-mounts`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateProfileIncludes = async (
|
||||
id: string,
|
||||
data: UpdateIncludesRequest,
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
onSubmit: (e?: React.FormEvent) => void;
|
||||
onReset: () => void;
|
||||
onPreview: () => void;
|
||||
onRefreshGitMounts: () => void;
|
||||
onAddInclude: (id: string) => void;
|
||||
onRemoveInclude: (index: number) => void;
|
||||
onDragStart: (e: React.DragEvent, index: number) => void;
|
||||
@@ -63,6 +64,7 @@ export const ConfigProfileEditorPanel = ({
|
||||
onSubmit,
|
||||
onReset,
|
||||
onPreview,
|
||||
onRefreshGitMounts,
|
||||
onAddInclude,
|
||||
onRemoveInclude,
|
||||
onDragStart,
|
||||
@@ -114,6 +116,9 @@ export const ConfigProfileEditorPanel = ({
|
||||
</div>
|
||||
{!isCreating && selectedProfile && (
|
||||
<div className="row row-sm">
|
||||
<button className="btn btn-secondary" onClick={onRefreshGitMounts}>
|
||||
<Icon name="refresh" size="sm" /> Refresh Git mounts
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={onPreview} disabled={previewingId === selectedProfile.id}>
|
||||
{previewingId === selectedProfile.id ? (
|
||||
<><Icon name="loading" size="sm" /> Previewing...</>
|
||||
|
||||
@@ -64,6 +64,7 @@ interface Props {
|
||||
) => void;
|
||||
onRemoveMountFile: (mountIndex: number, path: string) => void;
|
||||
onPreview: (id: string) => void;
|
||||
onRefreshGitMounts: (id: string) => void;
|
||||
onClosePreview: () => void;
|
||||
}
|
||||
|
||||
@@ -104,6 +105,7 @@ export const ConfigProfilesMobileView = ({
|
||||
onUpdateMountFile,
|
||||
onRemoveMountFile,
|
||||
onPreview,
|
||||
onRefreshGitMounts,
|
||||
onClosePreview,
|
||||
}: Props) => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -363,6 +365,13 @@ export const ConfigProfilesMobileView = ({
|
||||
onDelete={handleDeleteClick}
|
||||
>
|
||||
<div className="mobile-detail-actions-extra">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => onRefreshGitMounts(selectedProfile.id)}
|
||||
>
|
||||
<Icon name="refresh" size="sm" /> Refresh Git mounts
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
deleteConfigProfile,
|
||||
listConfigProfiles,
|
||||
previewConfigProfile,
|
||||
refreshConfigProfileGitMounts,
|
||||
updateConfigProfile,
|
||||
updateProfileIncludes,
|
||||
type ConfigProfile,
|
||||
@@ -273,6 +274,25 @@ export const useConfigProfiles = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshGitMounts = async (id: string): Promise<boolean> => {
|
||||
setError(null);
|
||||
try {
|
||||
const result = await refreshConfigProfileGitMounts(id);
|
||||
const refreshed = result.refresh_outcomes.filter(
|
||||
(outcome) => outcome.status === "refreshed",
|
||||
);
|
||||
setError(
|
||||
refreshed.length
|
||||
? `Refreshed Git mounts for ${refreshed.length} running instance${refreshed.length === 1 ? "" : "s"}.`
|
||||
: "No running instances currently use this profile's Git mounts.",
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(extractErrorMessage(err));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreview = async (id: string) => {
|
||||
try {
|
||||
setPreviewingId(id);
|
||||
@@ -434,6 +454,7 @@ export const useConfigProfiles = () => {
|
||||
handleSubmit,
|
||||
handleDelete,
|
||||
handlePreview,
|
||||
handleRefreshGitMounts,
|
||||
updateFormField,
|
||||
addEnvVar,
|
||||
updateEnvVar,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const ConfigProfilesPage = () => {
|
||||
handleSubmit,
|
||||
handleDelete,
|
||||
handlePreview,
|
||||
handleRefreshGitMounts,
|
||||
updateFormField,
|
||||
addEnvVar,
|
||||
updateEnvVar,
|
||||
@@ -135,6 +136,7 @@ export const ConfigProfilesPage = () => {
|
||||
onUpdateMountFile={updateMountFile}
|
||||
onRemoveMountFile={removeMountFile}
|
||||
onPreview={handlePreview}
|
||||
onRefreshGitMounts={(id) => void handleRefreshGitMounts(id)}
|
||||
onClosePreview={() => setPreviewData(null)}
|
||||
/>
|
||||
);
|
||||
@@ -176,6 +178,7 @@ export const ConfigProfilesPage = () => {
|
||||
onSubmit={handleSubmit}
|
||||
onReset={handleReset}
|
||||
onPreview={() => selectedProfile && handlePreview(selectedProfile.id)}
|
||||
onRefreshGitMounts={() => selectedProfile && handleRefreshGitMounts(selectedProfile.id)}
|
||||
onAddInclude={addInclude}
|
||||
onRemoveInclude={removeInclude}
|
||||
onDragStart={handleDragStart}
|
||||
|
||||
Reference in New Issue
Block a user