# CRRF Backup mgmt-basic-view Full Coverage — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Eliminate all `coreCli` (core-svc / regional K8s API) dependencies from the backup/restore cluster-lookup paths so that CRRF backup/restore operations continue to work when the regional control-plane is offline.

**Architecture:** Replace every `coreCli.GetAvailableClusterByID`, `coreCli.GetAnyClusterByID`, and `coreCli.ListClusters` call in `console/backup.go`, `inner/backup.go`, and `open/restore.go` with their `MgmtSvcGrpcClient` equivalents (`GetDedicatedClusterBasicByID` / `GetAnyDedicatedClusterBasicByID` / `ListClusters`). `MgmtSvcGrpcClient` is backed by `tidb-management-service` DB and survives regional CP outages. A new `GetDedicatedClusterBasicByID` method (non-deleted semantics, BASIC view) is added to the interface, and a new `DedicatedClusterState2OpenAPIList` conversion function is added in `open/cvt.go`.

**Tech Stack:** Go 1.21, gRPC, `github.com/tidbcloud/tidb-management-service/api`, `github.com/AlekSi/pointer`, mockery v2 for mock generation.

---

## File Map

| File | Change |
|------|--------|
| `pkg/grpc/grpcli/mgmt_svc.go` | Add `GetDedicatedClusterBasicByID` to interface + implementation |
| `pkg/grpc/grpcli/mock/mgmt_svc_grpc_client.go` | Regenerate (auto) after interface change |
| `app/controllers/console/backup.go` | `canClusterVersionEnableLogBackup`: coreCli → mgmtCli |
| `app/controllers/inner/backup.go` | Add `mgmtCli` field; fix `Restore` + `DeleteAllManualBackups`; remove `coreCli` |
| `app/controllers/open/cvt.go` | Add `DedicatedClusterState2OpenAPIList` |
| `app/controllers/open/restore.go` | Replace `coreCli` with `mgmtCli`; update all callsites |
| `app/container/container.go` | Wire `MgmtSvcGrpcClient` into `NewBackupInnerController` + `NewRestoreController` |

---

## Task 1 — Add `GetDedicatedClusterBasicByID` to `MgmtSvcGrpcClient` + regenerate mock

**Files:**
- Modify: `pkg/grpc/grpcli/mgmt_svc.go`
- Regenerate: `pkg/grpc/grpcli/mock/mgmt_svc_grpc_client.go`

### Context

The existing interface in `pkg/grpc/grpcli/mgmt_svc.go` (around line 42) already has:
```go
GetAnyDedicatedClusterBasicByID(ctx context.Context, clusterID uint64) (*mgmt.Cluster, bizerr.BizError)
```
`GetDedicatedClusterBasicByID` is the same but additionally filters to non-deleted clusters by using `Deleted: &notDeleted` in the `ClusterFilter`. This replaces the semantics of `coreCli.GetAvailableClusterByID`.

- [x] **Step 1: Add `GetDedicatedClusterBasicByID` to the `MgmtSvcGrpcClient` interface**

In `pkg/grpc/grpcli/mgmt_svc.go`, find the interface block containing `GetAnyDedicatedClusterBasicByID`. Add the new method immediately after it:

```go
// GetDedicatedClusterBasicByID returns the cluster with BASIC view for any non-deleted state.
// Returns ErrCodeRecordNotFound if the cluster does not exist or is deleted.
// Use this in place of coreCli.GetAvailableClusterByID.
GetDedicatedClusterBasicByID(ctx context.Context, clusterID uint64) (*mgmt.Cluster, bizerr.BizError)
```

- [x] **Step 2: Implement `GetDedicatedClusterBasicByID` on `mgmtSvcGrpcClient`**

Find the existing `GetAnyDedicatedClusterBasicByID` implementation (search for `func (m *mgmtSvcGrpcClient) GetAnyDedicatedClusterBasicByID`). Add the new implementation immediately after it:

