Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 21 additions & 26 deletions client/src/protoFleet/api/buildings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,20 @@ interface DeleteBuildingProps {
onFinally?: () => void;
}

interface AssignRackToBuildingProps {
// RackPlacementInput is one rack's slot in an AssignRacksToBuilding
// batch. aisleIndex + positionInAisle must be paired (both set or both
// unset); the server rejects half-set inputs.
export interface RackPlacementInput {
rackId: bigint;
// Unset = unassign from any building.
buildingId?: bigint;
// Optional grid cell. Must be paired.
aisleIndex?: number;
positionInAisle?: number;
}

interface AssignRacksToBuildingProps {
// Bulk-friendly. Pass a single-element array for the singular case.
racks: RackPlacementInput[];
// Unset = unassign every rack in the batch from any building.
targetBuildingId?: bigint;
signal?: AbortSignal;
onSuccess?: (siteReassignedDeviceCount: bigint) => void;
onError?: (message: string) => void;
Expand Down Expand Up @@ -353,29 +360,17 @@ const useBuildings = () => {
[handleAuthErrors],
);

// assignRackToBuilding wraps the dedicated rack-positioning RPC.
// Unset `buildingId` unassigns the rack; passing both `aisleIndex`
// and `positionInAisle` positions the rack at that grid cell.
// Passing one without the other is rejected by the server; the
// wrapper preserves the failure surface so callers can react.
const assignRackToBuilding = useCallback(
async ({
rackId,
buildingId,
aisleIndex,
positionInAisle,
signal,
onSuccess,
onError,
onFinally,
}: AssignRackToBuildingProps) => {
// assignRacksToBuilding wraps the bulk rack-positioning RPC.
// Unset `targetBuildingId` unassigns every rack in the batch;
// each rack's `aisleIndex` + `positionInAisle` must be paired.
// The server rejects half-set inputs and out-of-bounds positions.
const assignRacksToBuilding = useCallback(
async ({ racks, targetBuildingId, signal, onSuccess, onError, onFinally }: AssignRacksToBuildingProps) => {
try {
const response = await buildingsClient.assignRackToBuilding(
const response = await buildingsClient.assignRacksToBuilding(
{
rackId,
buildingId,
aisleIndex,
positionInAisle,
racks,
targetBuildingId,
},
{ signal },
);
Expand Down Expand Up @@ -404,7 +399,7 @@ const useBuildings = () => {
createBuilding,
updateBuilding,
deleteBuilding,
assignRackToBuilding,
assignRacksToBuilding,
};
};

Expand Down
102 changes: 63 additions & 39 deletions client/src/protoFleet/api/generated/buildings/v1/buildings_pb.ts

Large diffs are not rendered by default.

Large diffs are not rendered by default.

160 changes: 118 additions & 42 deletions client/src/protoFleet/api/generated/sites/v1/sites_pb.ts

Large diffs are not rendered by default.

73 changes: 61 additions & 12 deletions client/src/protoFleet/api/sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ interface DeleteSiteProps {
onFinally?: () => void;
}

interface ReassignDevicesToSiteProps {
interface AssignDevicesToSiteProps {
// Unset routes the devices to the "Unassigned" bucket; the create flow
// always supplies a target so this is typically set in practice.
targetSiteId?: bigint;
Expand All @@ -128,16 +128,30 @@ interface ReassignDevicesToSiteProps {
onFinally?: () => void;
}

interface AssignBuildingToSiteProps {
buildingId: bigint;
// Unset moves the building to "Unassigned".
interface AssignBuildingsToSiteProps {
// Bulk-friendly. Pass a single-element array for the singular case.
buildingIds: bigint[];
// Unset moves the buildings to "Unassigned".
targetSiteId?: bigint;
signal?: AbortSignal;
onSuccess?: (reassignedRackCount: bigint, reassignedDeviceCount: bigint) => void;
onError?: (message: string) => void;
onFinally?: () => void;
}

interface AssignRacksToSiteProps {
// Bulk-friendly. Pass a single-element array for the singular case.
rackIds: bigint[];
// Unset moves the racks to "Unassigned".
targetSiteId?: bigint;
signal?: AbortSignal;
// onSuccess args: device cascade count, count of racks whose
// building was auto-cleared because the move crossed sites.
onSuccess?: (reassignedDeviceCount: bigint, clearedBuildingCount: bigint) => void;
onError?: (message: string) => void;
onFinally?: () => void;
}

const useSites = () => {
const { handleAuthErrors } = useAuthErrors();

Expand Down Expand Up @@ -267,10 +281,10 @@ const useSites = () => {
[handleAuthErrors],
);

const reassignDevicesToSite = useCallback(
async ({ targetSiteId, deviceIdentifiers, signal, onSuccess, onError, onFinally }: ReassignDevicesToSiteProps) => {
const assignDevicesToSite = useCallback(
async ({ targetSiteId, deviceIdentifiers, signal, onSuccess, onError, onFinally }: AssignDevicesToSiteProps) => {
try {
const response = await sitesClient.reassignDevicesToSite(
const response = await sitesClient.assignDevicesToSite(
{
targetSiteId,
deviceIdentifiers,
Expand Down Expand Up @@ -298,12 +312,12 @@ const useSites = () => {
[handleAuthErrors],
);

const assignBuildingToSite = useCallback(
async ({ buildingId, targetSiteId, signal, onSuccess, onError, onFinally }: AssignBuildingToSiteProps) => {
const assignBuildingsToSite = useCallback(
async ({ buildingIds, targetSiteId, signal, onSuccess, onError, onFinally }: AssignBuildingsToSiteProps) => {
try {
const response = await sitesClient.assignBuildingToSite(
const response = await sitesClient.assignBuildingsToSite(
{
buildingId,
buildingIds,
targetSiteId,
},
{ signal },
Expand All @@ -325,7 +339,42 @@ const useSites = () => {
[handleAuthErrors],
);

return { listSites, createSite, updateSite, deleteSite, reassignDevicesToSite, assignBuildingToSite };
const assignRacksToSite = useCallback(
async ({ rackIds, targetSiteId, signal, onSuccess, onError, onFinally }: AssignRacksToSiteProps) => {
try {
const response = await sitesClient.assignRacksToSite(
{
rackIds,
targetSiteId,
},
{ signal },
);
if (signal?.aborted) return;
onSuccess?.(response.reassignedDeviceCount, response.clearedBuildingCount);
} catch (err) {
if (signal?.aborted) return;
handleAuthErrors({
error: err,
onError: (error) => {
onError?.(getErrorMessage(error));
},
});
} finally {
onFinally?.();
}
},
[handleAuthErrors],
);

return {
listSites,
createSite,
updateSite,
deleteSite,
assignDevicesToSite,
assignBuildingsToSite,
assignRacksToSite,
};
};

export { useSites };
38 changes: 38 additions & 0 deletions client/src/protoFleet/api/useDeviceSets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ interface ClearRackSlotPositionProps {
onFinally?: () => void;
}

interface AssignDevicesToRackProps {
// Unset clears rack membership without re-assigning (site/building
// stay intact).
targetRackId?: bigint;
deviceIdentifiers: string[];
onSuccess?: (assignedCount: bigint, siteReassignedCount: bigint, removedCount: bigint) => void;
onError?: (message: string) => void;
onFinally?: () => void;
}

interface SaveRackProps {
deviceSetId?: bigint;
label: string;
Expand Down Expand Up @@ -601,6 +611,33 @@ const useDeviceSets = () => {
[handleAuthErrors],
);

// assignDevicesToRack wraps the atomic rack-reassignment RPC.
// Replaces the old client-side remove + add orchestration so a
// server error / network blip between the two calls can't orphan
// miners from rack assignment (issue #420). Pass targetRackId
// unset to clear rack membership without re-assigning.
const assignDevicesToRack = useCallback(
async ({ targetRackId, deviceIdentifiers, onSuccess, onError, onFinally }: AssignDevicesToRackProps) => {
try {
const response = await deviceSetClient.assignDevicesToRack({
targetRackId,
deviceIdentifiers,
});
onSuccess?.(response.assignedCount, response.siteReassignedCount, response.removedCount);
} catch (err) {
handleAuthErrors({
error: err,
onError: () => {
onError?.(getErrorMessage(err));
},
});
} finally {
onFinally?.();
}
},
[handleAuthErrors],
);

const removeDevicesFromDeviceSet = useCallback(
async ({
deviceSetId,
Expand Down Expand Up @@ -848,6 +885,7 @@ const useDeviceSets = () => {
listGroupMembers,
getDeviceSetStats,
addDevicesToDeviceSet,
assignDevicesToRack,
removeDevicesFromDeviceSet,
getRackSlots,
setRackSlotPosition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const mockApi = {
createBuilding: vi.fn(),
updateBuilding: vi.fn(),
deleteBuilding: vi.fn(),
assignRackToBuilding: vi.fn(),
assignRacksToBuilding: vi.fn(),
};
vi.mock("@/protoFleet/api/buildings", async () => {
const actual = await vi.importActual<typeof import("@/protoFleet/api/buildings")>("@/protoFleet/api/buildings");
Expand Down
Loading
Loading