From b758ec74439f559061ff68b101f33683f252e46b Mon Sep 17 00:00:00 2001 From: Gareth George Date: Tue, 21 Apr 2026 01:32:28 -0700 Subject: [PATCH] fix test failures etc --- internal/api/syncapi/syncclient.go | 51 ++--- internal/api/syncapi/syncserver.go | 5 +- webui/src/app/App.tsx | 287 ++++++++++++++++++----------- 3 files changed, 212 insertions(+), 131 deletions(-) diff --git a/internal/api/syncapi/syncclient.go b/internal/api/syncapi/syncclient.go index e9c611cc..1c33aea8 100644 --- a/internal/api/syncapi/syncclient.go +++ b/internal/api/syncapi/syncclient.go @@ -280,28 +280,27 @@ func (c *syncSessionHandlerClient) OnConnectionEstablished(ctx context.Context, c.mgr.peerStateManager.SetPeerState(peer.Keyid, peerState) // Clear the pairing secret from the known host entry now that pairing has succeeded. - if c.syncConfigSnapshot.config.GetMultihost() != nil { - for _, knownHost := range c.syncConfigSnapshot.config.GetMultihost().GetKnownHosts() { - if knownHost.GetKeyid() == peer.GetKeyid() && knownHost.GetInitialPairingSecret() != "" { - cfg, err := c.mgr.configMgr.Get() - if err != nil { - c.l.Sugar().Warnf("failed to get config to clear pairing secret: %v", err) - break - } - cfg = proto.Clone(cfg).(*v1.Config) - for _, kh := range cfg.GetMultihost().GetKnownHosts() { - if kh.GetKeyid() == peer.GetKeyid() { - kh.InitialPairingSecret = "" - break - } - } - cfg.Modno++ - if err := c.mgr.configMgr.Update(cfg); err != nil { - c.l.Sugar().Warnf("failed to clear pairing secret after successful pairing: %v", err) - } else { - c.l.Sugar().Infof("cleared pairing secret for peer %q after successful connection", peer.InstanceId) - } - break + knownHosts := c.syncConfigSnapshot.config.GetMultihost().GetKnownHosts() + khIdx := slices.IndexFunc(knownHosts, func(kh *v1.Multihost_Peer) bool { + return kh.GetKeyid() == peer.GetKeyid() + }) + if khIdx >= 0 && knownHosts[khIdx].GetInitialPairingSecret() != "" { + cfg, err := c.mgr.configMgr.Get() + if err != nil { + c.l.Sugar().Warnf("failed to get config to clear pairing secret: %v", err) + } else { + cfg = proto.Clone(cfg).(*v1.Config) + liveIdx := slices.IndexFunc(cfg.GetMultihost().GetKnownHosts(), func(kh *v1.Multihost_Peer) bool { + return kh.GetKeyid() == peer.GetKeyid() + }) + if liveIdx >= 0 { + cfg.GetMultihost().GetKnownHosts()[liveIdx].InitialPairingSecret = "" + } + cfg.Modno++ + if err := c.mgr.configMgr.Update(cfg); err != nil { + c.l.Sugar().Warnf("failed to clear pairing secret after successful pairing: %v", err) + } else { + c.l.Sugar().Infof("cleared pairing secret for peer %q after successful connection", peer.InstanceId) } } } @@ -517,6 +516,14 @@ func (c *syncSessionHandlerClient) HandleSetConfig(ctx context.Context, stream * if idx >= 0 { latestConfig.Repos[idx] = repo } else { + // Check for conflicts with existing local repos by ID or URI + conflictIdx := slices.IndexFunc(latestConfig.Repos, func(r *v1.Repo) bool { + return r.Id == repo.Id || r.Uri == repo.Uri + }) + if conflictIdx >= 0 { + c.l.Sugar().Warnf("received shared repo %q (guid %s) conflicts with existing local repo %q (guid %s), skipping", repo.Id, repo.Guid, latestConfig.Repos[conflictIdx].Id, latestConfig.Repos[conflictIdx].Guid) + continue + } latestConfig.Repos = append(latestConfig.Repos, repo) } } diff --git a/internal/api/syncapi/syncserver.go b/internal/api/syncapi/syncserver.go index 4fd72da3..a755425c 100644 --- a/internal/api/syncapi/syncserver.go +++ b/internal/api/syncapi/syncserver.go @@ -520,9 +520,10 @@ func (h *syncSessionHandlerServer) HandleOperationManifest(ctx context.Context, } } - // Find ops we need (new or changed modno) + // Find ops we need (new or changed modno), preserving manifest order var needIDs []int64 - for id, modno := range remoteSet { + for i, id := range item.GetOpIds() { + modno := item.GetModnos()[i] local, exists := localState[id] if !exists || local.modno != modno { needIDs = append(needIDs, id) diff --git a/webui/src/app/App.tsx b/webui/src/app/App.tsx index dfa808e7..e9982fb9 100644 --- a/webui/src/app/App.tsx +++ b/webui/src/app/App.tsx @@ -13,6 +13,7 @@ import { FiEdit2, FiMenu, FiHome, + FiChevronRight, } from "react-icons/fi"; import { @@ -256,6 +257,175 @@ const PlanViewContainer = () => { ); }; +const PeerNavItem = ({ + icon, + typeLabel, + name, + active, + onClick, + onEdit, +}: { + icon: React.ReactNode; + typeLabel: string; + name: string; + active: boolean; + onClick: () => void; + onEdit?: (e: React.MouseEvent) => void; +}) => ( + + + {icon} + + + {typeLabel} + + + {name} + + {onEdit && ( + + { + e.stopPropagation(); + onEdit(e); + }} + > + + + + )} + +); + +const PeerInstanceSection = ({ + peerState, + sel, + remoteConfig, + isActive, + handleNav, + handleRemoteRepoEdit, + handleRemotePlanEdit, +}: { + peerState: PeerState; + sel: OpSelector; + remoteConfig: PeerState["remoteConfig"]; + isActive: (path: string) => boolean; + handleNav: (path: string) => void; + handleRemoteRepoEdit: (repo: Repo) => void; + handleRemotePlanEdit: (plan: Plan) => void; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( + + setExpanded((prev) => !prev)} + > + + + + + + + + {peerState.peerInstanceId} + + + + {expanded && ( + <> + {peerState.knownRepos.map((repo: RepoMetadata) => { + const repoPath = `/peer/${peerState.peerInstanceId}/repo/${repo.id}`; + const editableRepo = remoteConfig?.repos?.find( + (r: Repo) => r.guid === repo.guid, + ); + return ( + + } + typeLabel="repo" + name={repo.id} + active={isActive(repoPath)} + onClick={() => handleNav(repoPath)} + onEdit={ + editableRepo + ? () => handleRemoteRepoEdit(editableRepo) + : undefined + } + /> + ); + })} + + {peerState.knownPlans.map((planMeta: PlanMetadata) => { + const planPath = `/peer/${peerState.peerInstanceId}/plan/${planMeta.id}`; + const editablePlan = remoteConfig?.plans?.find( + (p: Plan) => p.id === planMeta.id, + ); + return ( + + } + typeLabel="plan" + name={planMeta.id} + active={isActive(planPath)} + onClick={() => handleNav(planPath)} + onEdit={ + editablePlan + ? () => handleRemotePlanEdit(editablePlan) + : undefined + } + /> + ); + })} + + )} + + ); +}; + const SidebarPlanItem = React.memo( ({ plan, @@ -640,113 +810,16 @@ const SidebarContent = ({ onClose }: { onClose?: () => void }) => { }; return ( - - - - - - - {peerState.peerInstanceId} - - - - {/* Nested Repos for Peer — listed from knownRepos (READ_OPERATIONS), edit from remoteConfig (READ_CONFIG) */} - {peerState.knownRepos.map((repo: RepoMetadata) => { - const repoPath = `/peer/${peerState.peerInstanceId}/repo/${repo.id}`; - const active = isActive(repoPath); - const editableRepo = remoteConfig?.repos?.find((r: Repo) => r.guid === repo.guid); - return ( - handleNav(repoPath)} - > - - - - - {repo.id} - - {editableRepo && ( - - { - e.stopPropagation(); - handleRemoteRepoEdit(editableRepo); - }} - > - - - - )} - - ); - })} - - {/* Nested Plans for Peer — listed from knownPlans, edit from remoteConfig */} - {peerState.knownPlans.map((planMeta: PlanMetadata) => { - const planPath = `/peer/${peerState.peerInstanceId}/plan/${planMeta.id}`; - const active = isActive(planPath); - const editablePlan = remoteConfig?.plans?.find((p: Plan) => p.id === planMeta.id); - return ( - handleNav(planPath)} - > - - - - - {planMeta.id} - - {editablePlan && ( - - { - e.stopPropagation(); - handleRemotePlanEdit(editablePlan); - }} - > - - - - )} - - ); - })} - + ); })}