```go
func (m *mgmtSvcGrpcClient) GetDedicatedClusterBasicByID(ctx context.Context, clusterID uint64) (*mgmt.Cluster, bizerr.BizError) {
	notDeleted := false
	req := &mgmt.ListClustersRequest{
		Filter: &mgmt.ClusterFilter{
			ClusterIds: []uint64{clusterID},
			View:       mgmt.ClusterFilter_CLUSTER_VIEW_BASIC,
			Deleted:    &notDeleted,
		},
	}
	listResp, err := m.client.ListClusters(ctx, req)
	if err != nil {
		m.log.Error("GetDedicatedClusterBasicByID failed", zap.Uint64("clusterID", clusterID), zap.Error(err))
		return nil, bizerr.WrapError(err, "GetDedicatedClusterBasicByID failed")
	}
	if len(listResp.GetClusters()) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "cluster %d is not found or unavailable", clusterID)
	}
	return listResp.GetClusters()[0], nil
}
```

No new imports needed — `mgmt`, `bizerr`, and `zap` are already imported.

- [x] **Step 3: Regenerate mock**

```bash
cd repos/dataflow-service
make generate4test
```

Expected: `pkg/grpc/grpcli/mock/mgmt_svc_grpc_client.go` is updated with a new `GetDedicatedClusterBasicByID` mock method.

- [x] **Step 4: Verify the generated mock compiles**

```bash
cd repos/dataflow-service
go build ./pkg/grpc/grpcli/...
```

Expected: no compilation errors.

- [x] **Step 5: Commit**

```bash
cd repos/dataflow-service
git add pkg/grpc/grpcli/mgmt_svc.go pkg/grpc/grpcli/mock/mgmt_svc_grpc_client.go
git commit -m "feat(grpcli): add GetDedicatedClusterBasicByID to MgmtSvcGrpcClient

Returns BASIC-view cluster for non-deleted state; replaces coreCli.GetAvailableClusterByID semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
```

---

## Task 2 — Fix `canClusterVersionEnableLogBackup` in `console/backup.go`

**Files:**
- Modify: `app/controllers/console/backup.go` (around line 928)

### Context

`backupControllerImpl` already has `mgmtCli pkggrpcli.MgmtSvcGrpcClient` from PR #3415.  
The only remaining `coreCli` call in this file is in `canClusterVersionEnableLogBackup`:

```go
// Before (line ~936):
cluster, bizErr := c.coreCli.GetAvailableClusterByID(ctx, clusterId)
...
v, err := semver.NewVersion(cluster.Version)
...
logger.Sugar().Warnf("parse cluster %d version %s failed: %s", clusterId, cluster.Version, err.Error())
...
logger.Sugar().Warnf("log backup enable min version is %s, but cluster %d version is %s",
    backupCfg.LogBackupEnableMinVer, clusterId, cluster.Version)
```

`cluster.Version` on `*mgmt.Cluster` is accessed via `cluster.GetDedicatedCluster().GetVersion()`.

- [x] **Step 1: Replace the cluster lookup and field access**

Find the block starting with `cluster, bizErr := c.coreCli.GetAvailableClusterByID(ctx, clusterId)` and replace as follows:

```go
cluster, bizErr := c.mgmtCli.GetDedicatedClusterBasicByID(ctx, clusterId)
if bizErr != nil {
    logger.Sugar().Warnf("get cluster %d failed: %s", clusterId, bizErr.Error())
    return false, nil
}

clusterVersion := cluster.GetDedicatedCluster().GetVersion()
v, err := semver.NewVersion(clusterVersion)
if err != nil {
    logger.Sugar().Warnf("parse cluster %d version %s failed: %s", clusterId, clusterVersion, err.Error())
    return false, nil
}

// check tidb version and phase
backupCfg := internal_config.Global().BackupConfig
if !backupCfg.LogBackupEnableMinVerConstraint.Check(v) {
    logger.Sugar().Warnf("log backup enable min version is %s, but cluster %d version is %s",
        backupCfg.LogBackupEnableMinVer, clusterId, clusterVersion)
    return false, nil
}
return true, nil
```

