API: handler bugs found while aligning the OpenAPI spec

Found while aligning engine/api/swagger-spec/dblab_openapi.yaml with the handlers. None of these is fixed there. Line refs are for 8a42a6ef.

1. Not-found and bad-request errors return 500

api.SendError type-asserts errors.Cause(err).(models.Error) (engine/internal/srv/api/errors.go:23), but models.New returns *models.Error (engine/pkg/models/error.go:28). The assertion fails, so every models.New(ErrCodeNotFound|ErrCodeBadRequest, ...) goes out as 500 INTERNAL_ERROR with the right message.

17 call sites in engine/internal/cloning/base.go and engine/internal/cloning/upgrade.go, reached from DELETE/PATCH /clone/{id}, /clone/{id}/reset and /clone/{id}/upgrade: unknown clone, protected clone, clone being upgraded, clone not started. errors_test.go only covers the value form.

Fix: match both forms with errors.As. Then the two # TODO: fix it in engine (currently returns 500) 404 blocks in the spec can be enabled.

2. Percent-encoded snapshot IDs are rejected with 400

The router uses UseEncodedPath() (engine/internal/srv/server.go:263), so mux.Vars(r)["id"] stays escaped, and no handler unescapes it before snapshotIDRegexp (engine/internal/srv/branch.go:32). An ID sent as pool%2Fbranch%2Fmain%40snap fails validation.

Affected: getSnapshot (branch.go:322), getCommit (branch.go:342), deleteSnapshot (routes.go:220), patchSnapshot (routes.go:896). The engine's own Swagger UI percent-encodes path params, so "Try it out" fails on /snapshot/{id} and /branch/snapshot/{id}. The Go client works only because it sends raw slashes.

Fix: url.PathUnescape the id in these handlers.

3. POST /snapshot returns the pool manager's struct

The handler writes snapshotList[0] (engine/internal/srv/routes.go:206,213), a resources.Snapshot (engine/internal/provision/resources/resources.go:35) with used/logicalReferenced. Every other snapshot endpoint returns models.Snapshot with physicalSize/logicalSize, numClones and the protection fields. The Go client decodes the response into models.Snapshot (engine/pkg/client/dblabapi/snapshot.go:58), so its sizes are always 0.

The spec now documents the actual shape as CreatedSnapshot.

Fix: return the models.Snapshot of the new snapshot, then drop CreatedSnapshot from the spec.

4. PATCH with "deleteAt": "" schedules deletion at 0001-01-01

LocalTime.UnmarshalJSON returns nil on an empty string (engine/pkg/models/local_time.go:29) and leaves a non-nil zero time, so PATCH /snapshot/{id} and PATCH /branch/{branchName} store 0001-01-01T00:00:00Z (engine/internal/srv/routes.go:888). With retention enabled, the sweeper treats it as due (engine/internal/srv/auto_delete.go:413) and deletes an unused, unprotected snapshot or branch on the next tick instead of after the grace period.

The UI and CLI never send an empty deleteAt; a hand-written API request does.

Fix: reject a zero deleteAt with 400, or treat it as "clear the schedule".

5. A branch named snapshot has no reachable log

/branch/snapshot/{id:.*} (engine/internal/srv/server.go:288) is registered before /branch/{branchName}/log (server.go:291), so GET /branch/snapshot/log is routed to getCommit. snapshot passes branchNameRegexp (engine/internal/srv/branch.go:27).

Fix: reserve snapshot as a branch name, or register the log route first.