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);
- }}
- >
-
-
-
- )}
-
- );
- })}
-
+
);
})}