- [x] **Step 2: Build to confirm no compile errors**

```bash
cd repos/dataflow-service
go build ./app/controllers/console/...
```

Expected: no errors.

- [x] **Step 3: Commit**

```bash
cd repos/dataflow-service
git add app/controllers/console/backup.go
git commit -m "fix(console/backup): replace coreCli with mgmtCli in canClusterVersionEnableLogBackup

Cluster version lookup now uses tidb-management-service BASIC view instead of
regional core-svc, so it remains functional when regional control-plane is down.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
```

---

## Task 3 — Add `mgmtCli` to `BackupInnerController`, fix `Restore` and `DeleteAllManualBackups`

**Files:**
- Modify: `app/controllers/inner/backup.go`
- Modify: `app/container/container.go`

### Context

Current struct (lines ~30–50):
```go
type BackupInnerController struct {
    dataflow.UnimplementedBackupInternalServiceServer
    snapshotBackupModel backup.SnapshotBackupModel
    restoreRouter       restore.RestoreModel
    bucketModelProxy    bucket.BucketModelV2
    coreCli             pkggrpcli.CoreGrpcClient
}

func NewBackupInnerController(
    model backup.SnapshotBackupModel,
    bucketModelProxy bucket.BucketModelV2,
    restoreRouter restore.RestoreModel,
    coreCli pkggrpcli.CoreGrpcClient,
) BackupInnerController {
    return BackupInnerController{
        snapshotBackupModel: model,
        bucketModelProxy:    bucketModelProxy,
        restoreRouter:       restoreRouter,
        coreCli:             coreCli,
    }
}
```

After this task, `coreCli` is **removed** (no other usages in the file). The `mgmt` import alias is `mgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"`.

#### `DeleteAllManualBackups` diff

Old (lines ~104–120):
```go
listReq := &core.ListClustersReq{
    Filter: &core.ClusterFilter{
        TenantIds: []uint64{uint64(orgID)},
        Statuses: []core.ClusterStatus{
            core.ClusterStatus_CLUSTER_STATUS_DELETED,
            core.ClusterStatus_CLUSTER_STATUS_DELETECOMPLETE,
        },
        IsDevTier: pointer.To(false),
    },
}
resp, err := s.coreCli.ListClusters(ctx, listReq)
...
for _, cluster := range resp.Clusters {
    err = s.markBackupsAsDeleted(ctx, cluster.Id)
```

New:
```go
// TODO(ideascf): pr #3415 — mgmt.ClusterFilter has no DELETECOMPLETE equivalent;
// Deleted:true covers DELETED state from tidb-management-service perspective.
deletedOnly := true
listReq := &mgmt.ListClustersRequest{
    Filter: &mgmt.ClusterFilter{
        TenantIds: []uint64{uint64(orgID)},
        View:      mgmt.ClusterFilter_CLUSTER_VIEW_BASIC,
        Deleted:   &deletedOnly,
    },
}
resp, err := s.mgmtCli.ListClusters(ctx, listReq)
...
for _, cluster := range resp.GetClusters() {
    err = s.markBackupsAsDeleted(ctx, cluster.GetClusterId())
```

#### `Restore` diff

Old (line ~360):
```go
cluster, err := s.coreCli.GetAvailableClusterByID(ctx, req.TargetClusterId)
...
ClusterConfig: domain.ClusterConfig{
    ClusterID: req.TargetClusterId,
    Cloned:    true,
    Name:      cluster.Name,
    Region:    cluster.RegionName,
},
```

New:
```go
cluster, err := s.mgmtCli.GetDedicatedClusterBasicByID(ctx, req.TargetClusterId)
...
ClusterConfig: domain.ClusterConfig{
    ClusterID: req.TargetClusterId,
    Cloned:    true,
    Name:      cluster.GetClusterName(),
    Region:    cluster.GetRegion(),
},
```

