Compare commits

..

2 Commits

Author SHA1 Message Date
alex 900a8e47a5 fix(config-profiles): show Git mount refresh progress
Disable duplicate refresh requests and show in-progress feedback while Git mount sources are refreshed.
2026-07-21 21:14:08 +02:00
alex 25e870ba43 merge: fix container mount layering 2026-07-21 20:54:44 +02:00
7 changed files with 44 additions and 5 deletions
@@ -14,6 +14,7 @@ interface Props {
saveStatus: "idle" | "saving" | "saved" | "error"; saveStatus: "idle" | "saving" | "saved" | "error";
previewData: ResolvedProfile | null; previewData: ResolvedProfile | null;
previewingId: string | null; previewingId: string | null;
isRefreshingGitMounts: boolean;
projects: ProjectWithRepos[]; projects: ProjectWithRepos[];
toolTypes: ToolType[]; toolTypes: ToolType[];
availableProfiles: ConfigProfile[]; availableProfiles: ConfigProfile[];
@@ -55,6 +56,7 @@ export const ConfigProfileEditorPanel = ({
saveStatus, saveStatus,
previewData, previewData,
previewingId, previewingId,
isRefreshingGitMounts,
projects, projects,
toolTypes, toolTypes,
availableProfiles, availableProfiles,
@@ -116,8 +118,16 @@ export const ConfigProfileEditorPanel = ({
</div> </div>
{!isCreating && selectedProfile && ( {!isCreating && selectedProfile && (
<div className="row row-sm"> <div className="row row-sm">
<button className="btn btn-secondary" onClick={onRefreshGitMounts}> <button className="btn btn-secondary" onClick={onRefreshGitMounts} disabled={isRefreshingGitMounts}>
<Icon name="refresh" size="sm" /> Refresh Git mounts {isRefreshingGitMounts ? (
<>
<Icon name="loading" size="sm" /> Refreshing Git mounts...
</>
) : (
<>
<Icon name="refresh" size="sm" /> Refresh Git mounts
</>
)}
</button> </button>
<button className="btn btn-secondary" onClick={onPreview} disabled={previewingId === selectedProfile.id}> <button className="btn btn-secondary" onClick={onPreview} disabled={previewingId === selectedProfile.id}>
{previewingId === selectedProfile.id ? ( {previewingId === selectedProfile.id ? (
@@ -28,6 +28,7 @@ interface Props {
availableProfiles: ConfigProfile[]; availableProfiles: ConfigProfile[];
previewData: ResolvedProfile | null; previewData: ResolvedProfile | null;
previewingId: string | null; previewingId: string | null;
isRefreshingGitMounts: boolean;
saveStatus: "idle" | "saving" | "saved" | "error"; saveStatus: "idle" | "saving" | "saved" | "error";
error: string | null; error: string | null;
onViewChange: (view: MobileView) => void; onViewChange: (view: MobileView) => void;
@@ -80,6 +81,7 @@ export const ConfigProfilesMobileView = ({
availableProfiles, availableProfiles,
previewData, previewData,
previewingId, previewingId,
isRefreshingGitMounts,
saveStatus, saveStatus,
error, error,
onViewChange, onViewChange,
@@ -368,9 +370,19 @@ export const ConfigProfilesMobileView = ({
<button <button
type="button" type="button"
className="secondary-button" className="secondary-button"
disabled={isRefreshingGitMounts}
onClick={() => onRefreshGitMounts(selectedProfile.id)} onClick={() => onRefreshGitMounts(selectedProfile.id)}
> >
<Icon name="refresh" size="sm" /> Refresh Git mounts {isRefreshingGitMounts ? (
<>
<Icon name="loading" size="sm" />
Refreshing Git mounts...
</>
) : (
<>
<Icon name="refresh" size="sm" /> Refresh Git mounts
</>
)}
</button> </button>
<button <button
type="button" type="button"
@@ -43,6 +43,7 @@ export const useConfigProfiles = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [previewData, setPreviewData] = useState<ResolvedProfile | null>(null); const [previewData, setPreviewData] = useState<ResolvedProfile | null>(null);
const [previewingId, setPreviewingId] = useState<string | null>(null); const [previewingId, setPreviewingId] = useState<string | null>(null);
const [isRefreshingGitMounts, setIsRefreshingGitMounts] = useState(false);
const [formData, setFormData] = const [formData, setFormData] =
useState<CreateConfigProfileRequest>(defaultForm); useState<CreateConfigProfileRequest>(defaultForm);
const [includedProfileIds, setIncludedProfileIds] = useState<string[]>([]); const [includedProfileIds, setIncludedProfileIds] = useState<string[]>([]);
@@ -275,7 +276,10 @@ export const useConfigProfiles = () => {
}; };
const handleRefreshGitMounts = async (id: string): Promise<boolean> => { const handleRefreshGitMounts = async (id: string): Promise<boolean> => {
if (isRefreshingGitMounts) return false;
setError(null); setError(null);
setIsRefreshingGitMounts(true);
try { try {
const result = await refreshConfigProfileGitMounts(id); const result = await refreshConfigProfileGitMounts(id);
const refreshed = result.refresh_outcomes.filter( const refreshed = result.refresh_outcomes.filter(
@@ -290,6 +294,8 @@ export const useConfigProfiles = () => {
} catch (err) { } catch (err) {
setError(extractErrorMessage(err)); setError(extractErrorMessage(err));
return false; return false;
} finally {
setIsRefreshingGitMounts(false);
} }
}; };
@@ -445,6 +451,7 @@ export const useConfigProfiles = () => {
error, error,
previewData, previewData,
previewingId, previewingId,
isRefreshingGitMounts,
formData, formData,
includedProfileIds, includedProfileIds,
dragOverIndex, dragOverIndex,
+6 -1
View File
@@ -23,6 +23,7 @@ export const ConfigProfilesPage = () => {
error, error,
previewData, previewData,
previewingId, previewingId,
isRefreshingGitMounts,
formData, formData,
includedProfileIds, includedProfileIds,
dragOverIndex, dragOverIndex,
@@ -111,6 +112,7 @@ export const ConfigProfilesPage = () => {
availableProfiles={availableProfilesForInclude()} availableProfiles={availableProfilesForInclude()}
previewData={previewData} previewData={previewData}
previewingId={previewingId} previewingId={previewingId}
isRefreshingGitMounts={isRefreshingGitMounts}
saveStatus={saveStatus} saveStatus={saveStatus}
error={error} error={error}
onViewChange={setMobileView} onViewChange={setMobileView}
@@ -169,6 +171,7 @@ export const ConfigProfilesPage = () => {
saveStatus={saveStatus} saveStatus={saveStatus}
previewData={previewData} previewData={previewData}
previewingId={previewingId} previewingId={previewingId}
isRefreshingGitMounts={isRefreshingGitMounts}
projects={projects} projects={projects}
toolTypes={toolTypes} toolTypes={toolTypes}
availableProfiles={availableProfilesForInclude()} availableProfiles={availableProfilesForInclude()}
@@ -178,7 +181,9 @@ export const ConfigProfilesPage = () => {
onSubmit={handleSubmit} onSubmit={handleSubmit}
onReset={handleReset} onReset={handleReset}
onPreview={() => selectedProfile && handlePreview(selectedProfile.id)} onPreview={() => selectedProfile && handlePreview(selectedProfile.id)}
onRefreshGitMounts={() => selectedProfile && handleRefreshGitMounts(selectedProfile.id)} onRefreshGitMounts={() =>
selectedProfile && handleRefreshGitMounts(selectedProfile.id)
}
onAddInclude={addInclude} onAddInclude={addInclude}
onRemoveInclude={removeInclude} onRemoveInclude={removeInclude}
onDragStart={handleDragStart} onDragStart={handleDragStart}
@@ -14,6 +14,7 @@ Move Git Config Profile mounts to profile-scoped canonical host clones. Bind the
- In-place refresh for existing directory mounts only. - In-place refresh for existing directory mounts only.
- Explicit outcomes for live refresh, restart-required topology changes, and refresh failures. - Explicit outcomes for live refresh, restart-required topology changes, and refresh failures.
- Read-only container Git config mounts. - Read-only container Git config mounts.
- An in-progress indicator that prevents duplicate refresh requests in desktop and mobile Config Profile views.
## Out of scope ## Out of scope
@@ -21,3 +22,4 @@ Move Git Config Profile mounts to profile-scoped canonical host clones. Bind the
- Global cross-user clone sharing. - Global cross-user clone sharing.
- Live mount-topology changes, direct-file mappings, or glob match-set changes. - Live mount-topology changes, direct-file mappings, or glob match-set changes.
- Atomic all-files revision switching for processes already reading the mount. - Atomic all-files revision switching for processes already reading the mount.
- Automatic refresh of an editor buffer that has already loaded a mounted file.
@@ -17,6 +17,7 @@ Each selected Config Profile owns canonical Git clone directories beneath:
3. Clone into a temporary sibling, then rename on initial creation. 3. Clone into a temporary sibling, then rename on initial creation.
4. For refresh, fetch and update the existing working tree in place. 4. For refresh, fetch and update the existing working tree in place.
5. Bind directory mappings read-only. Existing containers see changed directory contents without recreation. 5. Bind directory mappings read-only. Existing containers see changed directory contents without recreation.
6. When profile and Git mount paths overlap, the instance-local composite source must be synchronized in place during refresh; replacing its root directory would leave a running bind mount attached to the old inode.
## Boundaries ## Boundaries
@@ -24,6 +25,7 @@ Each selected Config Profile owns canonical Git clone directories beneath:
- Refresh failure is reported without mutating a known-good checkout. - Refresh failure is reported without mutating a known-good checkout.
- No non-Git profile content may be copied into a Git checkout; overlapping targets are rejected or reported. - No non-Git profile content may be copied into a Git checkout; overlapping targets are rejected or reported.
- Containers must not write to shared Git mount sources. - Containers must not write to shared Git mount sources.
- A browser editor that already has a file open is not a filesystem watcher; the user must reload that editor buffer after the mounted source changes.
## Security ## Security
@@ -4,7 +4,8 @@
- [x] Make Git Config Profile mounts read-only and prevent profile-content copy into Git sources. - [x] Make Git Config Profile mounts read-only and prevent profile-content copy into Git sources.
- [x] Add lock-protected clone/fetch/ref checkout refresh that preserves a known-good checkout on failure. - [x] Add lock-protected clone/fetch/ref checkout refresh that preserves a known-good checkout on failure.
- [x] Add save/refresh outcomes for live refresh, restart-required topology, and failures. - [x] Add save/refresh outcomes for live refresh, restart-required topology, and failures.
- [x] Add desktop/mobile feedback for refresh outcomes. - [x] Add an in-progress desktop/mobile indicator that disables duplicate Git-mount refresh requests.
- [ ] Synchronize affected instance-local composite mounts in place so live refresh reaches running containers.
- [ ] Add focused resolver/service/API/frontend tests. - [ ] Add focused resolver/service/API/frontend tests.
- [x] Run available verification and document skipped checks. - [x] Run available verification and document skipped checks.