- [x] **Step 1: Replace struct field and constructor in `inner/backup.go`**

Replace the struct and constructor:

```go
type BackupInnerController struct {
	dataflow.UnimplementedBackupInternalServiceServer
	snapshotBackupModel backup.SnapshotBackupModel
	restoreRouter       restore.RestoreModel
	bucketModelProxy    bucket.BucketModelV2
	mgmtCli             pkggrpcli.MgmtSvcGrpcClient
}

func NewBackupInnerController(
	model backup.SnapshotBackupModel,
	bucketModelProxy bucket.BucketModelV2,
	restoreRouter restore.RestoreModel,
	mgmtCli pkggrpcli.MgmtSvcGrpcClient,
) BackupInnerController {
	return BackupInnerController{
		snapshotBackupModel: model,
		bucketModelProxy:    bucketModelProxy,
		restoreRouter:       restoreRouter,
		mgmtCli:             mgmtCli,
	}
}
```

- [x] **Step 2: Fix `DeleteAllManualBackups`**

Find the block containing `listReq := &core.ListClustersReq{` and replace the entire `listReq` + `ListClusters` + loop block:

```go
// TODO(ideascf): pr #3415 — mgmt.ClusterFilter has no DELETECOMPLETE equivalent;
// Deleted:true covers DELETED state from tidb-management-service perspective.
deletedOnly := true
listReq := &mgmt.ListClustersRequest{
    Filter: &mgmt.ClusterFilter{
        TenantIds: []uint64{uint64(orgID)},
        View:      mgmt.ClusterFilter_CLUSTER_VIEW_BASIC,
        Deleted:   &deletedOnly,
    },
}
resp, err := s.mgmtCli.ListClusters(ctx, listReq)
if err != nil {
    logger.Error("Fail to get clusters when DeleteAllManualBackups", zap.Error(err))
    return nil, errors.Annotate(err, "fetch deleted no cleaned up")
}
if len(resp.GetClusters()) == 0 {
    return &emptypb.Empty{}, nil
}
for _, cluster := range resp.GetClusters() {
    err = s.markBackupsAsDeleted(ctx, cluster.GetClusterId())
    if err != nil {
        break
    }
}
```

- [x] **Step 3: Fix `Restore`**

Find `cluster, err := s.coreCli.GetAvailableClusterByID(ctx, req.TargetClusterId)` and replace it and the field accesses:

```go
cluster, err := s.mgmtCli.GetDedicatedClusterBasicByID(ctx, req.TargetClusterId)
if err != nil {
    logger.Error("failed to get target cluster", zap.Error(err), zap.Uint64("clusterID", req.TargetClusterId))
    return nil, bizerr_wrapper.NewInternalErr("failed to get target cluster: %s", err.Error())
}

restoreReq := &domain.CreateRestoreReqV2{
    TenantID:  req.OrgId,
    ProjectID: req.TargetProjectId,
    Mode:      domain.RestoreModeSnapshot,
    BackupID:  req.Id,
    ClusterConfig: domain.ClusterConfig{
        ClusterID: req.TargetClusterId,
        Cloned:    true,
        Name:      cluster.GetClusterName(),
        Region:    cluster.GetRegion(),
    },
    BackupRegion:    req.BackupRegion,
    BRVersion:       req.BrVersion,
    BRImageTag:      req.BrImageTag,
    BRCustomOptions: strings.TrimSpace(req.BrCustomOptions),
}
```

- [x] **Step 4: Update imports in `inner/backup.go`**

Remove the `core-svc` import (no longer needed):
```go
"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
```

Add the `mgmt` import:
```go
mgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
```

Also remove `"github.com/AlekSi/pointer"` if it's no longer used (check if `pointer.To` appears elsewhere in the file after changes).

- [x] **Step 5: Wire `MgmtSvcGrpcClient` in `container.go`**

Find (around line 497):
```go
container.BackupInnerController = inner.NewBackupInnerController(
    container.SnapshotBackupModel,
    container.BucketModelProxy,
    container.RestoreModelRouter,
    container.CoreGrpcClient,
)
```

Replace with:
```go
container.BackupInnerController = inner.NewBackupInnerController(
    container.SnapshotBackupModel,
    container.BucketModelProxy,
    container.RestoreModelRouter,
    container.MgmtSvcGrpcClient,
)
```

- [x] **Step 6: Build to confirm no compile errors**

```bash
cd repos/dataflow-service
go build ./app/controllers/inner/... ./app/container/...
```

Expected: no errors.

- [x] **Step 7: Commit**

```bash
cd repos/dataflow-service
git add app/controllers/inner/backup.go app/container/container.go
git commit -m "fix(inner/backup): replace coreCli with mgmtCli in BackupInnerController

- DeleteAllManualBackups: use mgmt.ListClusters with Deleted:true filter
- Restore: use mgmtCli.GetDedicatedClusterBasicByID for target cluster lookup
- Remove coreCli dependency from BackupInnerController

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
```

---

## Task 4 — Add `DedicatedClusterState2OpenAPIList` to `open/cvt.go`

**Files:**
- Modify: `app/controllers/open/cvt.go`

### Context

`open/restore.go` currently calls `ClusterStatus2OpenAPIList(cluster.Status)` where `cluster` is `*core.Cluster` and `cluster.Status` is `core.ClusterStatus`. After replacing with `*mgmt.Cluster`, the state becomes `globalv1.Cluster_State` via `cluster.GetDedicatedCluster().GetState()`.

Current `cvt.go` imports only `core` and `domain`. We need to add `globalv1`.

State → OpenAPI string mapping (based on existing `ClusterStatus2OpenAPIList` logic and mgmt enum values):

| `globalv1.Cluster_State` | OpenAPI string |
|--------------------------|----------------|
| `Cluster_CREATING` | `"INITIALIZING"` |
| `Cluster_AVAILABLE` | `"NORMAL"` |
| `Cluster_MODIFYING` | `"SCALING"` |
| `Cluster_SCALING` | `"SCALING"` |
| `Cluster_PAUSING` | `"PAUSING"` |
| `Cluster_PAUSED` | `"PAUSED"` |
| `Cluster_RESUMING` | `"RESUMING"` |
| `Cluster_UNAVAILABLE` | `"UNAVAILABLE"` |
| `Cluster_DELETED` | `"DELETED"` |
| default | `""` |

- [x] **Step 1: Add `globalv1` import and `DedicatedClusterState2OpenAPIList` to `cvt.go`**

Add to the import block:
```go
globalv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
```

Add the function at the bottom of the file:
```go
// DedicatedClusterState2OpenAPIList converts a dedicated cluster state (from mgmt.Cluster)
// to the Open API status string used in list/get restore responses.
// This is the mgmt.Cluster equivalent of ClusterStatus2OpenAPIList.
func DedicatedClusterState2OpenAPIList(state globalv1.Cluster_State) string {
	switch state {
	case globalv1.Cluster_CREATING:
		return "INITIALIZING"
	case globalv1.Cluster_AVAILABLE:
		return "NORMAL"
	case globalv1.Cluster_MODIFYING, globalv1.Cluster_SCALING:
		return "SCALING"
	case globalv1.Cluster_PAUSING:
		return "PAUSING"
	case globalv1.Cluster_PAUSED:
		return "PAUSED"
	case globalv1.Cluster_RESUMING:
		return "RESUMING"
	case globalv1.Cluster_UNAVAILABLE:
		return "UNAVAILABLE"
	case globalv1.Cluster_DELETED:
		return "DELETED"
	default:
		return ""
	}
}
```

- [x] **Step 2: Build to confirm no compile errors**

```bash
cd repos/dataflow-service
go build ./app/controllers/open/...
```

Expected: no errors.

- [x] **Step 3: Commit**

```bash
cd repos/dataflow-service
git add app/controllers/open/cvt.go
git commit -m "feat(open/cvt): add DedicatedClusterState2OpenAPIList for mgmt.Cluster state conversion

Maps globalv1.Cluster_State to OpenAPI status strings for restore list/get responses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
```

---

## Task 5 — Replace `coreCli` with `mgmtCli` in `open/restore.go`

**Files:**
- Modify: `app/controllers/open/restore.go`
- Modify: `app/container/container.go`

### Context

Current struct (lines ~30–55):
```go
type restoreControllerImpl struct {
    dp.UnimplementedRestoreServer
    BaseController

    restoreRouter       restore.RestoreModel
    snapshotBackupModel backup.SnapshotBackupModel

    coreCli grpcli.CoreGrpcClient
}

func NewRestoreController(
    trans db.TransProvider,
    restoreRouter restore.RestoreModel,
    snapshotBackupModel backup.SnapshotBackupModel,
    coreCli grpcli.CoreGrpcClient,
) RestoreController {
    return &restoreControllerImpl{
        BaseController: BaseController{trans: trans},
        restoreRouter:       restoreRouter,
        snapshotBackupModel: snapshotBackupModel,
        coreCli:             coreCli,
    }
}
```

Three callsites to migrate:

1. **`queryRestoringClusters`** — returns `map[uint64]*core.Cluster`, calls `c.coreCli.ListClusters`
2. **`GetRestore`** — calls `c.coreCli.GetAnyClusterByID`, accesses `cluster.Name`, `cluster.Status`
3. **`CreateRestore`** — calls `c.coreCli.GetAnyClusterByID`, accesses `cluster.RegionName`

After migration, the `core` import is removed. Add:
```go
mgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
globalv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
```

- [x] **Step 1: Replace struct and constructor**

```go
type restoreControllerImpl struct {
	dp.UnimplementedRestoreServer
	BaseController

	restoreRouter       restore.RestoreModel
	snapshotBackupModel backup.SnapshotBackupModel

	mgmtCli grpcli.MgmtSvcGrpcClient
}

func NewRestoreController(
	trans db.TransProvider,
	restoreRouter restore.RestoreModel,
	snapshotBackupModel backup.SnapshotBackupModel,
	mgmtCli grpcli.MgmtSvcGrpcClient,
) RestoreController {
	return &restoreControllerImpl{
		BaseController:      BaseController{trans: trans},
		restoreRouter:       restoreRouter,
		snapshotBackupModel: snapshotBackupModel,
		mgmtCli:             mgmtCli,
	}
}
```

- [x] **Step 2: Replace `queryRestoringClusters`**

Replace the entire function:

```go
func (c *restoreControllerImpl) queryRestoringClusters(
	ctx context.Context, listResp *domain.ListRestoreRespV2,
) (map[uint64]*mgmt.Cluster, bizerr.BizError) {
	logger := log.GetLogger(ctx)
	clusterIDs := pie.Map(pie.Filter(listResp.Items, func(t domain.ListRestoreRespV2Item) bool {
		return t.RestoreClusterID != 0
	}), func(t domain.ListRestoreRespV2Item) uint64 {
		return t.RestoreClusterID
	})
	if len(clusterIDs) == 0 {
		return nil, nil
	}
	grpcResp, err := c.mgmtCli.ListClusters(ctx, &mgmt.ListClustersRequest{
		Filter: &mgmt.ClusterFilter{
			ClusterIds: clusterIDs,
			View:       mgmt.ClusterFilter_CLUSTER_VIEW_BASIC,
		},
	})
	if err != nil {
		logger.Error("list cluster failed", zap.Error(err))
		return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
	}
	clusterMap := make(map[uint64]*mgmt.Cluster, len(grpcResp.GetClusters()))
	for _, cluster := range grpcResp.GetClusters() {
		clusterMap[cluster.GetClusterId()] = cluster
	}
	return clusterMap, nil
}
```

- [x] **Step 3: Fix `convertToListRestoreResp` caller of `queryRestoringClusters`**

In `convertToListRestoreResp`, the type of `clusterMap` changes automatically when `queryRestoringClusters` is updated. Update the field accesses inside the loop:

```go
// Before:
if cluster, ok := clusterMap[item.RestoreClusterID]; ok {
    newItem.Cluster.Name = cluster.Name
    newItem.Cluster.Status = ClusterStatus2OpenAPIList(cluster.Status)
}

// After:
if cluster, ok := clusterMap[item.RestoreClusterID]; ok {
    newItem.Cluster.Name = cluster.GetClusterName()
    newItem.Cluster.Status = DedicatedClusterState2OpenAPIList(cluster.GetDedicatedCluster().GetState())
}
```

- [x] **Step 4: Fix `GetRestore`**

Find the block:
```go
cluster, err := c.coreCli.GetAnyClusterByID(ctx, queryResp.RestoreClusterID)
if err != nil {
    logger.Error("get cluster failed", zap.Error(err))
    return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
}
resp.Cluster = &dp.ClusterInfoOfRestore{
    Id:     queryResp.RestoreClusterID,
    Name:   cluster.Name,
    Status: ClusterStatus2OpenAPIList(cluster.Status),
}
```

Replace with:
```go
cluster, err := c.mgmtCli.GetAnyDedicatedClusterBasicByID(ctx, queryResp.RestoreClusterID)
if err != nil {
    logger.Error("get cluster failed", zap.Error(err))
    return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
}
resp.Cluster = &dp.ClusterInfoOfRestore{
    Id:     queryResp.RestoreClusterID,
    Name:   cluster.GetClusterName(),
    Status: DedicatedClusterState2OpenAPIList(cluster.GetDedicatedCluster().GetState()),
}
```

- [x] **Step 5: Fix `CreateRestore`**

Find the block:
```go
cluster, err := c.coreCli.GetAnyClusterByID(ctx, queryBackupResp.ClusterID)
if err != nil {
    logger.Error("get cluster failed", zap.Error(err))
    return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
}

user := account.GetUserFromContext(ctx)
createRestoreReq := &domain.CreateRestoreReqV2{
    ...
    ClusterConfig: domain.ClusterConfig{
        Region:       cluster.RegionName,
```

Replace with:
```go
cluster, err := c.mgmtCli.GetAnyDedicatedClusterBasicByID(ctx, queryBackupResp.ClusterID)
if err != nil {
    logger.Error("get cluster failed", zap.Error(err))
    return nil, bizerr_wrapper.NewInternalErr(appconfig.ErrInternalError)
}

user := account.GetUserFromContext(ctx)
createRestoreReq := &domain.CreateRestoreReqV2{
    ...
    ClusterConfig: domain.ClusterConfig{
        Region:       cluster.GetRegion(),
```

- [x] **Step 6: Update imports in `open/restore.go`**

Remove:
```go
"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
```

Add:
```go
mgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
```

(`globalv1` is only needed for the `DedicatedClusterState2OpenAPIList` call return type — but since `DedicatedClusterState2OpenAPIList` is called with `cluster.GetDedicatedCluster().GetState()` which returns `globalv1.Cluster_State`, the compiler handles it without explicit import. Verify after build; if an explicit import is required, add it.)

- [x] **Step 7: Wire `MgmtSvcGrpcClient` in `container.go`**

Find (around line 474):
```go
container.RestoreOpenController = open_api.NewRestoreController(
    container.TransProvider,
    container.RestoreModelRouter,
    container.SnapshotBackupModel,
    container.CoreGrpcClient,
)
```

Replace with:
```go
container.RestoreOpenController = open_api.NewRestoreController(
    container.TransProvider,
    container.RestoreModelRouter,
    container.SnapshotBackupModel,
    container.MgmtSvcGrpcClient,
)
```

- [x] **Step 8: Build to confirm no compile errors**

```bash
cd repos/dataflow-service
go build ./app/controllers/open/... ./app/container/...
```

Expected: no errors.

- [x] **Step 9: Commit**

```bash
cd repos/dataflow-service
git add app/controllers/open/restore.go app/container/container.go
git commit -m "fix(open/restore): replace coreCli with mgmtCli in RestoreController

- queryRestoringClusters: use mgmt.ListClusters (BASIC view) instead of core-svc
- GetRestore/CreateRestore: use mgmtCli.GetAnyDedicatedClusterBasicByID instead of coreCli.GetAnyClusterByID
- Remove core-svc dependency from RestoreController

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
```

---

## Task 6 — Full build and test verification

**Files:** none (verification only)

- [x] **Step 1: Full project build**

```bash
cd repos/dataflow-service
go build ./...
```

Expected: zero compile errors.

- [x] **Step 2: Run tests for affected packages**

```bash
cd repos/dataflow-service
go test ./app/controllers/... ./pkg/grpc/grpcli/... -v -count=1 2>&1 | tail -40
```

Expected: all tests pass (PASS). Any pre-existing failures are not introduced by this change.

- [x] **Step 3: Verify no remaining `coreCli` in backup/restore paths**

```bash
cd repos/dataflow-service
grep -n "coreCli" \
    app/controllers/console/backup.go \
    app/controllers/inner/backup.go \
    app/controllers/open/restore.go
```

Expected: **zero lines** printed (no matches).

- [x] **Step 4: Final commit if any fixups were needed**

If step 1–3 revealed any fixup needed (e.g., a stray import or missed field), apply it and commit:

```bash
cd repos/dataflow-service
git add -p
git commit -m "fix: address build/test issues from full coverage migration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
```

If no fixes needed, skip this step.

- [x] **Step 5: Push branch**

```bash
cd repos/dataflow-service
git push origin fix/crrf-backup-list-mgmt-basic-view
```

---

## Self-Review Checklist

- [x] **Spec coverage**: All 5 callsites from spec are covered: `canClusterVersionEnableLogBackup` (T2), `inner/Restore` (T3), `inner/DeleteAllManualBackups` (T3), `queryRestoringClusters` + `convertToListRestoreResp` (T5), `GetRestore` (T5), `CreateRestore` (T5).
- [x] **Mock regeneration**: Task 1 Step 3 runs `make generate4test`.
- [x] **`GetDedicatedClusterBasicByID` vs `GetAnyDedicatedClusterBasicByID`**: Only non-deleted callers (`canClusterVersionEnableLogBackup`, `inner/Restore`) use `GetDedicatedClusterBasicByID`. Callers that need any-state clusters (`GetRestore`, `CreateRestore`, `queryRestoringClusters`) use `GetAnyDedicatedClusterBasicByID` / `ListClusters` without deleted filter.
- [x] **DELETECOMPLETE TODO**: Task 3 Step 2 adds `TODO(ideascf): pr #3415`.
- [x] **`DedicatedClusterState2OpenAPIList`**: Added in Task 4, used in Task 5 Steps 3 and 4.
- [x] **Type consistency**: `map[uint64]*mgmt.Cluster` used consistently in T5 Steps 2 and 3. All field accessors use getter methods (`GetClusterName()`, `GetRegion()`, `GetDedicatedCluster().GetState()`).
- [x] **container.go wired twice**: Task 3 Step 5 (`BackupInnerController`) and Task 5 Step 7 (`RestoreOpenController`).
- [x] **`coreCli` removed from inner/backup.go**: All three usages (lines 115, 360, and struct) replaced; import removed.
- [x] **`coreCli` removed from open/restore.go**: All four usages (struct + 3 callsites) replaced; `core` import removed.
