Repository: cvat-ai/cvat (language: python)

Issue:
`random_per_job` frame selection mode doesn't work on the job creation page
### Actions before raising this issue

- [X] I searched the existing issues and did not find anything similar.
- [X] I read/searched [the docs](https://docs.cvat.ai/docs/)

### Steps to Reproduce

1. Create a task without quality checking configured
2. Open the task, open the job creation dialog (using the + button)
3. Select `random_per_job` frame selection method
4. Try to create a job

### Expected Behavior

Random per job mode is available on the job creation page.

### Possible Solution

_No response_

### Context

Should work similarly to the same option on the task creation page. The absolute number is equal to the percent value * task segment size.

### Environment

_No response_
UI progressbar bug
### Actions before raising this issue

- [X] I searched the existing issues and did not find anything similar.
- [X] I read/searched [the docs](https://docs.cvat.ai/docs/)

### Steps to Reproduce

Start the task creation and then look at "requests" page

### Expected Behavior

Percentage should be the same in tasks page and in requests page

https://github.com/user-attachments/assets/f61fe13b-7f6a-4c17-a2af-1d68deb55f57



### Possible Solution

_No response_

### Context

_No response_

### Environment

```Markdown
app.cvat.ai
```



Discussion / hints:

Hello, thanks for the report. @klakhov will check this. 

Fix patch:
diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml
index f3ec1d48262d..a74f70c54379 100644
--- a/.github/workflows/black.yml
+++ b/.github/workflows/black.yml
@@ -5,38 +5,11 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - id: files
-        uses: tj-actions/changed-files@v41.0.0
-        with:
-          files: |
-              cvat-sdk/**/*.py
-              cvat-cli/**/*.py
-              tests/python/**/*.py
-              cvat/apps/quality_control/**/*.py
-              cvat/apps/analytics_report/**/*.py
-          dir_names: true
 
       - name: Run checks
-        env:
-          PR_FILES_AM: ${{ steps.files.outputs.added_modified }}
-          PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }}
         run: |
-          # If different modules use different Black configs,
-          # we need to run Black for each python component group separately.
-          # Otherwise, they all will use the same config.
+          pipx install $(grep "^black" ./cvat-cli/requirements/development.txt)
 
-          UPDATED_DIRS="${{steps.files.outputs.all_changed_files}}"
+          echo "Black version: $(black --version)"
 
-          if [[ ! -z $UPDATED_DIRS ]]; then
-            pipx install $(egrep "black.*" ./cvat-cli/requirements/development.txt)
-
-            echo "Black version: "$(black --version)
-            echo "The dirs will be checked: $UPDATED_DIRS"
-            EXIT_CODE=0
-            for DIR in $UPDATED_DIRS; do
-              black --check --diff $DIR || EXIT_CODE=$(($? | $EXIT_CODE)) || true
-            done
-            exit $EXIT_CODE
-          else
-            echo "No files with the \"py\" extension found"
-          fi
+          black --check --diff .
diff --git a/.github/workflows/finalize-release.yml b/.github/workflows/finalize-release.yml
index 2cb6035769ae..8f19cb1e9e60 100644
--- a/.github/workflows/finalize-release.yml
+++ b/.github/workflows/finalize-release.yml
@@ -65,7 +65,7 @@ jobs:
 
       - name: Bump version
         run:
-          ./dev/update_version.py --minor
+          ./dev/update_version.py --patch
 
       - name: Commit post-release changes
         run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42d2893080be..dd8854040e02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 <!-- scriv-insert-here -->
 
+<a id='changelog-2.22.0'></a>
+## \[2.22.0\] - 2024-11-11
+
+### Added
+
+- Feature to hide a mask during editing  (<https://github.com/cvat-ai/cvat/pull/8554>)
+
+- A quality setting to compare point groups without using bbox
+  (<https://github.com/cvat-ai/cvat/pull/8634>)
+
+- A quality check option to consider empty frames matching
+  (<https://github.com/cvat-ai/cvat/pull/8652>)
+
+### Changed
+
+- Reduced memory usage of the utils container
+  (<https://github.com/cvat-ai/cvat/pull/8672>)
+
+### Removed
+
+- Removed unused business group
+  (<https://github.com/cvat-ai/cvat/pull/8607>)
+
+### Fixed
+
+- Propagation creates copies on non-existing frames in a ground truth job
+  (<https://github.com/cvat-ai/cvat/pull/8550>)
+
+- Exporting projects with tasks containing honeypots. Honeypots are no longer exported.
+  (<https://github.com/cvat-ai/cvat/pull/8597>)
+
+- Error after creating GT job on Create job page with frame selection method `random_per_job`
+  (<https://github.com/cvat-ai/cvat/pull/8623>)
+
+- Fixed issue 'Cannot read properties of undefined (reading 'push')'
+  (<https://github.com/cvat-ai/cvat/pull/8648>)
+
+- Re-newed import/export request failed immediately if the previous failed
+  (<https://github.com/cvat-ai/cvat/pull/8649>)
+
+- Fixed automatic zooming in attribute annotation mode for masks
+  (<https://github.com/cvat-ai/cvat/pull/8657>)
+
+- Export dataset in CVAT format misses frames in tasks with non-default frame step
+  (<https://github.com/cvat-ai/cvat/pull/8662>)
+
+- Incorrect progress representation on `Requests` page
+  (<https://github.com/cvat-ai/cvat/pull/8668>)
+
 <a id='changelog-2.21.3'></a>
 ## \[2.21.3\] - 2024-10-31
 
diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json
index 2b24ff47e347..c89e7506854c 100644
--- a/cvat-canvas/package.json
+++ b/cvat-canvas/package.json
@@ -1,6 +1,6 @@
 {
   "name": "cvat-canvas",
-  "version": "2.20.9",
+  "version": "2.20.10",
   "type": "module",
   "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
   "main": "src/canvas.ts",
diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts
index 2c7a1f08d203..0ad62484c14c 100644
--- a/cvat-canvas/src/typescript/canvasModel.ts
+++ b/cvat-canvas/src/typescript/canvasModel.ts
@@ -96,6 +96,7 @@ export interface Configuration {
     controlPointsSize?: number;
     outlinedBorders?: string | false;
     resetZoom?: boolean;
+    hideEditedObject?: boolean;
 }
 
 export interface BrushTool {
@@ -416,6 +417,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
                 textPosition: consts.DEFAULT_SHAPE_TEXT_POSITION,
                 textContent: consts.DEFAULT_SHAPE_TEXT_CONTENT,
                 undefinedAttrValue: consts.DEFAULT_UNDEFINED_ATTR_VALUE,
+                hideEditedObject: false,
             },
             imageBitmap: false,
             image: null,
@@ -981,6 +983,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
             this.data.configuration.CSSImageFilter = configuration.CSSImageFilter;
         }
 
+        if (typeof configuration.hideEditedObject === 'boolean') {
+            this.data.configuration.hideEditedObject = configuration.hideEditedObject;
+        }
+
         this.notify(UpdateReasons.CONFIG_UPDATED);
     }
 
diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts
index ab9a96682746..4c346b4d6735 100644
--- a/cvat-canvas/src/typescript/canvasView.ts
+++ b/cvat-canvas/src/typescript/canvasView.ts
@@ -1918,15 +1918,26 @@ export class CanvasViewImpl implements CanvasView, Listener {
             this.gridPattern.setAttribute('height', `${size.height}`);
         } else if (reason === UpdateReasons.SHAPE_FOCUSED) {
             const { padding, clientID } = this.controller.focusData;
+            const drawnState = this.drawnStates[clientID];
             const object = this.svgShapes[clientID];
-            if (object) {
-                const bbox: SVG.BBox = object.bbox();
-                this.onFocusRegion(
-                    bbox.x - padding,
-                    bbox.y - padding,
-                    bbox.width + padding * 2,
-                    bbox.height + padding * 2,
-                );
+            if (drawnState && object) {
+                const { offset } = this.geometry;
+                let [x, y, width, height] = [0, 0, 0, 0];
+
+                if (drawnState.shapeType === 'mask') {
+                    const [xtl, ytl, xbr, ybr] = drawnState.points.slice(-4);
+                    x = xtl + offset;
+                    y = ytl + offset;
+                    width = xbr - xtl + 1;
+                    height = ybr - ytl + 1;
+                } else {
+                    const bbox: SVG.BBox = object.bbox();
+                    ({
+                        x, y, width, height,
+                    } = bbox);
+                }
+
+                this.onFocusRegion(x - padding, y - padding, width + padding * 2, height + padding * 2);
             }
         } else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
             this.activate(this.controller.activeElement);
diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts
index b7e9cbb90130..77b674dec05e 100644
--- a/cvat-canvas/src/typescript/drawHandler.ts
+++ b/cvat-canvas/src/typescript/drawHandler.ts
@@ -5,7 +5,7 @@
 
 import * as SVG from 'svg.js';
 import 'svg.draw.js';
-import './svg.patch';
+import { CIRCLE_STROKE } from './svg.patch';
 
 import { AutoborderHandler } from './autoborderHandler';
 import {
@@ -104,6 +104,7 @@ export class DrawHandlerImpl implements DrawHandler {
     private controlPointsSize: number;
     private selectedShapeOpacity: number;
     private outlinedBorders: string;
+    private isHidden: boolean;
 
     // we should use any instead of SVG.Shape because svg plugins cannot change declared interface
     // so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
@@ -1276,6 +1277,7 @@ export class DrawHandlerImpl implements DrawHandler {
         this.selectedShapeOpacity = configuration.selectedShapeOpacity;
         this.outlinedBorders = configuration.outlinedBorders || 'black';
         this.autobordersEnabled = false;
+        this.isHidden = false;
         this.startTimestamp = Date.now();
         this.onDrawDoneDefault = onDrawDone;
         this.canvas = canvas;
@@ -1301,10 +1303,28 @@ export class DrawHandlerImpl implements DrawHandler {
         });
     }
 
+    private strokePoint(point: SVG.Element): void {
+        point.attr('stroke', this.isHidden ? 'none' : CIRCLE_STROKE);
+        point.fill({ opacity: this.isHidden ? 0 : 1 });
+    }
+
+    private updateHidden(value: boolean) {
+        this.isHidden = value;
+
+        if (value) {
+            this.canvas.attr('pointer-events', 'none');
+        } else {
+            this.canvas.attr('pointer-events', 'all');
+        }
+    }
+
     public configurate(configuration: Configuration): void {
         this.controlPointsSize = configuration.controlPointsSize;
         this.selectedShapeOpacity = configuration.selectedShapeOpacity;
         this.outlinedBorders = configuration.outlinedBorders || 'black';
+        if (this.isHidden !== configuration.hideEditedObject) {
+            this.updateHidden(configuration.hideEditedObject);
+        }
 
         const isFillableRect = this.drawData &&
             this.drawData.shapeType === 'rectangle' &&
@@ -1315,15 +1335,26 @@ export class DrawHandlerImpl implements DrawHandler {
         const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon';
 
         if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) {
-            this.drawInstance.fill({ opacity: configuration.selectedShapeOpacity });
+            this.drawInstance.fill({
+                opacity: configuration.hideEditedObject ? 0 : configuration.selectedShapeOpacity,
+            });
+        }
+
+        if (this.drawInstance && (isFilalblePolygon)) {
+            const paintHandler = this.drawInstance.remember('_paintHandler');
+            if (paintHandler) {
+                for (const point of (paintHandler as any).set.members) {
+                    this.strokePoint(point);
+                }
+            }
         }
 
         if (this.drawInstance && this.drawInstance.attr('stroke')) {
-            this.drawInstance.attr('stroke', this.outlinedBorders);
+            this.drawInstance.attr('stroke', configuration.hideEditedObject ? 'none' : this.outlinedBorders);
         }
 
         if (this.pointsGroup && this.pointsGroup.attr('stroke')) {
-            this.pointsGroup.attr('stroke', this.outlinedBorders);
+            this.pointsGroup.attr('stroke', configuration.hideEditedObject ? 'none' : this.outlinedBorders);
         }
 
         this.autobordersEnabled = configuration.autoborders;
@@ -1369,6 +1400,7 @@ export class DrawHandlerImpl implements DrawHandler {
             const paintHandler = this.drawInstance.remember('_paintHandler');
 
             for (const point of (paintHandler as any).set.members) {
+                this.strokePoint(point);
                 point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
                 point.attr('r', `${this.controlPointsSize / geometry.scale}`);
             }
diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts
index 567eea29c7de..84ecb1684ad4 100644
--- a/cvat-canvas/src/typescript/editHandler.ts
+++ b/cvat-canvas/src/typescript/editHandler.ts
@@ -472,7 +472,7 @@ export class EditHandlerImpl implements EditHandler {
 
             const paintHandler = this.editLine.remember('_paintHandler');
 
-            for (const point of (paintHandler as any).set.members) {
+            for (const point of paintHandler.set.members) {
                 point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
                 point.attr('r', `${this.controlPointsSize / geometry.scale}`);
             }
diff --git a/cvat-canvas/src/typescript/masksHandler.ts b/cvat-canvas/src/typescript/masksHandler.ts
index cdaa4d86d2fa..ca6e5e469a63 100644
--- a/cvat-canvas/src/typescript/masksHandler.ts
+++ b/cvat-canvas/src/typescript/masksHandler.ts
@@ -6,7 +6,7 @@ import { fabric } from 'fabric';
 import debounce from 'lodash/debounce';
 
 import {
-    DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy,
+    DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy, Position,
 } from './canvasModel';
 import consts from './consts';
 import { DrawHandler } from './drawHandler';
@@ -61,10 +61,11 @@ export class MasksHandlerImpl implements MasksHandler {
     private editData: MasksEditData | null;
 
     private colorBy: ColorBy;
-    private latestMousePos: { x: number; y: number; };
+    private latestMousePos: Position;
     private startTimestamp: number;
     private geometry: Geometry;
     private drawingOpacity: number;
+    private isHidden: boolean;
 
     private keepDrawnPolygon(): void {
         const canvasWrapper = this.canvas.getElement().parentElement;
@@ -217,12 +218,29 @@ export class MasksHandlerImpl implements MasksHandler {
     private imageDataFromCanvas(wrappingBBox: WrappingBBox): Uint8ClampedArray {
         const imageData = this.canvas.toCanvasElement()
             .getContext('2d').getImageData(
-                wrappingBBox.left, wrappingBBox.top,
-                wrappingBBox.right - wrappingBBox.left + 1, wrappingBBox.bottom - wrappingBBox.top + 1,
+                wrappingBBox.left,
+                wrappingBBox.top,
+                wrappingBBox.right - wrappingBBox.left + 1,
+                wrappingBBox.bottom - wrappingBBox.top + 1,
             ).data;
         return imageData;
     }
 
+    private updateHidden(value: boolean) {
+        this.isHidden = value;
+
+        // Need to update style of upper canvas explicitly because update of default cursor is not applied immediately
+        // https://github.com/fabricjs/fabric.js/issues/1456
+        const newOpacity = value ? '0' : '';
+        const newCursor = value ? 'inherit' : 'none';
+        this.canvas.getElement().parentElement.style.opacity = newOpacity;
+        const upperCanvas = this.canvas.getElement().parentElement.querySelector('.upper-canvas') as HTMLElement;
+        if (upperCanvas) {
+            upperCanvas.style.cursor = newCursor;
+        }
+        this.canvas.defaultCursor = newCursor;
+    }
+
     private updateBrushTools(brushTool?: BrushTool, opts: Partial<BrushTool> = {}): void {
         if (this.isPolygonDrawing) {
             // tool was switched from polygon to brush for example
@@ -350,6 +368,7 @@ export class MasksHandlerImpl implements MasksHandler {
         this.editData = null;
         this.drawingOpacity = 0.5;
         this.brushMarker = null;
+        this.isHidden = false;
         this.colorBy = ColorBy.LABEL;
         this.onDrawDone = onDrawDone;
         this.onDrawRepeat = onDrawRepeat;
@@ -452,7 +471,7 @@ export class MasksHandlerImpl implements MasksHandler {
                 this.canvas.renderAll();
             }
 
-            if (isMouseDown && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) {
+            if (isMouseDown && !this.isHidden && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) {
                 const color = fabric.Color.fromHex(tool.color);
                 color.setAlpha(tool.type === 'eraser' ? 1 : 0.5);
 
@@ -530,6 +549,10 @@ export class MasksHandlerImpl implements MasksHandler {
 
     public configurate(configuration: Configuration): void {
         this.colorBy = configuration.colorBy;
+
+        if (this.isHidden !== configuration.hideEditedObject) {
+            this.updateHidden(configuration.hideEditedObject);
+        }
     }
 
     public transform(geometry: Geometry): void {
@@ -563,7 +586,10 @@ export class MasksHandlerImpl implements MasksHandler {
                 const color = fabric.Color.fromHex(this.getStateColor(drawData.initialState)).getSource();
                 const [left, top, right, bottom] = points.slice(-4);
                 const imageBitmap = expandChannels(color[0], color[1], color[2], points);
-                imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1,
+                imageDataToDataURL(
+                    imageBitmap,
+                    right - left + 1,
+                    bottom - top + 1,
                     (dataURL: string) => new Promise((resolve) => {
                         fabric.Image.fromURL(dataURL, (image: fabric.Image) => {
                             try {
@@ -654,7 +680,10 @@ export class MasksHandlerImpl implements MasksHandler {
                 const color = fabric.Color.fromHex(this.getStateColor(editData.state)).getSource();
                 const [left, top, right, bottom] = points.slice(-4);
                 const imageBitmap = expandChannels(color[0], color[1], color[2], points);
-                imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1,
+                imageDataToDataURL(
+                    imageBitmap,
+                    right - left + 1,
+                    bottom - top + 1,
                     (dataURL: string) => new Promise((resolve) => {
                         fabric.Image.fromURL(dataURL, (image: fabric.Image) => {
                             try {
diff --git a/cvat-canvas/src/typescript/svg.patch.ts b/cvat-canvas/src/typescript/svg.patch.ts
index 40af155a956f..7b728b274335 100644
--- a/cvat-canvas/src/typescript/svg.patch.ts
+++ b/cvat-canvas/src/typescript/svg.patch.ts
@@ -86,6 +86,7 @@ SVG.Element.prototype.draw.extend(
     }),
 );
 
+export const CIRCLE_STROKE = '#000';
 // Fix method drawCircles
 function drawCircles(): void {
     const array = this.el.array().valueOf();
@@ -109,6 +110,7 @@ function drawCircles(): void {
                 .circle(5)
                 .stroke({
                     width: 1,
+                    color: CIRCLE_STROKE,
                 })
                 .fill('#ccc')
                 .center(p.x, p.y),
diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt
index 063da9c864b4..e9be53974d91 100644
--- a/cvat-cli/requirements/base.txt
+++ b/cvat-cli/requirements/base.txt
@@ -1,3 +1,3 @@
-cvat-sdk~=2.21.3
+cvat-sdk~=2.22.0
 Pillow>=10.3.0
 setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability
diff --git a/cvat-cli/src/cvat_cli/__main__.py b/cvat-cli/src/cvat_cli/__main__.py
index b18c8d8bb751..3d89935fc2d0 100755
--- a/cvat-cli/src/cvat_cli/__main__.py
+++ b/cvat-cli/src/cvat_cli/__main__.py
@@ -1,79 +1,37 @@
 # Copyright (C) 2020-2022 Intel Corporation
-# Copyright (C) 2022 CVAT.ai Corporation
+# Copyright (C) 2022-2024 CVAT.ai Corporation
 #
 # SPDX-License-Identifier: MIT
 
+import argparse
 import logging
 import sys
-from http.client import HTTPConnection
-from types import SimpleNamespace
-from typing import List
 
 import urllib3.exceptions
 from cvat_sdk import exceptions
-from cvat_sdk.core.client import Client, Config
 
-from cvat_cli.cli import CLI
-from cvat_cli.parser import get_action_args, make_cmdline_parser
+from ._internal.commands import COMMANDS
+from ._internal.common import build_client, configure_common_arguments, configure_logger
+from ._internal.utils import popattr
 
 logger = logging.getLogger(__name__)
 
 
-def configure_logger(level):
-    formatter = logging.Formatter(
-        "[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", style="%"
-    )
-    handler = logging.StreamHandler(sys.stdout)
-    handler.setFormatter(formatter)
-    logger.addHandler(handler)
-    logger.setLevel(level)
-    if level <= logging.DEBUG:
-        HTTPConnection.debuglevel = 1
+def main(args: list[str] = None):
+    parser = argparse.ArgumentParser(description=COMMANDS.description)
+    configure_common_arguments(parser)
+    COMMANDS.configure_parser(parser)
 
-
-def build_client(parsed_args: SimpleNamespace, logger: logging.Logger) -> Client:
-    config = Config(verify_ssl=not parsed_args.insecure)
-
-    url = parsed_args.server_host
-    if parsed_args.server_port:
-        url += f":{parsed_args.server_port}"
-
-    client = Client(
-        url=url,
-        logger=logger,
-        config=config,
-        check_server_version=False,  # version is checked after auth to support versions < 2.3
-    )
-
-    client.organization_slug = parsed_args.organization
-
-    return client
-
-
-def main(args: List[str] = None):
-    actions = {
-        "create": CLI.tasks_create,
-        "delete": CLI.tasks_delete,
-        "ls": CLI.tasks_list,
-        "frames": CLI.tasks_frames,
-        "dump": CLI.tasks_dump,
-        "upload": CLI.tasks_upload,
-        "export": CLI.tasks_export,
-        "import": CLI.tasks_import,
-        "auto-annotate": CLI.tasks_auto_annotate,
-    }
-    parser = make_cmdline_parser()
     parsed_args = parser.parse_args(args)
-    configure_logger(parsed_args.loglevel)
 
-    with build_client(parsed_args, logger=logger) as client:
-        action_args = get_action_args(parser, parsed_args)
-        try:
-            cli = CLI(client=client, credentials=parsed_args.auth)
-            actions[parsed_args.action](cli, **vars(action_args))
-        except (exceptions.ApiException, urllib3.exceptions.HTTPError) as e:
-            logger.critical(e)
-            return 1
+    configure_logger(logger, parsed_args)
+
+    try:
+        with build_client(parsed_args, logger=logger) as client:
+            popattr(parsed_args, "_executor")(client, **vars(parsed_args))
+    except (exceptions.ApiException, urllib3.exceptions.HTTPError) as e:
+        logger.critical(e)
+        return 1
 
     return 0
 
diff --git a/cvat-cli/src/cvat_cli/_internal/__init__.py b/cvat-cli/src/cvat_cli/_internal/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/cvat-cli/src/cvat_cli/_internal/command_base.py b/cvat-cli/src/cvat_cli/_internal/command_base.py
new file mode 100644
index 000000000000..ec6ccbbcd47f
--- /dev/null
+++ b/cvat-cli/src/cvat_cli/_internal/command_base.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2024 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import argparse
+import types
+from collections.abc import Mapping
+from typing import Callable, Protocol
+
+
+class Command(Protocol):
+    @property
+    def description(self) -> str: ...
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None: ...
+
+    # The exact parameters accepted by `execute` vary between commands,
+    # so we're forced to declare it like this instead of as a method.
+    @property
+    def execute(self) -> Callable[..., None]: ...
+
+
+class CommandGroup:
+    def __init__(self, *, description: str) -> None:
+        self._commands: dict[str, Command] = {}
+        self.description = description
+
+    def command_class(self, name: str):
+        def decorator(cls: type):
+            self._commands[name] = cls()
+            return cls
+
+        return decorator
+
+    def add_command(self, name: str, command: Command) -> None:
+        self._commands[name] = command
+
+    @property
+    def commands(self) -> Mapping[str, Command]:
+        return types.MappingProxyType(self._commands)
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        subparsers = parser.add_subparsers(required=True)
+
+        for name, command in self._commands.items():
+            subparser = subparsers.add_parser(name, description=command.description)
+            subparser.set_defaults(_executor=command.execute)
+            command.configure_parser(subparser)
+
+    def execute(self) -> None:
+        # It should be impossible for a command group to be executed,
+        # because configure_parser requires that a subcommand is specified.
+        assert False, "unreachable code"
diff --git a/cvat-cli/src/cvat_cli/_internal/commands.py b/cvat-cli/src/cvat_cli/_internal/commands.py
new file mode 100644
index 000000000000..e86ef3b6350f
--- /dev/null
+++ b/cvat-cli/src/cvat_cli/_internal/commands.py
@@ -0,0 +1,500 @@
+# Copyright (C) 2022-2024 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
+
+from __future__ import annotations
+
+import argparse
+import importlib
+import importlib.util
+import json
+import textwrap
+from collections.abc import Sequence
+from pathlib import Path
+from typing import Any, Optional
+
+import cvat_sdk.auto_annotation as cvataa
+from attr.converters import to_bool
+from cvat_sdk import Client, models
+from cvat_sdk.core.helpers import DeferredTqdmProgressReporter
+from cvat_sdk.core.proxies.tasks import ResourceType
+
+from .command_base import CommandGroup
+from .parsers import BuildDictAction, parse_function_parameter, parse_label_arg, parse_resource_type
+
+COMMANDS = CommandGroup(description="Perform common operations related to CVAT tasks.")
+
+
+@COMMANDS.command_class("ls")
+class TaskList:
+    description = "List all CVAT tasks in either basic or JSON format."
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument(
+            "--json",
+            dest="use_json_output",
+            default=False,
+            action="store_true",
+            help="output JSON data",
+        )
+
+    def execute(self, client: Client, *, use_json_output: bool = False):
+        results = client.tasks.list(return_json=use_json_output)
+        if use_json_output:
+            print(json.dumps(json.loads(results), indent=2))
+        else:
+            for r in results:
+                print(r.id)
+
+
+@COMMANDS.command_class("create")
+class TaskCreate:
+    description = textwrap.dedent(
+        """\
+        Create a new CVAT task. To create a task, you need
+        to specify labels using the --labels argument or
+        attach the task to an existing project using the
+        --project_id argument.
+        """
+    )
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("name", type=str, help="name of the task")
+        parser.add_argument(
+            "resource_type",
+            default="local",
+            choices=list(ResourceType),
+            type=parse_resource_type,
+            help="type of files specified",
+        )
+        parser.add_argument("resources", type=str, help="list of paths or URLs", nargs="+")
+        parser.add_argument(
+            "--annotation_path", default="", type=str, help="path to annotation file"
+        )
+        parser.add_argument(
+            "--annotation_format",
+            default="CVAT 1.1",
+            type=str,
+            help="format of the annotation file being uploaded, e.g. CVAT 1.1",
+        )
+        parser.add_argument(
+            "--bug_tracker", "--bug", default=argparse.SUPPRESS, type=str, help="bug tracker URL"
+        )
+        parser.add_argument(
+            "--chunk_size",
+            default=argparse.SUPPRESS,
+            type=int,
+            help="the number of frames per chunk",
+        )
+        parser.add_argument(
+            "--completion_verification_period",
+            dest="status_check_period",
+            default=2,
+            type=float,
+            help=textwrap.dedent(
+                """\
+                number of seconds to wait until checking
+                if data compression finished (necessary before uploading annotations)
+                """
+            ),
+        )
+        parser.add_argument(
+            "--copy_data",
+            default=False,
+            action="store_true",
+            help=textwrap.dedent(
+                """\
+                set the option to copy the data, only used when resource type is
+                share (default: %(default)s)
+                """
+            ),
+        )
+        parser.add_argument(
+            "--frame_step",
+            default=argparse.SUPPRESS,
+            type=int,
+            help=textwrap.dedent(
+                """\
+                set the frame step option in the advanced configuration
+                when uploading image series or videos
+                """
+            ),
+        )
+        parser.add_argument(
+            "--image_quality",
+            default=70,
+            type=int,
+            help=textwrap.dedent(
+                """\
+                set the image quality option in the advanced configuration
+                when creating tasks.(default: %(default)s)
+                """
+            ),
+        )
+        parser.add_argument(
+            "--labels",
+            default="[]",
+            type=parse_label_arg,
+            help="string or file containing JSON labels specification",
+        )
+        parser.add_argument(
+            "--project_id", default=argparse.SUPPRESS, type=int, help="project ID if project exists"
+        )
+        parser.add_argument(
+            "--overlap",
+            default=argparse.SUPPRESS,
+            type=int,
+            help="the number of intersected frames between different segments",
+        )
+        parser.add_argument(
+            "--segment_size",
+            default=argparse.SUPPRESS,
+            type=int,
+            help="the number of frames in a segment",
+        )
+        parser.add_argument(
+            "--sorting-method",
+            default="lexicographical",
+            choices=["lexicographical", "natural", "predefined", "random"],
+            help="""data soring method (default: %(default)s)""",
+        )
+        parser.add_argument(
+            "--start_frame",
+            default=argparse.SUPPRESS,
+            type=int,
+            help="the start frame of the video",
+        )
+        parser.add_argument(
+            "--stop_frame", default=argparse.SUPPRESS, type=int, help="the stop frame of the video"
+        )
+        parser.add_argument(
+            "--use_cache",
+            action="store_true",
+            help="""use cache""",  # automatically sets default=False
+        )
+        parser.add_argument(
+            "--use_zip_chunks",
+            action="store_true",  # automatically sets default=False
+            help="""zip chunks before sending them to the server""",
+        )
+        parser.add_argument(
+            "--cloud_storage_id",
+            default=argparse.SUPPRESS,
+            type=int,
+            help="cloud storage ID if you would like to use data from cloud storage",
+        )
+        parser.add_argument(
+            "--filename_pattern",
+            default=argparse.SUPPRESS,
+            type=str,
+            help=textwrap.dedent(
+                """\
+                pattern for filtering data from the manifest file for the upload.
+                Only shell-style wildcards are supported:
+                * - matches everything;
+                ? - matches any single character;
+                [seq] - matches any character in 'seq';
+                [!seq] - matches any character not in seq
+                """
+            ),
+        )
+
+    def execute(
+        self,
+        client,
+        *,
+        name: str,
+        labels: list[dict[str, str]],
+        resources: Sequence[str],
+        resource_type: ResourceType,
+        annotation_path: str,
+        annotation_format: str,
+        status_check_period: int,
+        **kwargs,
+    ) -> None:
+        task_params = {}
+        data_params = {}
+
+        for k, v in kwargs.items():
+            if k in models.DataRequest.attribute_map or k == "frame_step":
+                data_params[k] = v
+            else:
+                task_params[k] = v
+
+        task = client.tasks.create_from_data(
+            spec=models.TaskWriteRequest(name=name, labels=labels, **task_params),
+            resource_type=resource_type,
+            resources=resources,
+            data_params=data_params,
+            annotation_path=annotation_path,
+            annotation_format=annotation_format,
+            status_check_period=status_check_period,
+            pbar=DeferredTqdmProgressReporter(),
+        )
+        print("Created task id", task.id)
+
+
+@COMMANDS.command_class("delete")
+class TaskDelete:
+    description = "Delete a list of tasks, ignoring those which don't exist."
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("task_ids", type=int, help="list of task IDs", nargs="+")
+
+    def execute(self, client: Client, *, task_ids: Sequence[int]) -> None:
+        client.tasks.remove_by_ids(task_ids=task_ids)
+
+
+@COMMANDS.command_class("frames")
+class TaskFrames:
+    description = textwrap.dedent(
+        """\
+        Download the requested frame numbers for a task and save images as
+        task_<ID>_frame_<FRAME>.jpg.
+        """
+    )
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("task_id", type=int, help="task ID")
+        parser.add_argument("frame_ids", type=int, help="list of frame IDs to download", nargs="+")
+        parser.add_argument(
+            "--outdir", type=str, default="", help="directory to save images (default: CWD)"
+        )
+        parser.add_argument(
+            "--quality",
+            type=str,
+            choices=("original", "compressed"),
+            default="original",
+            help="choose quality of images (default: %(default)s)",
+        )
+
+    def execute(
+        self,
+        client: Client,
+        *,
+        task_id: int,
+        frame_ids: Sequence[int],
+        outdir: str,
+        quality: str,
+    ) -> None:
+        client.tasks.retrieve(obj_id=task_id).download_frames(
+            frame_ids=frame_ids,
+            outdir=outdir,
+            quality=quality,
+            filename_pattern=f"task_{task_id}" + "_frame_{frame_id:06d}{frame_ext}",
+        )
+
+
+@COMMANDS.command_class("dump")
+class TaskDump:
+    description = textwrap.dedent(
+        """\
+        Download annotations for a task in the specified format (e.g. 'YOLO ZIP 1.0').
+        """
+    )
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("task_id", type=int, help="task ID")
+        parser.add_argument("filename", type=str, help="output file")
+        parser.add_argument(
+            "--format",
+            dest="fileformat",
+            type=str,
+            default="CVAT for images 1.1",
+            help="annotation format (default: %(default)s)",
+        )
+        parser.add_argument(
+            "--completion_verification_period",
+            dest="status_check_period",
+            default=2,
+            type=float,
+            help="number of seconds to wait until checking if dataset building finished",
+        )
+        parser.add_argument(
+            "--with-images",
+            type=to_bool,
+            default=False,
+            dest="include_images",
+            help="Whether to include images or not (default: %(default)s)",
+        )
+
+    def execute(
+        self,
+        client: Client,
+        *,
+        task_id: int,
+        fileformat: str,
+        filename: str,
+        status_check_period: int,
+        include_images: bool,
+    ) -> None:
+        client.tasks.retrieve(obj_id=task_id).export_dataset(
+            format_name=fileformat,
+            filename=filename,
+            pbar=DeferredTqdmProgressReporter(),
+            status_check_period=status_check_period,
+            include_images=include_images,
+        )
+
+
+@COMMANDS.command_class("upload")
+class TaskUpload:
+    description = textwrap.dedent(
+        """\
+        Upload annotations for a task in the specified format
+        (e.g. 'YOLO ZIP 1.0').
+        """
+    )
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("task_id", type=int, help="task ID")
+        parser.add_argument("filename", type=str, help="upload file")
+        parser.add_argument(
+            "--format",
+            dest="fileformat",
+            type=str,
+            default="CVAT 1.1",
+            help="annotation format (default: %(default)s)",
+        )
+
+    def execute(
+        self,
+        client: Client,
+        *,
+        task_id: int,
+        fileformat: str,
+        filename: str,
+    ) -> None:
+        client.tasks.retrieve(obj_id=task_id).import_annotations(
+            format_name=fileformat,
+            filename=filename,
+            pbar=DeferredTqdmProgressReporter(),
+        )
+
+
+@COMMANDS.command_class("export")
+class TaskExport:
+    description = """Download a task backup."""
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("task_id", type=int, help="task ID")
+        parser.add_argument("filename", type=str, help="output file")
+        parser.add_argument(
+            "--completion_verification_period",
+            dest="status_check_period",
+            default=2,
+            type=float,
+            help="time interval between checks if archive building has been finished, in seconds",
+        )
+
+    def execute(
+        self, client: Client, *, task_id: int, filename: str, status_check_period: int
+    ) -> None:
+        client.tasks.retrieve(obj_id=task_id).download_backup(
+            filename=filename,
+            status_check_period=status_check_period,
+            pbar=DeferredTqdmProgressReporter(),
+        )
+
+
+@COMMANDS.command_class("import")
+class TaskImport:
+    description = """Import a task from a backup file."""
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("filename", type=str, help="upload file")
+        parser.add_argument(
+            "--completion_verification_period",
+            dest="status_check_period",
+            default=2,
+            type=float,
+            help="time interval between checks if archive processing was finished, in seconds",
+        )
+
+    def execute(self, client: Client, *, filename: str, status_check_period: int) -> None:
+        client.tasks.create_from_backup(
+            filename=filename,
+            status_check_period=status_check_period,
+            pbar=DeferredTqdmProgressReporter(),
+        )
+
+
+@COMMANDS.command_class("auto-annotate")
+class TaskAutoAnnotate:
+    description = "Automatically annotate a CVAT task by running a function on the local machine."
+
+    def configure_parser(self, parser: argparse.ArgumentParser) -> None:
+        parser.add_argument("task_id", type=int, help="task ID")
+
+        function_group = parser.add_mutually_exclusive_group(required=True)
+
+        function_group.add_argument(
+            "--function-module",
+            metavar="MODULE",
+            help="qualified name of a module to use as the function",
+        )
+
+        function_group.add_argument(
+            "--function-file",
+            metavar="PATH",
+            type=Path,
+            help="path to a Python source file to use as the function",
+        )
+
+        parser.add_argument(
+            "--function-parameter",
+            "-p",
+            metavar="NAME=TYPE:VALUE",
+            type=parse_function_parameter,
+            action=BuildDictAction,
+            dest="function_parameters",
+            help="parameter for the function",
+        )
+
+        parser.add_argument(
+            "--clear-existing",
+            action="store_true",
+            help="Remove existing annotations from the task",
+        )
+
+        parser.add_argument(
+            "--allow-unmatched-labels",
+            action="store_true",
+            help="Allow the function to declare labels not configured in the task",
+        )
+
+    def execute(
+        self,
+        client: Client,
+        *,
+        task_id: int,
+        function_module: Optional[str] = None,
+        function_file: Optional[Path] = None,
+        function_parameters: dict[str, Any],
+        clear_existing: bool = False,
+        allow_unmatched_labels: bool = False,
+    ) -> None:
+        if function_module is not None:
+            function = importlib.import_module(function_module)
+        elif function_file is not None:
+            module_spec = importlib.util.spec_from_file_location("__cvat_function__", function_file)
+            function = importlib.util.module_from_spec(module_spec)
+            module_spec.loader.exec_module(function)
+        else:
+            assert False, "function identification arguments missing"
+
+        if hasattr(function, "create"):
+            # this is actually a function factory
+            function = function.create(**function_parameters)
+        else:
+            if function_parameters:
+                raise TypeError("function takes no parameters")
+
+        cvataa.annotate_task(
+            client,
+            task_id,
+            function,
+            pbar=DeferredTqdmProgressReporter(),
+            clear_existing=clear_existing,
+            allow_unmatched_labels=allow_unmatched_labels,
+        )
diff --git a/cvat-cli/src/cvat_cli/_internal/common.py b/cvat-cli/src/cvat_cli/_internal/common.py
new file mode 100644
index 000000000000..415a1340958e
--- /dev/null
+++ b/cvat-cli/src/cvat_cli/_internal/common.py
@@ -0,0 +1,104 @@
+# Copyright (C) 2021-2022 Intel Corporation
+# Copyright (C) 2022-2024 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import argparse
+import getpass
+import logging
+import os
+import sys
+from http.client import HTTPConnection
+
+from cvat_sdk.core.client import Client, Config
+
+from ..version import VERSION
+from .utils import popattr
+
+
+def get_auth(s):
+    """Parse USER[:PASS] strings and prompt for password if none was
+    supplied."""
+    user, _, password = s.partition(":")
+    password = password or os.environ.get("PASS") or getpass.getpass()
+    return user, password
+
+
+def configure_common_arguments(parser: argparse.ArgumentParser) -> None:
+    parser.add_argument("--version", action="version", version=VERSION)
+    parser.add_argument(
+        "--insecure",
+        action="store_true",
+        help="Allows to disable SSL certificate check",
+    )
+
+    parser.add_argument(
+        "--auth",
+        type=get_auth,
+        metavar="USER:[PASS]",
+        default=getpass.getuser(),
+        help="""defaults to the current user and supports the PASS
+                environment variable or password prompt
+                (default user: %(default)s).""",
+    )
+    parser.add_argument(
+        "--server-host", type=str, default="localhost", help="host (default: %(default)s)"
+    )
+    parser.add_argument(
+        "--server-port",
+        type=int,
+        default=None,
+        help="port (default: 80 for http and 443 for https connections)",
+    )
+    parser.add_argument(
+        "--organization",
+        "--org",
+        metavar="SLUG",
+        help="""short name (slug) of the organization
+                to use when listing or creating resources;
+                set to blank string to use the personal workspace
+                (default: list all accessible objects, create in personal workspace)""",
+    )
+    parser.add_argument(
+        "--debug",
+        action="store_const",
+        dest="loglevel",
+        const=logging.DEBUG,
+        default=logging.INFO,
+        help="show debug output",
+    )
+
+
+def configure_logger(logger: logging.Logger, parsed_args: argparse.Namespace) -> None:
+    level = popattr(parsed_args, "loglevel")
+    formatter = logging.Formatter(
+        "[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", style="%"
+    )
+    handler = logging.StreamHandler(sys.stdout)
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+    logger.setLevel(level)
+    if level <= logging.DEBUG:
+        HTTPConnection.debuglevel = 1
+
+
+def build_client(parsed_args: argparse.Namespace, logger: logging.Logger) -> Client:
+    config = Config(verify_ssl=not popattr(parsed_args, "insecure"))
+
+    url = popattr(parsed_args, "server_host")
+    if server_port := popattr(parsed_args, "server_port"):
+        url += f":{server_port}"
+
+    client = Client(
+        url=url,
+        logger=logger,
+        config=config,
+        check_server_version=False,  # version is checked after auth to support versions < 2.3
+    )
+
+    client.login(popattr(parsed_args, "auth"))
+    client.check_server_version(fail_if_unsupported=False)
+
+    client.organization_slug = popattr(parsed_args, "organization")
+
+    return client
diff --git a/cvat-cli/src/cvat_cli/_internal/parsers.py b/cvat-cli/src/cvat_cli/_internal/parsers.py
new file mode 100644
index 000000000000..a66710a09f47
--- /dev/null
+++ b/cvat-cli/src/cvat_cli/_internal/parsers.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2021-2022 Intel Corporation
+# Copyright (C) 2022-2024 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import argparse
+import json
+import os.path
+from typing import Any
+
+from attr.converters import to_bool
+from cvat_sdk.core.proxies.tasks import ResourceType
+
+
+def parse_resource_type(s: str) -> ResourceType:
+    try:
+        return ResourceType[s.upper()]
+    except KeyError:
+        return s
+
+
+def parse_label_arg(s):
+    """If s is a file load it as JSON, otherwise parse s as JSON."""
+    if os.path.exists(s):
+        with open(s, "r") as fp:
+            return json.load(fp)
+    else:
+        return json.loads(s)
+
+
+def parse_function_parameter(s: str) -> tuple[str, Any]:
+    key, sep, type_and_value = s.partition("=")
+
+    if not sep:
+        raise argparse.ArgumentTypeError("parameter value not specified")
+
+    type_, sep, value = type_and_value.partition(":")
+
+    if not sep:
+        raise argparse.ArgumentTypeError("parameter type not specified")
+
+    if type_ == "int":
+        value = int(value)
+    elif type_ == "float":
+        value = float(value)
+    elif type_ == "str":
+        pass
+    elif type_ == "bool":
+        value = to_bool(value)
+    else:
+        raise argparse.ArgumentTypeError(f"unsupported parameter type {type_!r}")
+
+    return (key, value)
+
+
+class BuildDictAction(argparse.Action):
+    def __init__(self, option_strings, dest, default=None, **kwargs):
+        super().__init__(option_strings, dest, default=default or {}, **kwargs)
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        key, value = values
+        getattr(namespace, self.dest)[key] = value
diff --git a/cvat-cli/src/cvat_cli/_internal/utils.py b/cvat-cli/src/cvat_cli/_internal/utils.py
new file mode 100644
index 000000000000..b541534790c4
--- /dev/null
+++ b/cvat-cli/src/cvat_cli/_internal/utils.py
@@ -0,0 +1,9 @@
+# Copyright (C) 2024 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
+
+
+def popattr(obj, name):
+    value = getattr(obj, name)
+    delattr(obj, name)
+    return value
diff --git a/cvat-cli/src/cvat_cli/cli.py b/cvat-cli/src/cvat_cli/cli.py
deleted file mode 100644
index e7945b18bb2e..000000000000
--- a/cvat-cli/src/cvat_cli/cli.py
+++ /dev/null
@@ -1,177 +0,0 @@
-# Copyright (C) 2022 CVAT.ai Corporation
-#
-# SPDX-License-Identifier: MIT
-
-from __future__ import annotations
-
-import importlib
-import importlib.util
-import json
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Sequence, Tuple
-
-import cvat_sdk.auto_annotation as cvataa
-from cvat_sdk import Client, models
-from cvat_sdk.core.helpers import DeferredTqdmProgressReporter
-from cvat_sdk.core.proxies.tasks import ResourceType
-
-
-class CLI:
-    def __init__(self, client: Client, credentials: Tuple[str, str]):
-        self.client = client
-
-        self.client.login(credentials)
-
-        self.client.check_server_version(fail_if_unsupported=False)
-
-    def tasks_list(self, *, use_json_output: bool = False, **kwargs):
-        """List all tasks in either basic or JSON format."""
-        results = self.client.tasks.list(return_json=use_json_output, **kwargs)
-        if use_json_output:
-            print(json.dumps(json.loads(results), indent=2))
-        else:
-            for r in results:
-                print(r.id)
-
-    def tasks_create(
-        self,
-        name: str,
-        labels: List[Dict[str, str]],
-        resources: Sequence[str],
-        *,
-        resource_type: ResourceType = ResourceType.LOCAL,
-        annotation_path: str = "",
-        annotation_format: str = "CVAT XML 1.1",
-        status_check_period: int = 2,
-        **kwargs,
-    ) -> None:
-        """
-        Create a new task with the given name and labels JSON and add the files to it.
-        """
-
-        task_params = {}
-        data_params = {}
-
-        for k, v in kwargs.items():
-            if k in models.DataRequest.attribute_map or k == "frame_step":
-                data_params[k] = v
-            else:
-                task_params[k] = v
-
-        task = self.client.tasks.create_from_data(
-            spec=models.TaskWriteRequest(name=name, labels=labels, **task_params),
-            resource_type=resource_type,
-            resources=resources,
-            data_params=data_params,
-            annotation_path=annotation_path,
-            annotation_format=annotation_format,
-            status_check_period=status_check_period,
-            pbar=DeferredTqdmProgressReporter(),
-        )
-        print("Created task id", task.id)
-
-    def tasks_delete(self, task_ids: Sequence[int]) -> None:
-        """Delete a list of tasks, ignoring those which don't exist."""
-        self.client.tasks.remove_by_ids(task_ids=task_ids)
-
-    def tasks_frames(
-        self,
-        task_id: int,
-        frame_ids: Sequence[int],
-        *,
-        outdir: str = "",
-        quality: str = "original",
-    ) -> None:
-        """
-        Download the requested frame numbers for a task and save images as
-        task_<ID>_frame_<FRAME>.jpg.
-        """
-        self.client.tasks.retrieve(obj_id=task_id).download_frames(
-            frame_ids=frame_ids,
-            outdir=outdir,
-            quality=quality,
-            filename_pattern=f"task_{task_id}" + "_frame_{frame_id:06d}{frame_ext}",
-        )
-
-    def tasks_dump(
-        self,
-        task_id: int,
-        fileformat: str,
-        filename: str,
-        *,
-        status_check_period: int = 2,
-        include_images: bool = False,
-    ) -> None:
-        """
-        Download annotations for a task in the specified format (e.g. 'YOLO ZIP 1.0').
-        """
-        self.client.tasks.retrieve(obj_id=task_id).export_dataset(
-            format_name=fileformat,
-            filename=filename,
-            pbar=DeferredTqdmProgressReporter(),
-            status_check_period=status_check_period,
-            include_images=include_images,
-        )
-
-    def tasks_upload(
-        self, task_id: str, fileformat: str, filename: str, *, status_check_period: int = 2
-    ) -> None:
-        """Upload annotations for a task in the specified format
-        (e.g. 'YOLO ZIP 1.0')."""
-        self.client.tasks.retrieve(obj_id=task_id).import_annotations(
-            format_name=fileformat,
-            filename=filename,
-            status_check_period=status_check_period,
-            pbar=DeferredTqdmProgressReporter(),
-        )
-
-    def tasks_export(self, task_id: str, filename: str, *, status_check_period: int = 2) -> None:
-        """Download a task backup"""
-        self.client.tasks.retrieve(obj_id=task_id).download_backup(
-            filename=filename,
-            status_check_period=status_check_period,
-            pbar=DeferredTqdmProgressReporter(),
-        )
-
-    def tasks_import(self, filename: str, *, status_check_period: int = 2) -> None:
-        """Import a task from a backup file"""
-        self.client.tasks.create_from_backup(
-            filename=filename,
-            status_check_period=status_check_period,
-            pbar=DeferredTqdmProgressReporter(),
-        )
-
-    def tasks_auto_annotate(
-        self,
-        task_id: int,
-        *,
-        function_module: Optional[str] = None,
-        function_file: Optional[Path] = None,
-        function_parameters: Dict[str, Any],
-        clear_existing: bool = False,
-        allow_unmatched_labels: bool = False,
-    ) -> None:
-        if function_module is not None:
-            function = importlib.import_module(function_module)
-        elif function_file is not None:
-            module_spec = importlib.util.spec_from_file_location("__cvat_function__", function_file)
-            function = importlib.util.module_from_spec(module_spec)
-            module_spec.loader.exec_module(function)
-        else:
-            assert False, "function identification arguments missing"
-
-        if hasattr(function, "create"):
-            # this is actually a function factory
-            function = function.create(**function_parameters)
-        else:
-            if function_parameters:
-                raise TypeError("function takes no parameters")
-
-        cvataa.annotate_task(
-            self.client,
-            task_id,
-            function,
-            pbar=DeferredTqdmProgressReporter(),
-            clear_existing=clear_existing,
-            allow_unmatched_labels=allow_unmatched_labels,
-        )
diff --git a/cvat-cli/src/cvat_cli/parser.py b/cvat-cli/src/cvat_cli/parser.py
deleted file mode 100644
index d456b087cd65..000000000000
--- a/cvat-cli/src/cvat_cli/parser.py
+++ /dev/null
@@ -1,452 +0,0 @@
-# Copyright (C) 2021-2022 Intel Corporation
-# Copyright (C) 2022 CVAT.ai Corporation
-#
-# SPDX-License-Identifier: MIT
-
-import argparse
-import getpass
-import json
-import logging
-import os
-import textwrap
-from pathlib import Path
-from typing import Any, Tuple
-
-from attr.converters import to_bool
-from cvat_sdk.core.proxies.tasks import ResourceType
-
-from .version import VERSION
-
-
-def get_auth(s):
-    """Parse USER[:PASS] strings and prompt for password if none was
-    supplied."""
-    user, _, password = s.partition(":")
-    password = password or os.environ.get("PASS") or getpass.getpass()
-    return user, password
-
-
-def parse_label_arg(s):
-    """If s is a file load it as JSON, otherwise parse s as JSON."""
-    if os.path.exists(s):
-        with open(s, "r") as fp:
-            return json.load(fp)
-    else:
-        return json.loads(s)
-
-
-def parse_resource_type(s: str) -> ResourceType:
-    try:
-        return ResourceType[s.upper()]
-    except KeyError:
-        return s
-
-
-def parse_function_parameter(s: str) -> Tuple[str, Any]:
-    key, sep, type_and_value = s.partition("=")
-
-    if not sep:
-        raise argparse.ArgumentTypeError("parameter value not specified")
-
-    type_, sep, value = type_and_value.partition(":")
-
-    if not sep:
-        raise argparse.ArgumentTypeError("parameter type not specified")
-
-    if type_ == "int":
-        value = int(value)
-    elif type_ == "float":
-        value = float(value)
-    elif type_ == "str":
-        pass
-    elif type_ == "bool":
-        value = to_bool(value)
-    else:
-        raise argparse.ArgumentTypeError(f"unsupported parameter type {type_!r}")
-
-    return (key, value)
-
-
-class BuildDictAction(argparse.Action):
-    def __init__(self, option_strings, dest, default=None, **kwargs):
-        super().__init__(option_strings, dest, default=default or {}, **kwargs)
-
-    def __call__(self, parser, namespace, values, option_string=None):
-        key, value = values
-        getattr(namespace, self.dest)[key] = value
-
-
-def make_cmdline_parser() -> argparse.ArgumentParser:
-    #######################################################################
-    # Command line interface definition
-    #######################################################################
-    parser = argparse.ArgumentParser(
-        description="Perform common operations related to CVAT tasks.\n\n"
-    )
-    parser.add_argument("--version", action="version", version=VERSION)
-    parser.add_argument(
-        "--insecure",
-        action="store_true",
-        help="Allows to disable SSL certificate check",
-    )
-
-    task_subparser = parser.add_subparsers(dest="action")
-
-    #######################################################################
-    # Positional arguments
-    #######################################################################
-    parser.add_argument(
-        "--auth",
-        type=get_auth,
-        metavar="USER:[PASS]",
-        default=getpass.getuser(),
-        help="""defaults to the current user and supports the PASS
-                environment variable or password prompt
-                (default user: %(default)s).""",
-    )
-    parser.add_argument(
-        "--server-host", type=str, default="localhost", help="host (default: %(default)s)"
-    )
-    parser.add_argument(
-        "--server-port",
-        type=int,
-        default=None,
-        help="port (default: 80 for http and 443 for https connections)",
-    )
-    parser.add_argument(
-        "--organization",
-        "--org",
-        metavar="SLUG",
-        help="""short name (slug) of the organization
-                to use when listing or creating resources;
-                set to blank string to use the personal workspace
-                (default: list all accessible objects, create in personal workspace)""",
-    )
-    parser.add_argument(
-        "--debug",
-        action="store_const",
-        dest="loglevel",
-        const=logging.DEBUG,
-        default=logging.INFO,
-        help="show debug output",
-    )
-
-    #######################################################################
-    # Create
-    #######################################################################
-    task_create_parser = task_subparser.add_parser(
-        "create",
-        description=textwrap.dedent(
-            """\
-            Create a new CVAT task. To create a task, you need
-            to specify labels using the --labels argument or
-            attach the task to an existing project using the
-            --project_id argument.
-        """
-        ),
-        formatter_class=argparse.RawTextHelpFormatter,
-    )
-    task_create_parser.add_argument("name", type=str, help="name of the task")
-    task_create_parser.add_argument(
-        "resource_type",
-        default="local",
-        choices=list(ResourceType),
-        type=parse_resource_type,
-        help="type of files specified",
-    )
-    task_create_parser.add_argument("resources", type=str, help="list of paths or URLs", nargs="+")
-    task_create_parser.add_argument(
-        "--annotation_path", default="", type=str, help="path to annotation file"
-    )
-    task_create_parser.add_argument(
-        "--annotation_format",
-        default="CVAT 1.1",
-        type=str,
-        help="format of the annotation file being uploaded, e.g. CVAT 1.1",
-    )
-    task_create_parser.add_argument(
-        "--bug_tracker", "--bug", default=None, type=str, help="bug tracker URL"
-    )
-    task_create_parser.add_argument(
-        "--chunk_size", default=None, type=int, help="the number of frames per chunk"
-    )
-    task_create_parser.add_argument(
-        "--completion_verification_period",
-        dest="status_check_period",
-        default=2,
-        type=float,
-        help=textwrap.dedent(
-            """\
-            number of seconds to wait until checking
-            if data compression finished (necessary before uploading annotations)
-        """
-        ),
-    )
-    task_create_parser.add_argument(
-        "--copy_data",
-        default=False,
-        action="store_true",
-        help=textwrap.dedent(
-            """\
-            set the option to copy the data, only used when resource type is
-            share (default: %(default)s)
-        """
-        ),
-    )
-    task_create_parser.add_argument(
-        "--frame_step",
-        default=None,
-        type=int,
-        help=textwrap.dedent(
-            """\
-            set the frame step option in the advanced configuration
-            when uploading image series or videos (default: %(default)s)
-        """
-        ),
-    )
-    task_create_parser.add_argument(
-        "--image_quality",
-        default=70,
-        type=int,
-        help=textwrap.dedent(
-            """\
-            set the image quality option in the advanced configuration
-            when creating tasks.(default: %(default)s)
-        """
-        ),
-    )
-    task_create_parser.add_argument(
-        "--labels",
-        default="[]",
-        type=parse_label_arg,
-        help="string or file containing JSON labels specification",
-    )
-    task_create_parser.add_argument(
-        "--project_id", default=None, type=int, help="project ID if project exists"
-    )
-    task_create_parser.add_argument(
-        "--overlap",
-        default=None,
-        type=int,
-        help="the number of intersected frames between different segments",
-    )
-    task_create_parser.add_argument(
-        "--segment_size", default=None, type=int, help="the number of frames in a segment"
-    )
-    task_create_parser.add_argument(
-        "--sorting-method",
-        default="lexicographical",
-        choices=["lexicographical", "natural", "predefined", "random"],
-        help="""data soring method (default: %(default)s)""",
-    )
-    task_create_parser.add_argument(
-        "--start_frame", default=None, type=int, help="the start frame of the video"
-    )
-    task_create_parser.add_argument(
-        "--stop_frame", default=None, type=int, help="the stop frame of the video"
-    )
-    task_create_parser.add_argument(
-        "--use_cache", action="store_true", help="""use cache"""  # automatically sets default=False
-    )
-    task_create_parser.add_argument(
-        "--use_zip_chunks",
-        action="store_true",  # automatically sets default=False
-        help="""zip chunks before sending them to the server""",
-    )
-    task_create_parser.add_argument(
-        "--cloud_storage_id",
-        default=None,
-        type=int,
-        help="cloud storage ID if you would like to use data from cloud storage",
-    )
-    task_create_parser.add_argument(
-        "--filename_pattern",
-        type=str,
-        help=textwrap.dedent(
-            """\
-            pattern for filtering data from the manifest file for the upload.
-            Only shell-style wildcards are supported:
-            * - matches everything
-            ? - matches any single character
-            [seq] - matches any character in 'seq'
-            [!seq] - matches any character not in seq
-        """
-        ),
-    )
-
-    #######################################################################
-    # Delete
-    #######################################################################
-    delete_parser = task_subparser.add_parser("delete", description="Delete a CVAT task.")
-    delete_parser.add_argument("task_ids", type=int, help="list of task IDs", nargs="+")
-
-    #######################################################################
-    # List
-    #######################################################################
-    ls_parser = task_subparser.add_parser(
-        "ls", description="List all CVAT tasks in simple or JSON format."
-    )
-    ls_parser.add_argument(
-        "--json",
-        dest="use_json_output",
-        default=False,
-        action="store_true",
-        help="output JSON data",
-    )
-
-    #######################################################################
-    # Frames
-    #######################################################################
-    frames_parser = task_subparser.add_parser(
-        "frames", description="Download all frame images for a CVAT task."
-    )
-    frames_parser.add_argument("task_id", type=int, help="task ID")
-    frames_parser.add_argument(
-        "frame_ids", type=int, help="list of frame IDs to download", nargs="+"
-    )
-    frames_parser.add_argument(
-        "--outdir", type=str, default="", help="directory to save images (default: CWD)"
-    )
-    frames_parser.add_argument(
-        "--quality",
-        type=str,
-        choices=("original", "compressed"),
-        default="original",
-        help="choose quality of images (default: %(default)s)",
-    )
-
-    #######################################################################
-    # Dump
-    #######################################################################
-    dump_parser = task_subparser.add_parser(
-        "dump", description="Download annotations for a CVAT task."
-    )
-    dump_parser.add_argument("task_id", type=int, help="task ID")
-    dump_parser.add_argument("filename", type=str, help="output file")
-    dump_parser.add_argument(
-        "--format",
-        dest="fileformat",
-        type=str,
-        default="CVAT for images 1.1",
-        help="annotation format (default: %(default)s)",
-    )
-    dump_parser.add_argument(
-        "--completion_verification_period",
-        dest="status_check_period",
-        default=2,
-        type=float,
-        help="number of seconds to wait until checking if dataset building finished",
-    )
-    dump_parser.add_argument(
-        "--with-images",
-        type=to_bool,
-        default=False,
-        dest="include_images",
-        help="Whether to include images or not (default: %(default)s)",
-    )
-
-    #######################################################################
-    # Upload Annotations
-    #######################################################################
-    upload_parser = task_subparser.add_parser(
-        "upload", description="Upload annotations for a CVAT task."
-    )
-    upload_parser.add_argument("task_id", type=int, help="task ID")
-    upload_parser.add_argument("filename", type=str, help="upload file")
-    upload_parser.add_argument(
-        "--format",
-        dest="fileformat",
-        type=str,
-        default="CVAT 1.1",
-        help="annotation format (default: %(default)s)",
-    )
-
-    #######################################################################
-    # Export task
-    #######################################################################
-    export_task_parser = task_subparser.add_parser("export", description="Export a CVAT task.")
-    export_task_parser.add_argument("task_id", type=int, help="task ID")
-    export_task_parser.add_argument("filename", type=str, help="output file")
-    export_task_parser.add_argument(
-        "--completion_verification_period",
-        dest="status_check_period",
-        default=2,
-        type=float,
-        help="time interval between checks if archive building has been finished, in seconds",
-    )
-
-    #######################################################################
-    # Import task
-    #######################################################################
-    import_task_parser = task_subparser.add_parser("import", description="Import a CVAT task.")
-    import_task_parser.add_argument("filename", type=str, help="upload file")
-    import_task_parser.add_argument(
-        "--completion_verification_period",
-        dest="status_check_period",
-        default=2,
-        type=float,
-        help="time interval between checks if archive processing was finished, in seconds",
-    )
-
-    #######################################################################
-    # Auto-annotate
-    #######################################################################
-    auto_annotate_task_parser = task_subparser.add_parser(
-        "auto-annotate",
-        description="Automatically annotate a CVAT task by running a function on the local machine.",
-    )
-    auto_annotate_task_parser.add_argument("task_id", type=int, help="task ID")
-
-    function_group = auto_annotate_task_parser.add_mutually_exclusive_group(required=True)
-
-    function_group.add_argument(
-        "--function-module",
-        metavar="MODULE",
-        help="qualified name of a module to use as the function",
-    )
-
-    function_group.add_argument(
-        "--function-file",
-        metavar="PATH",
-        type=Path,
-        help="path to a Python source file to use as the function",
-    )
-
-    auto_annotate_task_parser.add_argument(
-        "--function-parameter",
-        "-p",
-        metavar="NAME=TYPE:VALUE",
-        type=parse_function_parameter,
-        action=BuildDictAction,
-        dest="function_parameters",
-        help="parameter for the function",
-    )
-
-    auto_annotate_task_parser.add_argument(
-        "--clear-existing", action="store_true", help="Remove existing annotations from the task"
-    )
-
-    auto_annotate_task_parser.add_argument(
-        "--allow-unmatched-labels",
-        action="store_true",
-        help="Allow the function to declare labels not configured in the task",
-    )
-
-    return parser
-
-
-def get_action_args(
-    parser: argparse.ArgumentParser, parsed_args: argparse.Namespace
-) -> argparse.Namespace:
-    # FIXME: a hacky way to remove unnecessary args
-    action_args = dict(vars(parsed_args))
-
-    for action in parser._actions:
-        action_args.pop(action.dest, None)
-
-    # remove default args
-    for k, v in dict(action_args).items():
-        if v is None:
-            action_args.pop(k, None)
-
-    return argparse.Namespace(**action_args)
diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py
index 3899bcb7cd96..b2829a54b105 100644
--- a/cvat-cli/src/cvat_cli/version.py
+++ b/cvat-cli/src/cvat_cli/version.py
@@ -1,1 +1,1 @@
-VERSION = "2.21.3"
+VERSION = "2.22.0"
diff --git a/cvat-core/src/annotations-actions.ts b/cvat-core/src/annotations-actions.ts
index 9e956421ae08..43d3ef29a910 100644
--- a/cvat-core/src/annotations-actions.ts
+++ b/cvat-core/src/annotations-actions.ts
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: MIT
 
-import { omit, throttle } from 'lodash';
+import { omit, range, throttle } from 'lodash';
 import { ArgumentError } from './exceptions';
 import { SerializedCollection, SerializedShape } from './server-response-types';
 import { Job, Task } from './session';
@@ -107,13 +107,15 @@ class PropagateShapes extends BaseSingleFrameAction {
     }
 
     public async run(
-        instance,
+        instance: Job | Task,
         { collection: { shapes }, frameData: { number } },
     ): Promise<SingleFrameActionOutput> {
         if (number === this.#targetFrame) {
             return { collection: { shapes } };
         }
-        const propagatedShapes = propagateShapes<SerializedShape>(shapes, number, this.#targetFrame);
+
+        const frameNumbers = instance instanceof Job ? await instance.frames.frameNumbers() : range(0, instance.size);
+        const propagatedShapes = propagateShapes<SerializedShape>(shapes, number, this.#targetFrame, frameNumbers);
         return { collection: { shapes: [...shapes, ...propagatedShapes] } };
     }
 
diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts
index b29335865d01..e02b3e640a83 100644
--- a/cvat-core/src/frames.ts
+++ b/cvat-core/src/frames.ts
@@ -211,6 +211,11 @@ export class FramesMetaData {
         return Math.floor(this.getFrameIndex(dataFrameNumber) / this.chunkSize);
     }
 
+    getSegmentFrameNumbers(jobStartFrame: number): number[] {
+        const frames = this.getDataFrameNumbers();
+        return frames.map((frame) => this.getJobRelativeFrameNumber(frame) + jobStartFrame);
+    }
+
     getDataFrameNumbers(): number[] {
         if (this.includedFrames) {
             return [...this.includedFrames];
@@ -348,9 +353,7 @@ Object.defineProperty(FrameData.prototype.data, 'implementation', {
             const requestId = +_.uniqueId();
             const requestedDataFrameNumber = meta.getDataFrameNumber(this.number - jobStartFrame);
             const chunkIndex = meta.getFrameChunkIndex(requestedDataFrameNumber);
-            const segmentFrameNumbers = meta.getDataFrameNumbers().map((dataFrameNumber: number) => (
-                meta.getJobRelativeFrameNumber(dataFrameNumber) + jobStartFrame
-            ));
+            const segmentFrameNumbers = meta.getSegmentFrameNumbers(jobStartFrame);
             const frame = provider.frame(this.number);
 
             function findTheNextNotDecodedChunk(currentFrameIndex: number): number | null {
@@ -889,9 +892,7 @@ export function getJobFrameNumbers(jobID: number): number[] {
     }
 
     const { meta, jobStartFrame } = frameDataCache[jobID];
-    return meta.getDataFrameNumbers().map((dataFrameNumber: number): number => (
-        meta.getJobRelativeFrameNumber(dataFrameNumber) + jobStartFrame
-    ));
+    return meta.getSegmentFrameNumbers(jobStartFrame);
 }
 
 export function clear(jobID: number): void {
diff --git a/cvat-core/src/object-utils.ts b/cvat-core/src/object-utils.ts
index 6e7fcbbd8d8c..b3592e5cbe1c 100644
--- a/cvat-core/src/object-utils.ts
+++ b/cvat-core/src/object-utils.ts
@@ -360,7 +360,7 @@ export function rle2Mask(rle: number[], width: number, height: number): number[]
 }
 
 export function propagateShapes<T extends SerializedShape | ObjectState>(
-    shapes: T[], from: number, to: number,
+    shapes: T[], from: number, to: number, frameNumbers: number[],
 ): T[] {
     const getCopy = (shape: T): SerializedShape | SerializedData => {
         if (shape instanceof ObjectState) {
@@ -397,9 +397,18 @@ export function propagateShapes<T extends SerializedShape | ObjectState>(
         };
     };
 
+    const targetFrameNumbers = frameNumbers.filter(
+        (frameNumber: number) => frameNumber >= Math.min(from, to) &&
+            frameNumber <= Math.max(from, to) &&
+            frameNumber !== from,
+    );
+
     const states: T[] = [];
-    const sign = Math.sign(to - from);
-    for (let frame = from + sign; sign > 0 ? frame <= to : frame >= to; frame += sign) {
+    for (const frame of targetFrameNumbers) {
+        if (frame === from) {
+            continue;
+        }
+
         for (const shape of shapes) {
             const copy = getCopy(shape);
 
diff --git a/cvat-core/src/quality-settings.ts b/cvat-core/src/quality-settings.ts
index c5e3ea6974c2..7c591e371cc4 100644
--- a/cvat-core/src/quality-settings.ts
+++ b/cvat-core/src/quality-settings.ts
@@ -14,6 +14,11 @@ export enum TargetMetric {
     RECALL = 'recall',
 }
 
+export enum PointSizeBase {
+    IMAGE_SIZE = 'image_size',
+    GROUP_BBOX_SIZE = 'group_bbox_size',
+}
+
 export default class QualitySettings {
     #id: number;
     #targetMetric: TargetMetric;
@@ -22,6 +27,7 @@ export default class QualitySettings {
     #task: number;
     #iouThreshold: number;
     #oksSigma: number;
+    #pointSizeBase: PointSizeBase;
     #lineThickness: number;
     #lowOverlapThreshold: number;
     #orientedLines: boolean;
@@ -32,6 +38,7 @@ export default class QualitySettings {
     #objectVisibilityThreshold: number;
     #panopticComparison: boolean;
     #compareAttributes: boolean;
+    #matchEmptyFrames: boolean;
     #descriptions: Record<string, string>;
 
     constructor(initialData: SerializedQualitySettingsData) {
@@ -42,6 +49,7 @@ export default class QualitySettings {
         this.#maxValidationsPerJob = initialData.max_validations_per_job;
         this.#iouThreshold = initialData.iou_threshold;
         this.#oksSigma = initialData.oks_sigma;
+        this.#pointSizeBase = initialData.point_size_base as PointSizeBase;
         this.#lineThickness = initialData.line_thickness;
         this.#lowOverlapThreshold = initialData.low_overlap_threshold;
         this.#orientedLines = initialData.compare_line_orientation;
@@ -52,6 +60,7 @@ export default class QualitySettings {
         this.#objectVisibilityThreshold = initialData.object_visibility_threshold;
         this.#panopticComparison = initialData.panoptic_comparison;
         this.#compareAttributes = initialData.compare_attributes;
+        this.#matchEmptyFrames = initialData.match_empty_frames;
         this.#descriptions = initialData.descriptions;
     }
 
@@ -79,6 +88,14 @@ export default class QualitySettings {
         this.#oksSigma = newVal;
     }
 
+    get pointSizeBase(): PointSizeBase {
+        return this.#pointSizeBase;
+    }
+
+    set pointSizeBase(newVal: PointSizeBase) {
+        this.#pointSizeBase = newVal;
+    }
+
     get lineThickness(): number {
         return this.#lineThickness;
     }
@@ -183,6 +200,14 @@ export default class QualitySettings {
         this.#maxValidationsPerJob = newVal;
     }
 
+    get matchEmptyFrames(): boolean {
+        return this.#matchEmptyFrames;
+    }
+
+    set matchEmptyFrames(newVal: boolean) {
+        this.#matchEmptyFrames = newVal;
+    }
+
     get descriptions(): Record<string, string> {
         const descriptions: Record<string, string> = Object.keys(this.#descriptions).reduce((acc, key) => {
             const camelCaseKey = _.camelCase(key);
@@ -197,6 +222,7 @@ export default class QualitySettings {
         const result: SerializedQualitySettingsData = {
             iou_threshold: this.#iouThreshold,
             oks_sigma: this.#oksSigma,
+            point_size_base: this.#pointSizeBase,
             line_thickness: this.#lineThickness,
             low_overlap_threshold: this.#lowOverlapThreshold,
             compare_line_orientation: this.#orientedLines,
@@ -210,6 +236,7 @@ export default class QualitySettings {
             target_metric: this.#targetMetric,
             target_metric_threshold: this.#targetMetricThreshold,
             max_validations_per_job: this.#maxValidationsPerJob,
+            match_empty_frames: this.#matchEmptyFrames,
         };
 
         return result;
diff --git a/cvat-core/src/request.ts b/cvat-core/src/request.ts
index 1935f78b2f0c..ad8aa04d45aa 100644
--- a/cvat-core/src/request.ts
+++ b/cvat-core/src/request.ts
@@ -55,6 +55,7 @@ export class Request {
         return this.#status.toLowerCase() as RQStatus;
     }
 
+    // The `progress` represents a value between 0 and 1
     get progress(): number | undefined {
         return this.#progress;
     }
diff --git a/cvat-core/src/requests-manager.ts b/cvat-core/src/requests-manager.ts
index 711073988955..800b577242c8 100644
--- a/cvat-core/src/requests-manager.ts
+++ b/cvat-core/src/requests-manager.ts
@@ -34,7 +34,7 @@ class RequestsManager {
         requestDelayIdx: number | null,
         request: Request | null,
         timeout: number | null;
-        promise?: Promise<Request>;
+        promise: Promise<Request>;
     }>;
 
     private requestStack: number[];
@@ -71,6 +71,7 @@ class RequestsManager {
             }
             return this.listening[requestID].promise;
         }
+
         const promise = new Promise<Request>((resolve, reject) => {
             const timeoutCallback = async (): Promise<void> => {
                 // We make sure that no more than REQUESTS_COUNT requests are sent simultaneously
@@ -131,27 +132,29 @@ class RequestsManager {
                                     message: `Could not get a status of the request ${requestID}. ${error.toString()}`,
                                 })));
                         }
+
+                        delete this.listening[requestID];
                         reject(error);
                     }
                 }
             };
 
-            if (initialRequest?.status === RQStatus.FAILED) {
-                reject(new RequestError(initialRequest?.message));
-            } else {
-                this.listening[requestID] = {
-                    onUpdate: callback ? [callback] : [],
-                    timeout: window.setTimeout(timeoutCallback),
-                    request: initialRequest,
-                    requestDelayIdx: 0,
-                };
-            }
+            Promise.resolve().then(() => {
+                // running as microtask to make sure "promise" was initialized
+                if (initialRequest?.status === RQStatus.FAILED) {
+                    reject(new RequestError(initialRequest?.message));
+                } else {
+                    this.listening[requestID] = {
+                        onUpdate: callback ? [callback] : [],
+                        timeout: window.setTimeout(timeoutCallback),
+                        request: initialRequest,
+                        requestDelayIdx: 0,
+                        promise,
+                    };
+                }
+            });
         });
 
-        this.listening[requestID] = {
-            ...this.listening[requestID],
-            promise,
-        };
         return promise;
     }
 
diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts
index 4bf7a482bccb..ea97c0730aaa 100644
--- a/cvat-core/src/server-response-types.ts
+++ b/cvat-core/src/server-response-types.ts
@@ -47,7 +47,7 @@ export interface SerializedUser {
     first_name: string;
     last_name: string;
     email?: string;
-    groups?: ('user' | 'business' | 'admin')[];
+    groups?: ('user' | 'admin')[];
     is_staff?: boolean;
     is_superuser?: boolean;
     is_active?: boolean;
@@ -247,6 +247,7 @@ export interface SerializedQualitySettingsData {
     max_validations_per_job?: number;
     iou_threshold?: number;
     oks_sigma?: number;
+    point_size_base?: string;
     line_thickness?: number;
     low_overlap_threshold?: number;
     compare_line_orientation?: boolean;
@@ -257,6 +258,7 @@ export interface SerializedQualitySettingsData {
     object_visibility_threshold?: number;
     panoptic_comparison?: boolean;
     compare_attributes?: boolean;
+    match_empty_frames?: boolean;
     descriptions?: Record<string, string>;
 }
 
diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts
index 1c2194250155..904899831abf 100644
--- a/cvat-core/src/session-implementation.ts
+++ b/cvat-core/src/session-implementation.ts
@@ -879,6 +879,14 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass {
         },
     });
 
+    Object.defineProperty(Task.prototype.frames.frameNumbers, 'implementation', {
+        value: function includedFramesImplementation(
+            this: TaskClass,
+        ): ReturnType<typeof TaskClass.prototype.frames.frameNumbers> {
+            throw new Error('Not implemented for Task');
+        },
+    });
+
     Object.defineProperty(Task.prototype.frames.preview, 'implementation', {
         value: function previewImplementation(
             this: TaskClass,
diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts
index 1164ae0c07de..a2bc2008aef0 100644
--- a/cvat-core/src/session.ts
+++ b/cvat-core/src/session.ts
@@ -373,8 +373,8 @@ export class Session {
     };
 
     public actions: {
-        undo: (count: number) => Promise<number[]>;
-        redo: (count: number) => Promise<number[]>;
+        undo: (count?: number) => Promise<number[]>;
+        redo: (count?: number) => Promise<number[]>;
         freeze: (frozen: boolean) => Promise<void>;
         clear: () => Promise<void>;
         get: () => Promise<{ undo: [HistoryActions, number][], redo: [HistoryActions, number][] }>;
@@ -403,8 +403,8 @@ export class Session {
     public logger: {
         log: (
             scope: Parameters<typeof logger.log>[0],
-            payload: Parameters<typeof logger.log>[1],
-            wait: Parameters<typeof logger.log>[2],
+            payload?: Parameters<typeof logger.log>[1],
+            wait?: Parameters<typeof logger.log>[2],
         ) => ReturnType<typeof logger.log>;
     };
 
@@ -463,7 +463,7 @@ export class Session {
     }
 }
 
-type InitializerType = Readonly<Omit<SerializedJob, 'labels'> & { labels?: SerializedLabel[] }>;
+type InitializerType = Readonly<Partial<Omit<SerializedJob, 'labels'> & { labels?: SerializedLabel[] }>>;
 
 export class Job extends Session {
     #data: {
diff --git a/cvat-core/src/user.ts b/cvat-core/src/user.ts
index 6d7366151fb4..ef28f3633f0e 100644
--- a/cvat-core/src/user.ts
+++ b/cvat-core/src/user.ts
@@ -11,7 +11,7 @@ export default class User {
     public readonly email: string;
     public readonly firstName: string;
     public readonly lastName: string;
-    public readonly groups: ('user' | 'business' | 'admin')[];
+    public readonly groups: ('user' | 'admin')[];
     public readonly lastLogin: string;
     public readonly dateJoined: string;
     public readonly isStaff: boolean;
diff --git a/cvat-sdk/cvat_sdk/auto_annotation/driver.py b/cvat-sdk/cvat_sdk/auto_annotation/driver.py
index d6294f44f8f6..0f3d82ea32ea 100644
--- a/cvat-sdk/cvat_sdk/auto_annotation/driver.py
+++ b/cvat-sdk/cvat_sdk/auto_annotation/driver.py
@@ -3,7 +3,8 @@
 # SPDX-License-Identifier: MIT
 
 import logging
-from typing import List, Mapping, Optional, Sequence
+from collections.abc import Mapping, Sequence
+from typing import Optional
 
 import attrs
 
@@ -119,7 +120,7 @@ def __init__(
                 fun_label, ds_labels_by_name
             )
 
-    def validate_and_remap(self, shapes: List[models.LabeledShapeRequest], ds_frame: int) -> None:
+    def validate_and_remap(self, shapes: list[models.LabeledShapeRequest], ds_frame: int) -> None:
         new_shapes = []
 
         for shape in shapes:
diff --git a/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_detection.py b/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_detection.py
index 57457d742256..d257cb7ec889 100644
--- a/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_detection.py
+++ b/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_detection.py
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: MIT
 
 from functools import cached_property
-from typing import List
 
 import PIL.Image
 import torchvision.models
@@ -28,7 +27,7 @@ def spec(self) -> cvataa.DetectionFunctionSpec:
             ]
         )
 
-    def detect(self, context, image: PIL.Image.Image) -> List[models.LabeledShapeRequest]:
+    def detect(self, context, image: PIL.Image.Image) -> list[models.LabeledShapeRequest]:
         results = self._model([self._transforms(image)])
 
         return [
diff --git a/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_keypoint_detection.py b/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_keypoint_detection.py
index b4eb47d476d3..c7199b67738b 100644
--- a/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_keypoint_detection.py
+++ b/cvat-sdk/cvat_sdk/auto_annotation/functions/torchvision_keypoint_detection.py
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: MIT
 
 from functools import cached_property
-from typing import List
 
 import PIL.Image
 import torchvision.models
@@ -36,7 +35,7 @@ def spec(self) -> cvataa.DetectionFunctionSpec:
             ]
         )
 
-    def detect(self, context, image: PIL.Image.Image) -> List[models.LabeledShapeRequest]:
+    def detect(self, context, image: PIL.Image.Image) -> list[models.LabeledShapeRequest]:
         results = self._model([self._transforms(image)])
 
         return [
diff --git a/cvat-sdk/cvat_sdk/auto_annotation/interface.py b/cvat-sdk/cvat_sdk/auto_annotation/interface.py
index 67313a7da6e5..20a21fe4a5cf 100644
--- a/cvat-sdk/cvat_sdk/auto_annotation/interface.py
+++ b/cvat-sdk/cvat_sdk/auto_annotation/interface.py
@@ -3,7 +3,8 @@
 # SPDX-License-Identifier: MIT
 
 import abc
-from typing import List, Protocol, Sequence
+from collections.abc import Sequence
+from typing import Protocol
 
 import attrs
 import PIL.Image
@@ -79,7 +80,7 @@ def spec(self) -> DetectionFunctionSpec:
 
     def detect(
         self, context: DetectionFunctionContext, image: PIL.Image.Image
-    ) -> List[models.LabeledShapeRequest]:
+    ) -> list[models.LabeledShapeRequest]:
         """
         Detects objects on the supplied image and returns the results.
 
diff --git a/cvat-sdk/cvat_sdk/core/client.py b/cvat-sdk/cvat_sdk/core/client.py
index 0ae0b88ecad9..168259920c0f 100644
--- a/cvat-sdk/cvat_sdk/core/client.py
+++ b/cvat-sdk/cvat_sdk/core/client.py
@@ -7,10 +7,11 @@
 
 import logging
 import urllib.parse
+from collections.abc import Generator, Sequence
 from contextlib import contextmanager, suppress
 from pathlib import Path
 from time import sleep
-from typing import Any, Dict, Generator, Optional, Sequence, Tuple, TypeVar
+from typing import Any, Optional, TypeVar
 
 import attrs
 import packaging.specifiers as specifiers
@@ -95,7 +96,7 @@ def __init__(
         if check_server_version:
             self.check_server_version()
 
-        self._repos: Dict[str, Repo] = {}
+        self._repos: dict[str, Repo] = {}
         """A cache for created Repository instances"""
 
     _ORG_SLUG_HEADER = "X-Organization"
@@ -183,7 +184,7 @@ def __exit__(self, exc_type, exc_value, traceback) -> None:
     def close(self) -> None:
         return self.__exit__(None, None, None)
 
-    def login(self, credentials: Tuple[str, str]) -> None:
+    def login(self, credentials: tuple[str, str]) -> None:
         (auth, _) = self.api_client.auth_api.create_login(
             models.LoginSerializerExRequest(username=credentials[0], password=credentials[1])
         )
@@ -211,7 +212,7 @@ def wait_for_completion(
         rq_id: str,
         *,
         status_check_period: Optional[int] = None,
-    ) -> Tuple[models.Request, urllib3.HTTPResponse]:
+    ) -> tuple[models.Request, urllib3.HTTPResponse]:
         if status_check_period is None:
             status_check_period = self.config.status_check_period
 
@@ -319,8 +320,8 @@ def make_endpoint_url(
         path: str,
         *,
         psub: Optional[Sequence[Any]] = None,
-        kwsub: Optional[Dict[str, Any]] = None,
-        query_params: Optional[Dict[str, Any]] = None,
+        kwsub: Optional[dict[str, Any]] = None,
+        query_params: Optional[dict[str, Any]] = None,
     ) -> str:
         url = self.host + path
         if psub or kwsub:
@@ -331,7 +332,7 @@ def make_endpoint_url(
 
 
 def make_client(
-    host: str, *, port: Optional[int] = None, credentials: Optional[Tuple[str, str]] = None
+    host: str, *, port: Optional[int] = None, credentials: Optional[tuple[str, str]] = None
 ) -> Client:
     url = host.rstrip("/")
     if port:
diff --git a/cvat-sdk/cvat_sdk/core/downloading.py b/cvat-sdk/cvat_sdk/core/downloading.py
index 2e8263373350..d44535b2fc82 100644
--- a/cvat-sdk/cvat_sdk/core/downloading.py
+++ b/cvat-sdk/cvat_sdk/core/downloading.py
@@ -8,7 +8,7 @@
 import json
 from contextlib import closing
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Dict, Optional
+from typing import TYPE_CHECKING, Any, Optional
 
 from cvat_sdk.api_client.api_client import Endpoint
 from cvat_sdk.core.helpers import expect_status
@@ -80,8 +80,8 @@ def prepare_file(
         self,
         endpoint: Endpoint,
         *,
-        url_params: Optional[Dict[str, Any]] = None,
-        query_params: Optional[Dict[str, Any]] = None,
+        url_params: Optional[dict[str, Any]] = None,
+        query_params: Optional[dict[str, Any]] = None,
         status_check_period: Optional[int] = None,
     ):
         client = self._client
@@ -118,8 +118,8 @@ def prepare_and_download_file_from_endpoint(
         endpoint: Endpoint,
         filename: Path,
         *,
-        url_params: Optional[Dict[str, Any]] = None,
-        query_params: Optional[Dict[str, Any]] = None,
+        url_params: Optional[dict[str, Any]] = None,
+        query_params: Optional[dict[str, Any]] = None,
         pbar: Optional[ProgressReporter] = None,
         status_check_period: Optional[int] = None,
     ):
diff --git a/cvat-sdk/cvat_sdk/core/helpers.py b/cvat-sdk/cvat_sdk/core/helpers.py
index b04e33e4c687..425fbc78a083 100644
--- a/cvat-sdk/cvat_sdk/core/helpers.py
+++ b/cvat-sdk/cvat_sdk/core/helpers.py
@@ -7,7 +7,8 @@
 import io
 import json
 import warnings
-from typing import Any, Dict, Iterable, List, Optional, Union
+from collections.abc import Iterable
+from typing import Any, Optional, Union
 
 import tqdm
 import urllib3
@@ -19,7 +20,7 @@
 
 def get_paginated_collection(
     endpoint: Endpoint, *, return_json: bool = False, **kwargs
-) -> Union[List, List[Dict[str, Any]]]:
+) -> Union[list, list[dict[str, Any]]]:
     """
     Accumulates results from all the pages
     """
diff --git a/cvat-sdk/cvat_sdk/core/progress.py b/cvat-sdk/cvat_sdk/core/progress.py
index fd844de722a0..33c7e420714e 100644
--- a/cvat-sdk/cvat_sdk/core/progress.py
+++ b/cvat-sdk/cvat_sdk/core/progress.py
@@ -6,7 +6,8 @@
 from __future__ import annotations
 
 import contextlib
-from typing import Generator, Iterable, Optional, TypeVar
+from collections.abc import Generator, Iterable
+from typing import Optional, TypeVar
 
 T = TypeVar("T")
 
diff --git a/cvat-sdk/cvat_sdk/core/proxies/annotations.py b/cvat-sdk/cvat_sdk/core/proxies/annotations.py
index e9353888119f..53db2af34712 100644
--- a/cvat-sdk/cvat_sdk/core/proxies/annotations.py
+++ b/cvat-sdk/cvat_sdk/core/proxies/annotations.py
@@ -3,8 +3,9 @@
 # SPDX-License-Identifier: MIT
 
 from abc import ABC
+from collections.abc import Sequence
 from enum import Enum
-from typing import Optional, Sequence
+from typing import Optional
 
 from cvat_sdk import models
 from cvat_sdk.core.proxies.model_proxy import _EntityT
diff --git a/cvat-sdk/cvat_sdk/core/proxies/issues.py b/cvat-sdk/cvat_sdk/core/proxies/issues.py
index 5df1069c1178..8f844d68522a 100644
--- a/cvat-sdk/cvat_sdk/core/proxies/issues.py
+++ b/cvat-sdk/cvat_sdk/core/proxies/issues.py
@@ -4,8 +4,6 @@
 
 from __future__ import annotations
 
-from typing import List
-
 from cvat_sdk.api_client import apis, models
 from cvat_sdk.core.helpers import get_paginated_collection
 from cvat_sdk.core.proxies.model_proxy import (
@@ -53,7 +51,7 @@ class Issue(
 ):
     _model_partial_update_arg = "patched_issue_write_request"
 
-    def get_comments(self) -> List[Comment]:
+    def get_comments(self) -> list[Comment]:
         return [
             Comment(self._client, m)
             for m in get_paginated_collection(
diff --git a/cvat-sdk/cvat_sdk/core/proxies/jobs.py b/cvat-sdk/cvat_sdk/core/proxies/jobs.py
index ac81380b7566..5eb3e6767477 100644
--- a/cvat-sdk/cvat_sdk/core/proxies/jobs.py
+++ b/cvat-sdk/cvat_sdk/core/proxies/jobs.py
@@ -6,8 +6,9 @@
 
 import io
 import mimetypes
+from collections.abc import Sequence
 from pathlib import Path
-from typing import TYPE_CHECKING, List, Optional, Sequence
+from typing import TYPE_CHECKING, Optional
 
 from PIL import Image
 
@@ -93,7 +94,7 @@ def download_frames(
         outdir: StrPath = ".",
         quality: str = "original",
         filename_pattern: str = "frame_{frame_id:06d}{frame_ext}",
-    ) -> Optional[List[Image.Image]]:
+    ) -> Optional[list[Image.Image]]:
         """
         Download the requested frame numbers for a job and save images as outdir/filename_pattern
         """
@@ -125,12 +126,12 @@ def get_meta(self) -> models.IDataMetaRead:
         (meta, _) = self.api.retrieve_data_meta(self.id)
         return meta
 
-    def get_labels(self) -> List[models.ILabel]:
+    def get_labels(self) -> list[models.ILabel]:
         return get_paginated_collection(
             self._client.api_client.labels_api.list_endpoint, job_id=self.id
         )
 
-    def get_frames_info(self) -> List[models.IFrameMeta]:
+    def get_frames_info(self) -> list[models.IFrameMeta]:
         return self.get_meta().frames
 
     def remove_frames_by_ids(self, ids: Sequence[int]) -> None:
@@ -141,7 +142,7 @@ def remove_frames_by_ids(self, ids: Sequence[int]) -> None:
             ),
         )
 
-    def get_issues(self) -> List[Issue]:
+    def get_issues(self) -> list[Issue]:
         return [
             Issue(self._client, m)
             for m in get_paginated_collection(
diff --git a/cvat-sdk/cvat_sdk/core/proxies/model_proxy.py b/cvat-sdk/cvat_sdk/core/proxies/model_proxy.py
index 40b6ffd27549..1557a61861ae 100644
--- a/cvat-sdk/cvat_sdk/core/proxies/model_proxy.py
+++ b/cvat-sdk/cvat_sdk/core/proxies/model_proxy.py
@@ -12,13 +12,9 @@
     TYPE_CHECKING,
     Any,
     Callable,
-    Dict,
     Generic,
-    List,
     Literal,
     Optional,
-    Tuple,
-    Type,
     TypeVar,
     Union,
     overload,
@@ -96,15 +92,15 @@ class Repo(ModelProxy[ModelType, ApiType]):
     Implements group and management operations for entities.
     """
 
-    _entity_type: Type[Entity[ModelType, ApiType]]
+    _entity_type: type[Entity[ModelType, ApiType]]
 
 
 ### Utilities
 
 
 def build_model_bases(
-    mt: Type[ModelType], at: Type[ApiType], *, api_member_name: Optional[str] = None
-) -> Tuple[Type[Entity[ModelType, ApiType]], Type[Repo[ModelType, ApiType]]]:
+    mt: type[ModelType], at: type[ApiType], *, api_member_name: Optional[str] = None
+) -> tuple[type[Entity[ModelType, ApiType]], type[Repo[ModelType, ApiType]]]:
     """
     Helps to remove code duplication in declarations of derived classes
     """
@@ -128,7 +124,7 @@ class _RepoBase(Repo[ModelType, ApiType]):
 
 
 class ModelCreateMixin(Generic[_EntityT, IModel]):
-    def create(self: Repo, spec: Union[Dict[str, Any], IModel]) -> _EntityT:
+    def create(self: Repo, spec: Union[dict[str, Any], IModel]) -> _EntityT:
         """
         Creates a new object on the server and returns the corresponding local object
         """
@@ -149,12 +145,12 @@ def retrieve(self: Repo, obj_id: int) -> _EntityT:
 
 class ModelListMixin(Generic[_EntityT]):
     @overload
-    def list(self: Repo, *, return_json: Literal[False] = False) -> List[_EntityT]: ...
+    def list(self: Repo, *, return_json: Literal[False] = False) -> list[_EntityT]: ...
 
     @overload
-    def list(self: Repo, *, return_json: Literal[True] = False) -> List[Any]: ...
+    def list(self: Repo, *, return_json: Literal[True] = False) -> list[Any]: ...
 
-    def list(self: Repo, *, return_json: bool = False) -> List[Union[_EntityT, Any]]:
+    def list(self: Repo, *, return_json: bool = False) -> list[Union[_EntityT, Any]]:
         """
         Retrieves all objects from the server and returns them in basic or JSON format.
         """
@@ -174,8 +170,8 @@ class ModelUpdateMixin(ABC, Generic[IModel]):
     def _model_partial_update_arg(self: Entity) -> str: ...
 
     def _export_update_fields(
-        self: Entity, overrides: Optional[Union[Dict[str, Any], IModel]] = None
-    ) -> Dict[str, Any]:
+        self: Entity, overrides: Optional[Union[dict[str, Any], IModel]] = None
+    ) -> dict[str, Any]:
         # TODO: support field conversion and assignment updating
         # fields = to_json(self._model)
 
@@ -194,7 +190,7 @@ def fetch(self: Entity) -> Self:
         (self._model, _) = self.api.retrieve(id=getattr(self, self._model_id_field))
         return self
 
-    def update(self: Entity, values: Union[Dict[str, Any], IModel]) -> Self:
+    def update(self: Entity, values: Union[dict[str, Any], IModel]) -> Self:
         """
         Commits model changes to the server
 
diff --git a/cvat-sdk/cvat_sdk/core/proxies/projects.py b/cvat-sdk/cvat_sdk/core/proxies/projects.py
index 70c647bfd033..3e0eddc00e36 100644
--- a/cvat-sdk/cvat_sdk/core/proxies/projects.py
+++ b/cvat-sdk/cvat_sdk/core/proxies/projects.py
@@ -7,7 +7,7 @@
 import io
 import json
 from pathlib import Path
-from typing import TYPE_CHECKING, List, Optional
+from typing import TYPE_CHECKING, Optional
 
 from cvat_sdk.api_client import apis, models
 from cvat_sdk.core.helpers import get_paginated_collection
@@ -72,7 +72,7 @@ def get_annotations(self) -> models.ILabeledData:
         (annotations, _) = self.api.retrieve_annotations(self.id)
         return annotations
 
-    def get_tasks(self) -> List[Task]:
+    def get_tasks(self) -> list[Task]:
         return [
             Task(self._client, m)
             for m in get_paginated_collection(
@@ -80,7 +80,7 @@ def get_tasks(self) -> List[Task]:
             )
         ]
 
-    def get_labels(self) -> List[models.ILabel]:
+    def get_labels(self) -> list[models.ILabel]:
         return get_paginated_collection(
             self._client.api_client.labels_api.list_endpoint, project_id=self.id
         )
diff --git a/cvat-sdk/cvat_sdk/core/proxies/tasks.py b/cvat-sdk/cvat_sdk/core/proxies/tasks.py
index fe2d80d857b0..e0db111f8511 100644
--- a/cvat-sdk/cvat_sdk/core/proxies/tasks.py
+++ b/cvat-sdk/cvat_sdk/core/proxies/tasks.py
@@ -8,10 +8,11 @@
 import json
 import mimetypes
 import shutil
+from collections.abc import Sequence
 from enum import Enum
 from pathlib import Path
 from time import sleep
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence
+from typing import TYPE_CHECKING, Any, Optional
 
 from PIL import Image
 
@@ -72,7 +73,7 @@ def upload_data(
         *,
         resource_type: ResourceType = ResourceType.LOCAL,
         pbar: Optional[ProgressReporter] = None,
-        params: Optional[Dict[str, Any]] = None,
+        params: Optional[dict[str, Any]] = None,
         wait_for_completion: bool = True,
         status_check_period: Optional[int] = None,
     ) -> None:
@@ -226,7 +227,7 @@ def download_frames(
         outdir: StrPath = ".",
         quality: str = "original",
         filename_pattern: str = "frame_{frame_id:06d}{frame_ext}",
-    ) -> Optional[List[Image.Image]]:
+    ) -> Optional[list[Image.Image]]:
         """
         Download the requested frame numbers for a task and save images as outdir/filename_pattern
         """
@@ -253,7 +254,7 @@ def download_frames(
             outfile = filename_pattern.format(frame_id=frame_id, frame_ext=im_ext)
             im.save(outdir / outfile)
 
-    def get_jobs(self) -> List[Job]:
+    def get_jobs(self) -> list[Job]:
         return [
             Job(self._client, model=m)
             for m in get_paginated_collection(
@@ -265,12 +266,12 @@ def get_meta(self) -> models.IDataMetaRead:
         (meta, _) = self.api.retrieve_data_meta(self.id)
         return meta
 
-    def get_labels(self) -> List[models.ILabel]:
+    def get_labels(self) -> list[models.ILabel]:
         return get_paginated_collection(
             self._client.api_client.labels_api.list_endpoint, task_id=self.id
         )
 
-    def get_frames_info(self) -> List[models.IFrameMeta]:
+    def get_frames_info(self) -> list[models.IFrameMeta]:
         return self.get_meta().frames
 
     def remove_frames_by_ids(self, ids: Sequence[int]) -> None:
@@ -295,7 +296,7 @@ def create_from_data(
         resources: Sequence[StrPath],
         *,
         resource_type: ResourceType = ResourceType.LOCAL,
-        data_params: Optional[Dict[str, Any]] = None,
+        data_params: Optional[dict[str, Any]] = None,
         annotation_path: str = "",
         annotation_format: str = "CVAT XML 1.1",
         status_check_period: int = None,
diff --git a/cvat-sdk/cvat_sdk/core/uploading.py b/cvat-sdk/cvat_sdk/core/uploading.py
index 0ccfd902da61..068e4d89a0c3 100644
--- a/cvat-sdk/cvat_sdk/core/uploading.py
+++ b/cvat-sdk/cvat_sdk/core/uploading.py
@@ -6,8 +6,9 @@
 
 import json
 import os
+from contextlib import AbstractContextManager
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, ContextManager, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Optional
 
 import requests
 import urllib3
@@ -147,9 +148,9 @@ def upload_file(
         url: str,
         filename: Path,
         *,
-        meta: Dict[str, Any],
-        query_params: Dict[str, Any] = None,
-        fields: Optional[Dict[str, Any]] = None,
+        meta: dict[str, Any],
+        query_params: dict[str, Any] = None,
+        fields: Optional[dict[str, Any]] = None,
         pbar: Optional[ProgressReporter] = None,
         logger=None,
     ) -> urllib3.HTTPResponse:
@@ -194,7 +195,7 @@ def upload_file(
         return self._tus_finish_upload(url, query_params=query_params, fields=fields)
 
     @staticmethod
-    def _uploading_task(pbar: ProgressReporter, total_size: int) -> ContextManager[None]:
+    def _uploading_task(pbar: ProgressReporter, total_size: int) -> AbstractContextManager[None]:
         return pbar.task(
             total=total_size, desc="Uploading data", unit_scale=True, unit="B", unit_divisor=1024
         )
@@ -256,7 +257,7 @@ def upload_file_and_wait(
         filename: Path,
         format_name: str,
         *,
-        url_params: Optional[Dict[str, Any]] = None,
+        url_params: Optional[dict[str, Any]] = None,
         pbar: Optional[ProgressReporter] = None,
         status_check_period: Optional[int] = None,
     ):
@@ -279,7 +280,7 @@ def upload_file_and_wait(
         filename: Path,
         format_name: str,
         *,
-        url_params: Optional[Dict[str, Any]] = None,
+        url_params: Optional[dict[str, Any]] = None,
         pbar: Optional[ProgressReporter] = None,
         status_check_period: Optional[int] = None,
     ):
@@ -302,7 +303,7 @@ def __init__(self, client: Client, *, max_request_size: int = MAX_REQUEST_SIZE):
     def upload_files(
         self,
         url: str,
-        resources: List[Path],
+        resources: list[Path],
         *,
         pbar: Optional[ProgressReporter] = None,
         **kwargs,
@@ -351,10 +352,10 @@ def upload_files(
         return self._tus_finish_upload(url, fields=kwargs)
 
     def _split_files_by_requests(
-        self, filenames: List[Path]
-    ) -> Tuple[List[Tuple[List[Path], int]], List[Path], int]:
-        bulk_files: Dict[str, int] = {}
-        separate_files: Dict[str, int] = {}
+        self, filenames: list[Path]
+    ) -> tuple[list[tuple[list[Path], int]], list[Path], int]:
+        bulk_files: dict[str, int] = {}
+        separate_files: dict[str, int] = {}
         max_request_size = self.max_request_size
 
         # sort by size
@@ -369,9 +370,9 @@ def _split_files_by_requests(
         total_size = sum(bulk_files.values()) + sum(separate_files.values())
 
         # group small files by requests
-        bulk_file_groups: List[Tuple[List[str], int]] = []
+        bulk_file_groups: list[tuple[list[str], int]] = []
         current_group_size: int = 0
-        current_group: List[str] = []
+        current_group: list[str] = []
         for filename, file_size in bulk_files.items():
             if max_request_size < current_group_size + file_size:
                 bulk_file_groups.append((current_group, current_group_size))
diff --git a/cvat-sdk/cvat_sdk/core/utils.py b/cvat-sdk/cvat_sdk/core/utils.py
index 1ef434e3ad5b..efcc787d96de 100644
--- a/cvat-sdk/cvat_sdk/core/utils.py
+++ b/cvat-sdk/cvat_sdk/core/utils.py
@@ -7,37 +7,26 @@
 import contextlib
 import itertools
 import os
-from typing import (
-    IO,
-    Any,
-    BinaryIO,
-    ContextManager,
-    Dict,
-    Generator,
-    Literal,
-    Sequence,
-    TextIO,
-    Union,
-    overload,
-)
+from collections.abc import Generator, Sequence
+from typing import IO, Any, BinaryIO, Literal, TextIO, Union, overload
 
 
 def filter_dict(
-    d: Dict[str, Any], *, keep: Sequence[str] = None, drop: Sequence[str] = None
-) -> Dict[str, Any]:
+    d: dict[str, Any], *, keep: Sequence[str] = None, drop: Sequence[str] = None
+) -> dict[str, Any]:
     return {k: v for k, v in d.items() if (not keep or k in keep) and (not drop or k not in drop)}
 
 
 @overload
 def atomic_writer(
     path: Union[os.PathLike, str], mode: Literal["wb"]
-) -> ContextManager[BinaryIO]: ...
+) -> contextlib.AbstractContextManager[BinaryIO]: ...
 
 
 @overload
 def atomic_writer(
     path: Union[os.PathLike, str], mode: Literal["w"], encoding: str = "UTF-8"
-) -> ContextManager[TextIO]: ...
+) -> contextlib.AbstractContextManager[TextIO]: ...
 
 
 @contextlib.contextmanager
diff --git a/cvat-sdk/cvat_sdk/datasets/caching.py b/cvat-sdk/cvat_sdk/datasets/caching.py
index 08e0c123bfe1..f47cdfc3260f 100644
--- a/cvat-sdk/cvat_sdk/datasets/caching.py
+++ b/cvat-sdk/cvat_sdk/datasets/caching.py
@@ -6,9 +6,10 @@
 import json
 import shutil
 from abc import ABCMeta, abstractmethod
+from collections.abc import Mapping
 from enum import Enum, auto
 from pathlib import Path
-from typing import Any, Callable, Dict, List, Mapping, Type, TypeVar, Union, cast
+from typing import Any, Callable, TypeVar, Union, cast
 
 from attrs import define
 
@@ -39,7 +40,7 @@ class UpdatePolicy(Enum):
     """
 
 
-_CacheObject = Dict[str, Any]
+_CacheObject = dict[str, Any]
 
 
 class _CacheObjectModel(metaclass=ABCMeta):
@@ -106,7 +107,7 @@ def _serialize_model(self, model: _ModelType) -> _CacheObject:
         else:
             raise NotImplementedError("Unexpected model type")
 
-    def load_model(self, path: Path, model_type: Type[_ModelType]) -> _ModelType:
+    def load_model(self, path: Path, model_type: type[_ModelType]) -> _ModelType:
         return self._deserialize_model(self._load_object(path), model_type)
 
     def save_model(self, path: Path, model: _ModelType) -> None:
@@ -120,7 +121,7 @@ def ensure_task_model(
         self,
         task_id: int,
         filename: str,
-        model_type: Type[_ModelType],
+        model_type: type[_ModelType],
         downloader: Callable[[], _ModelType],
         model_description: str,
     ) -> _ModelType: ...
@@ -166,7 +167,7 @@ def ensure_task_model(
         self,
         task_id: int,
         filename: str,
-        model_type: Type[_ModelType],
+        model_type: type[_ModelType],
         downloader: Callable[[], _ModelType],
         model_description: str,
     ) -> _ModelType:
@@ -225,7 +226,7 @@ def ensure_task_model(
         self,
         task_id: int,
         filename: str,
-        model_type: Type[_ModelType],
+        model_type: type[_ModelType],
         downloader: Callable[[], _ModelType],
         model_description: str,
     ) -> _ModelType:
@@ -247,7 +248,7 @@ def retrieve_project(self, project_id: int) -> Project:
 @define
 class _OfflineTaskModel(_CacheObjectModel):
     api_model: models.ITaskRead
-    labels: List[models.ILabel]
+    labels: list[models.ILabel]
 
     def dump(self) -> _CacheObject:
         return {
@@ -278,15 +279,15 @@ def __init__(
         self._offline_model = cached_model
         self._cache_manager = cache_manager
 
-    def get_labels(self) -> List[models.ILabel]:
+    def get_labels(self) -> list[models.ILabel]:
         return self._offline_model.labels
 
 
 @define
 class _OfflineProjectModel(_CacheObjectModel):
     api_model: models.IProjectRead
-    task_ids: List[int]
-    labels: List[models.ILabel]
+    task_ids: list[int]
+    labels: list[models.ILabel]
 
     def dump(self) -> _CacheObject:
         return {
@@ -320,14 +321,14 @@ def __init__(
         self._offline_model = cached_model
         self._cache_manager = cache_manager
 
-    def get_tasks(self) -> List[Task]:
+    def get_tasks(self) -> list[Task]:
         return [self._cache_manager.retrieve_task(t) for t in self._offline_model.task_ids]
 
-    def get_labels(self) -> List[models.ILabel]:
+    def get_labels(self) -> list[models.ILabel]:
         return self._offline_model.labels
 
 
-_CACHE_MANAGER_CLASSES: Mapping[UpdatePolicy, Type[CacheManager]] = {
+_CACHE_MANAGER_CLASSES: Mapping[UpdatePolicy, type[CacheManager]] = {
     UpdatePolicy.IF_MISSING_OR_STALE: _CacheManagerOnline,
     UpdatePolicy.NEVER: _CacheManagerOffline,
 }
diff --git a/cvat-sdk/cvat_sdk/datasets/common.py b/cvat-sdk/cvat_sdk/datasets/common.py
index b407c490802c..9b816e688bf4 100644
--- a/cvat-sdk/cvat_sdk/datasets/common.py
+++ b/cvat-sdk/cvat_sdk/datasets/common.py
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: MIT
 
 import abc
-from typing import List, Optional
+from typing import Optional
 
 import attrs
 import attrs.validators
@@ -24,8 +24,8 @@ class FrameAnnotations:
     Contains annotations that pertain to a single frame.
     """
 
-    tags: List[models.LabeledImage] = attrs.Factory(list)
-    shapes: List[models.LabeledShape] = attrs.Factory(list)
+    tags: list[models.LabeledImage] = attrs.Factory(list)
+    shapes: list[models.LabeledShape] = attrs.Factory(list)
 
 
 class MediaElement(metaclass=abc.ABCMeta):
diff --git a/cvat-sdk/cvat_sdk/datasets/task_dataset.py b/cvat-sdk/cvat_sdk/datasets/task_dataset.py
index cf66fa7ab0ea..68424cbb3815 100644
--- a/cvat-sdk/cvat_sdk/datasets/task_dataset.py
+++ b/cvat-sdk/cvat_sdk/datasets/task_dataset.py
@@ -5,8 +5,8 @@
 from __future__ import annotations
 
 import zipfile
+from collections.abc import Iterable, Sequence
 from concurrent.futures import ThreadPoolExecutor
-from typing import Iterable, Sequence
 
 import PIL.Image
 
diff --git a/cvat-sdk/cvat_sdk/pytorch/common.py b/cvat-sdk/cvat_sdk/pytorch/common.py
index 97ef38bc33a8..0c208cfc0bd4 100644
--- a/cvat-sdk/cvat_sdk/pytorch/common.py
+++ b/cvat-sdk/cvat_sdk/pytorch/common.py
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: MIT
 
-from typing import Mapping
+from collections.abc import Mapping
 
 import attrs
 
diff --git a/cvat-sdk/cvat_sdk/pytorch/project_dataset.py b/cvat-sdk/cvat_sdk/pytorch/project_dataset.py
index ada554ee1210..7548d9e233a0 100644
--- a/cvat-sdk/cvat_sdk/pytorch/project_dataset.py
+++ b/cvat-sdk/cvat_sdk/pytorch/project_dataset.py
@@ -3,7 +3,8 @@
 # SPDX-License-Identifier: MIT
 
 import os
-from typing import Callable, Container, Mapping, Optional
+from collections.abc import Container, Mapping
+from typing import Callable, Optional
 
 import torch
 import torch.utils.data
diff --git a/cvat-sdk/cvat_sdk/pytorch/task_dataset.py b/cvat-sdk/cvat_sdk/pytorch/task_dataset.py
index 8964d2db47db..8434102d9e63 100644
--- a/cvat-sdk/cvat_sdk/pytorch/task_dataset.py
+++ b/cvat-sdk/cvat_sdk/pytorch/task_dataset.py
@@ -4,7 +4,8 @@
 
 import os
 import types
-from typing import Callable, Mapping, Optional
+from collections.abc import Mapping
+from typing import Callable, Optional
 
 import torchvision.datasets
 
diff --git a/cvat-sdk/cvat_sdk/pytorch/transforms.py b/cvat-sdk/cvat_sdk/pytorch/transforms.py
index 1fb99362defc..5c8a4f7390cb 100644
--- a/cvat-sdk/cvat_sdk/pytorch/transforms.py
+++ b/cvat-sdk/cvat_sdk/pytorch/transforms.py
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: MIT
 
-from typing import FrozenSet, TypedDict
+from typing import TypedDict
 
 import attrs
 import attrs.validators
@@ -63,7 +63,7 @@ class ExtractBoundingBoxes:
     * Rotated shapes are not supported.
     """
 
-    include_shape_types: FrozenSet[str] = attrs.field(
+    include_shape_types: frozenset[str] = attrs.field(
         converter=frozenset,
         validator=attrs.validators.deep_iterable(attrs.validators.in_(_SUPPORTED_SHAPE_TYPES)),
         kw_only=True,
diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh
index c506564999b6..ca9a08be98fe 100755
--- a/cvat-sdk/gen/generate.sh
+++ b/cvat-sdk/gen/generate.sh
@@ -8,7 +8,7 @@ set -e
 
 GENERATOR_VERSION="v6.0.1"
 
-VERSION="2.21.3"
+VERSION="2.22.0"
 LIB_NAME="cvat_sdk"
 LAYER1_LIB_NAME="${LIB_NAME}/api_client"
 DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)"
diff --git a/cvat-sdk/gen/postprocess.py b/cvat-sdk/gen/postprocess.py
index 779dc2e10326..d45d9680dc2f 100755
--- a/cvat-sdk/gen/postprocess.py
+++ b/cvat-sdk/gen/postprocess.py
@@ -30,8 +30,8 @@ def collect_operations(schema):
 
 
 class Replacer:
-    REPLACEMENT_TOKEN = r"%%%"
-    ARGS_TOKEN = r"!!!"
+    REPLACEMENT_TOKEN = r"%%%"  # nosec: hardcoded_password_string
+    ARGS_TOKEN = r"!!!"  # nosec: hardcoded_password_string
 
     def __init__(self, schema):
         self._schema = schema
@@ -57,9 +57,9 @@ def make_api_name(self, name: str) -> str:
         return underscore(name)
 
     def make_type_annotation(self, type_repr: str) -> str:
-        type_repr = type_repr.replace("[", "typing.List[")
+        type_repr = type_repr.replace("[", "list[")
         type_repr = type_repr.replace("(", "typing.Union[").replace(")", "]")
-        type_repr = type_repr.replace("{", "typing.Dict[").replace(":", ",").replace("}", "]")
+        type_repr = type_repr.replace("{", "dict[").replace(":", ",").replace("}", "]")
 
         ANY_pattern = "bool, date, datetime, dict, float, int, list, str"
         type_repr = type_repr.replace(ANY_pattern, "typing.Any")
diff --git a/cvat-sdk/gen/templates/openapi-generator/api.mustache b/cvat-sdk/gen/templates/openapi-generator/api.mustache
index 160d641bc305..aa7c3ac686a1 100644
--- a/cvat-sdk/gen/templates/openapi-generator/api.mustache
+++ b/cvat-sdk/gen/templates/openapi-generator/api.mustache
@@ -240,10 +240,10 @@ class {{classname}}(object):
         _spec_property_naming: bool = False,
         _content_type: typing.Optional[str] = None,
         _host_index: typing.Optional[int] = None,
-        _request_auths: typing.Optional[typing.List] = None,
+        _request_auths: typing.Optional[list] = None,
         _async_call: bool = False,
         **kwargs,
-    ) -> typing.Tuple[typing.Optional[{{>return_type}}], urllib3.HTTPResponse]:
+    ) -> tuple[typing.Optional[{{>return_type}}], urllib3.HTTPResponse]:
         """{{{summary}}}{{^summary}}{{>operation_name}}{{/summary}}  # noqa: E501
 
 {{#notes}}
diff --git a/cvat-sdk/gen/templates/openapi-generator/api_client.mustache b/cvat-sdk/gen/templates/openapi-generator/api_client.mustache
index 436bd26f2d54..d49af604ce94 100644
--- a/cvat-sdk/gen/templates/openapi-generator/api_client.mustache
+++ b/cvat-sdk/gen/templates/openapi-generator/api_client.mustache
@@ -68,8 +68,8 @@ class ApiClient(object):
 
     def __init__(self,
             configuration: typing.Optional[Configuration] = None,
-            headers: typing.Optional[typing.Dict[str, str]] = None,
-            cookies: typing.Optional[typing.Dict[str, str]] = None,
+            headers: typing.Optional[dict[str, str]] = None,
+            cookies: typing.Optional[dict[str, str]] = None,
             pool_threads: int = 1):
         """
         :param configuration: configuration object for this client
@@ -85,7 +85,7 @@ class ApiClient(object):
         self.pool_threads = pool_threads
 
         self.rest_client = rest.RESTClientObject(configuration)
-        self.default_headers: typing.Dict[str, str] = headers or {}
+        self.default_headers: dict[str, str] = headers or {}
         self.cookies = SimpleCookie()
         if cookies:
             self.cookies.update(cookies)
@@ -161,22 +161,22 @@ class ApiClient(object):
         self,
         resource_path: str,
         method: str,
-        path_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
-        query_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
-        header_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
+        path_params: typing.Optional[dict[str, typing.Any]] = None,
+        query_params: typing.Optional[list[tuple[str, typing.Any]]] = None,
+        header_params: typing.Optional[dict[str, typing.Any]] = None,
         body: typing.Optional[typing.Any] = None,
-        post_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
-        files: typing.Optional[typing.Dict[str, typing.List[io.IOBase]]] = None,
-        response_schema: typing.Optional[typing.Tuple[typing.Any]] = None,
-        auth_settings: typing.Optional[typing.List[str]] = None,
-        collection_formats: typing.Optional[typing.Dict[str, str]] = None,
+        post_params: typing.Optional[list[tuple[str, typing.Any]]] = None,
+        files: typing.Optional[dict[str, list[io.IOBase]]] = None,
+        response_schema: typing.Optional[tuple[typing.Any]] = None,
+        auth_settings: typing.Optional[list[str]] = None,
+        collection_formats: typing.Optional[dict[str, str]] = None,
         *,
         _parse_response: bool = True,
-        _request_timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None,
+        _request_timeout: typing.Optional[typing.Union[int, float, tuple]] = None,
         _host: typing.Optional[str] = None,
         _check_type: typing.Optional[bool] = None,
         _check_status: bool = True,
-        _request_auths: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] = None
+        _request_auths: typing.Optional[list[dict[str, typing.Any]]] = None
     ):
 
         config = self.configuration
@@ -271,7 +271,7 @@ class ApiClient(object):
         return (return_data, response)
 {{/tornado}}
 
-    def get_common_headers(self) -> typing.Dict[str, str]:
+    def get_common_headers(self) -> dict[str, str]:
         """
         Returns a headers dict with all the required headers for requests
         """
@@ -324,7 +324,7 @@ class ApiClient(object):
         """
         return to_json(obj, read_files=read_files)
 
-    def deserialize(self, response: HTTPResponse, response_schema: typing.Tuple, *, _check_type: bool):
+    def deserialize(self, response: HTTPResponse, response_schema: tuple, *, _check_type: bool):
         """Deserializes response into an object.
 
         :param response (urllib3.HTTPResponse): object to be deserialized.
@@ -384,22 +384,22 @@ class ApiClient(object):
         self,
         resource_path: str,
         method: str,
-        path_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
-        query_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
-        header_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
+        path_params: typing.Optional[dict[str, typing.Any]] = None,
+        query_params: typing.Optional[list[tuple[str, typing.Any]]] = None,
+        header_params: typing.Optional[dict[str, typing.Any]] = None,
         body: typing.Optional[typing.Any] = None,
-        post_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
-        files: typing.Optional[typing.Dict[str, typing.List[io.IOBase]]] = None,
-        response_schema: typing.Optional[typing.Tuple[typing.Any]] = None,
-        auth_settings: typing.Optional[typing.List[str]] = None,
-        collection_formats: typing.Optional[typing.Dict[str, str]] = None,
+        post_params: typing.Optional[list[tuple[str, typing.Any]]] = None,
+        files: typing.Optional[dict[str, list[io.IOBase]]] = None,
+        response_schema: typing.Optional[tuple[typing.Any]] = None,
+        auth_settings: typing.Optional[list[str]] = None,
+        collection_formats: typing.Optional[dict[str, str]] = None,
         *,
         _async_call: typing.Optional[bool] = None,
         _parse_response: bool = True,
-        _request_timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None,
+        _request_timeout: typing.Optional[typing.Union[int, float, tuple]] = None,
         _host: typing.Optional[str] = None,
         _check_type: typing.Optional[bool] = None,
-        _request_auths: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] = None,
+        _request_auths: typing.Optional[list[dict[str, typing.Any]]] = None,
         _check_status: bool = True,
     ):
         """Makes the HTTP request (synchronous) and returns deserialized data.
@@ -580,7 +580,7 @@ class ApiClient(object):
                 new_params.append((k, v))
         return new_params
 
-    def _serialize_file(self, file_instance: io.IOBase) -> typing.Tuple[str, typing.Union[str, bytes], str]:
+    def _serialize_file(self, file_instance: io.IOBase) -> tuple[str, typing.Union[str, bytes], str]:
         if file_instance.closed is True:
             raise ApiValueError("Cannot read a closed file.")
         filename = os.path.basename(file_instance.name)
@@ -592,8 +592,7 @@ class ApiClient(object):
         return filename, filedata, mimetype
 
     def files_parameters(self,
-                         files: typing.Optional[typing.Dict[str,
-                                                            typing.List[io.IOBase]]] = None):
+                         files: typing.Optional[dict[str, list[io.IOBase]]] = None):
         """Builds form parameters.
 
         :param files: None or a dict with key=param_name and
@@ -714,7 +713,7 @@ class ApiClient(object):
 {{#apiInfo}}{{#apis}}
     {{>api_name}}: '{{classname}}'{{/apis}}{{/apiInfo}}
 
-    _apis: typing.Dict[str, object] = { {{#apiInfo}}{{#apis}}
+    _apis: dict[str, object] = { {{#apiInfo}}{{#apis}}
         '{{>api_name}}': [None, '{{classname}}'],{{/apis}}{{/apiInfo}}
     }
 
@@ -739,10 +738,10 @@ class ApiClient(object):
 
 class Endpoint(object):
     def __init__(self,
-            settings: typing.Optional[typing.Dict[str, typing.Any]] = None,
-            params_map: typing.Optional[typing.Dict[str, typing.Any]] = None,
-            root_map: typing.Optional[typing.Dict[str, typing.Any]] = None,
-            headers_map: typing.Optional[typing.Dict[str, typing.Any]] = None,
+            settings: typing.Optional[dict[str, typing.Any]] = None,
+            params_map: typing.Optional[dict[str, typing.Any]] = None,
+            root_map: typing.Optional[dict[str, typing.Any]] = None,
+            headers_map: typing.Optional[dict[str, typing.Any]] = None,
             api_client: typing.Optional[ApiClient] = None
     ):
         """Creates an endpoint
@@ -897,9 +896,9 @@ class Endpoint(object):
             _spec_property_naming: bool = False,
             _content_type: typing.Optional[str] = None,
             _host_index: typing.Optional[int] = None,
-            _request_auths: typing.Optional[typing.List] = None,
+            _request_auths: typing.Optional[list] = None,
             _async_call: bool = False,
-            **kwargs) -> typing.Tuple[typing.Optional[typing.Any], HTTPResponse]:
+            **kwargs) -> tuple[typing.Optional[typing.Any], HTTPResponse]:
         """
         Keyword Args:
             endpoint args
diff --git a/cvat-sdk/gen/templates/openapi-generator/configuration.mustache b/cvat-sdk/gen/templates/openapi-generator/configuration.mustache
index cec0c548f1d7..e66aec294afc 100644
--- a/cvat-sdk/gen/templates/openapi-generator/configuration.mustache
+++ b/cvat-sdk/gen/templates/openapi-generator/configuration.mustache
@@ -169,8 +169,8 @@ class Configuration:
 
     def __init__(self,
         host: typing.Optional[str] = None,
-        api_key: typing.Optional[typing.Dict[str, str]] = None,
-        api_key_prefix: typing.Optional[typing.Dict[str, str]] = None,
+        api_key: typing.Optional[dict[str, str]] = None,
+        api_key_prefix: typing.Optional[dict[str, str]] = None,
         username: typing.Optional[str] = None,
         password: typing.Optional[str]=None,
         discard_unknown_keys: bool = False,
@@ -179,9 +179,9 @@ class Configuration:
         signing_info=None,
 {{/hasHttpSignatureMethods}}
         server_index: typing.Optional[int] = None,
-        server_variables: typing.Optional[typing.Dict[str, str]] = None,
+        server_variables: typing.Optional[dict[str, str]] = None,
         server_operation_index: typing.Optional[int] = None,
-        server_operation_variables: typing.Optional[typing.Dict[str, str]] = None,
+        server_operation_variables: typing.Optional[dict[str, str]] = None,
         ssl_ca_cert: typing.Optional[str] = None,
         verify_ssl: typing.Optional[bool] = None,
     ) -> None:
diff --git a/cvat-ui/package.json b/cvat-ui/package.json
index 2c43904a3fb9..a74485fa107d 100644
--- a/cvat-ui/package.json
+++ b/cvat-ui/package.json
@@ -1,6 +1,6 @@
 {
   "name": "cvat-ui",
-  "version": "1.66.2",
+  "version": "1.66.4",
   "description": "CVAT single-page application",
   "main": "src/index.tsx",
   "scripts": {
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index 670ace099e5a..0cc8f3052bc0 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -126,6 +126,8 @@ export enum AnnotationActionTypes {
     COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
     COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS',
     ACTIVATE_OBJECT = 'ACTIVATE_OBJECT',
+    UPDATE_EDITED_STATE = 'UPDATE_EDITED_STATE',
+    HIDE_ACTIVE_OBJECT = 'HIDE_ACTIVE_OBJECT',
     REMOVE_OBJECT = 'REMOVE_OBJECT',
     REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS',
     REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED',
@@ -450,6 +452,7 @@ export function propagateObjectAsync(from: number, to: number): ThunkAction {
         const {
             job: {
                 instance: sessionInstance,
+                frameNumbers,
             },
             annotations: {
                 activatedStateID,
@@ -463,12 +466,17 @@ export function propagateObjectAsync(from: number, to: number): ThunkAction {
                 throw new Error('There is not an activated object state to be propagated');
             }
 
-            await sessionInstance.logger.log(EventScope.propagateObject, { count: Math.abs(to - from) });
-            const states = cvat.utils.propagateShapes<ObjectState>([objectState], from, to);
+            if (!sessionInstance) {
+                throw new Error('SessionInstance is not defined, propagation is not possible');
+            }
 
-            await sessionInstance.annotations.put(states);
-            const history = await sessionInstance.actions.get();
+            const states = cvat.utils.propagateShapes<ObjectState>([objectState], from, to, frameNumbers);
+            if (states.length) {
+                await sessionInstance.logger.log(EventScope.propagateObject, { count: states.length });
+                await sessionInstance.annotations.put(states);
+            }
 
+            const history = await sessionInstance.actions.get();
             dispatch({
                 type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS,
                 payload: { history },
@@ -594,10 +602,10 @@ export function confirmCanvasReadyAsync(): ThunkAction {
     return async (dispatch: ThunkDispatch, getState: () => CombinedState): Promise<void> => {
         try {
             const state: CombinedState = getState();
-            const { instance: job } = state.annotation.job;
+            const job = state.annotation.job.instance as Job;
+            const includedFrames = state.annotation.job.frameNumbers;
             const { changeFrameEvent } = state.annotation.player.frame;
             const chunks = await job.frames.cachedChunks() as number[];
-            const includedFrames = await job.frames.frameNumbers() as number[];
             const { frameCount, dataChunkSize } = job;
 
             const ranges = chunks.map((chunk) => (
@@ -914,7 +922,6 @@ export function getJobAsync({
                 }
             }
 
-            const jobMeta = await cvat.frames.getMeta('job', job.id);
             // frame query parameter does not work for GT job
             const frameNumber = Number.isInteger(initialFrame) && gtJob?.id !== job.id ?
                 initialFrame as number :
@@ -923,6 +930,8 @@ export function getJobAsync({
                 )) || job.startFrame;
 
             const frameData = await job.frames.get(frameNumber);
+            const jobMeta = await cvat.frames.getMeta('job', job.id);
+            const frameNumbers = await job.frames.frameNumbers();
             try {
                 // call first getting of frame data before rendering interface
                 // to load and decode first chunk
@@ -960,6 +969,7 @@ export function getJobAsync({
                 payload: {
                     openTime,
                     job,
+                    frameNumbers,
                     jobMeta,
                     queryParameters,
                     groundTruthInstance: gtJob || null,
@@ -1320,7 +1330,7 @@ export function searchAnnotationsAsync(
     };
 }
 
-const ShapeTypeToControl: Record<ShapeType, ActiveControl> = {
+export const ShapeTypeToControl: Record<ShapeType, ActiveControl> = {
     [ShapeType.RECTANGLE]: ActiveControl.DRAW_RECTANGLE,
     [ShapeType.POLYLINE]: ActiveControl.DRAW_POLYLINE,
     [ShapeType.POLYGON]: ActiveControl.DRAW_POLYGON,
@@ -1608,3 +1618,50 @@ export function restoreFrameAsync(frame: number): ThunkAction {
         }
     };
 }
+
+export function changeHideActiveObjectAsync(hide: boolean): ThunkAction {
+    return async (dispatch: ThunkDispatch, getState): Promise<void> => {
+        const state = getState();
+        const { instance: canvas } = state.annotation.canvas;
+        if (canvas) {
+            (canvas as Canvas).configure({
+                hideEditedObject: hide,
+            });
+
+            const { objectState } = state.annotation.editing;
+            if (objectState) {
+                objectState.hidden = hide;
+                await dispatch(updateAnnotationsAsync([objectState]));
+            }
+
+            dispatch({
+                type: AnnotationActionTypes.HIDE_ACTIVE_OBJECT,
+                payload: {
+                    hide,
+                },
+            });
+        }
+    };
+}
+
+export function updateEditedStateAsync(objectState: ObjectState | null): ThunkAction {
+    return async (dispatch: ThunkDispatch, getState): Promise<void> => {
+        let newActiveObjectHidden = false;
+        if (objectState) {
+            newActiveObjectHidden = objectState.hidden;
+        }
+
+        dispatch({
+            type: AnnotationActionTypes.UPDATE_EDITED_STATE,
+            payload: {
+                objectState,
+            },
+        });
+
+        const state = getState();
+        const { activeObjectHidden } = state.annotation.canvas;
+        if (activeObjectHidden !== newActiveObjectHidden) {
+            dispatch(changeHideActiveObjectAsync(newActiveObjectHidden));
+        }
+    };
+}
diff --git a/cvat-ui/src/actions/export-actions.ts b/cvat-ui/src/actions/export-actions.ts
index db59a6315c6a..6872e4f853c0 100644
--- a/cvat-ui/src/actions/export-actions.ts
+++ b/cvat-ui/src/actions/export-actions.ts
@@ -4,11 +4,12 @@
 // SPDX-License-Identifier: MIT
 
 import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
-
-import { Storage, ProjectOrTaskOrJob, Job } from 'cvat-core-wrapper';
 import {
-    getInstanceType, RequestInstanceType, listen, RequestsActions,
-    shouldListenForProgress,
+    Storage, ProjectOrTaskOrJob, Job, getCore, StorageLocation,
+} from 'cvat-core-wrapper';
+import {
+    getInstanceType, RequestInstanceType, listen,
+    RequestsActions, updateRequestProgress,
 } from './requests-actions';
 
 export enum ExportActionTypes {
@@ -24,6 +25,8 @@ export enum ExportActionTypes {
     EXPORT_BACKUP_FAILED = 'EXPORT_BACKUP_FAILED',
 }
 
+const core = getCore();
+
 export const exportActions = {
     openExportDatasetModal: (instance: ProjectOrTaskOrJob) => (
         createAction(ExportActionTypes.OPEN_EXPORT_DATASET_MODAL, { instance })
@@ -36,7 +39,7 @@ export const exportActions = {
         instanceType: 'project' | 'task' | 'job',
         format: string,
         resource: 'dataset' | 'annotations',
-        target?: 'local' | 'cloudstorage',
+        target?: StorageLocation,
     ) => (
         createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, {
             instance,
@@ -67,7 +70,7 @@ export const exportActions = {
     closeExportBackupModal: (instance: ProjectOrTaskOrJob) => (
         createAction(ExportActionTypes.CLOSE_EXPORT_BACKUP_MODAL, { instance })
     ),
-    exportBackupSuccess: (instance: Exclude<ProjectOrTaskOrJob, Job> | RequestInstanceType, instanceType: 'task' | 'project', target?: 'local' | 'cloudstorage') => (
+    exportBackupSuccess: (instance: Exclude<ProjectOrTaskOrJob, Job> | RequestInstanceType, instanceType: 'task' | 'project', target?: StorageLocation) => (
         createAction(ExportActionTypes.EXPORT_BACKUP_SUCCESS, { instance, instanceType, target })
     ),
     exportBackupFailed: (instance: Exclude<ProjectOrTaskOrJob, Job> | RequestInstanceType, instanceType: 'task' | 'project', error: any) => (
@@ -75,30 +78,9 @@ export const exportActions = {
     ),
 };
 
-export async function listenExportDatasetAsync(
-    rqID: string,
-    dispatch: (action: ExportActions | RequestsActions) => void,
-    params: {
-        instance: ProjectOrTaskOrJob | RequestInstanceType,
-        format: string,
-        saveImages: boolean,
-    },
-): Promise<void> {
-    const { instance, format, saveImages } = params;
-    const resource = saveImages ? 'dataset' : 'annotations';
-
-    const instanceType = getInstanceType(instance);
-    try {
-        const result = await listen(rqID, dispatch);
-        const target = !result?.url ? 'cloudstorage' : 'local';
-        dispatch(exportActions.exportDatasetSuccess(
-            instance, instanceType, format, resource, target,
-        ));
-    } catch (error) {
-        dispatch(exportActions.exportDatasetFailed(instance, instanceType, format, resource, error));
-    }
-}
-
+/** *
+ * Function is supposed to be used when a new dataset export request initiated by a user
+** */
 export const exportDatasetAsync = (
     instance: ProjectOrTaskOrJob,
     format: string,
@@ -106,21 +88,23 @@ export const exportDatasetAsync = (
     useDefaultSettings: boolean,
     targetStorage: Storage,
     name?: string,
-): ThunkAction => async (dispatch, getState) => {
-    const state = getState();
-
+): ThunkAction => async (dispatch) => {
     const resource = saveImages ? 'dataset' : 'annotations';
     const instanceType = getInstanceType(instance);
 
     try {
         const rqID = await instance.annotations
             .exportDataset(format, saveImages, useDefaultSettings, targetStorage, name);
-        if (shouldListenForProgress(rqID, state.requests)) {
-            await listenExportDatasetAsync(rqID, dispatch, {
-                instance, format, saveImages,
+
+        if (rqID) {
+            await core.requests.listen(rqID, {
+                callback: (updatedRequest) => updateRequestProgress(updatedRequest, dispatch),
             });
-        }
-        if (!rqID) {
+            const target = targetStorage.location;
+            dispatch(exportActions.exportDatasetSuccess(
+                instance, instanceType, format, resource, target,
+            ));
+        } else {
             dispatch(exportActions.exportDatasetSuccess(
                 instance, instanceType, format, resource,
             ));
@@ -130,47 +114,79 @@ export const exportDatasetAsync = (
     }
 };
 
-export async function listenExportBackupAsync(
+/** *
+ * Function is supposed to be used when a new backup export request initiated by a user
+** */
+export const exportBackupAsync = (
+    instance: Exclude<ProjectOrTaskOrJob, Job>,
+    targetStorage: Storage,
+    useDefaultSetting: boolean,
+    fileName: string,
+): ThunkAction => async (dispatch) => {
+    const instanceType = getInstanceType(instance) as 'project' | 'task';
+    try {
+        const rqID = await instance.backup(targetStorage, useDefaultSetting, fileName);
+        if (rqID) {
+            await core.requests.listen(rqID, {
+                callback: (updatedRequest) => updateRequestProgress(updatedRequest, dispatch),
+            });
+            const target = targetStorage.location;
+            dispatch(exportActions.exportBackupSuccess(instance, instanceType, target));
+        } else {
+            dispatch(exportActions.exportBackupSuccess(instance, instanceType));
+        }
+    } catch (error) {
+        dispatch(exportActions.exportBackupFailed(instance, instanceType, error as Error));
+    }
+};
+
+/** *
+ * Function is supposed to be used when application starts listening to existing dataset export request
+** */
+export async function listenExportDatasetAsync(
     rqID: string,
     dispatch: (action: ExportActions | RequestsActions) => void,
     params: {
-        instance: Exclude<ProjectOrTaskOrJob, Job> | RequestInstanceType,
+        instance: ProjectOrTaskOrJob | RequestInstanceType,
+        format: string,
+        saveImages: boolean,
     },
 ): Promise<void> {
-    const { instance } = params;
-    const instanceType = getInstanceType(instance) as 'project' | 'task';
+    const { instance, format, saveImages } = params;
+    const resource = saveImages ? 'dataset' : 'annotations';
 
+    const instanceType = getInstanceType(instance);
     try {
         const result = await listen(rqID, dispatch);
-        const target = !result?.url ? 'cloudstorage' : 'local';
-        dispatch(exportActions.exportBackupSuccess(instance, instanceType, target));
+        const target = !result?.url ? StorageLocation.CLOUD_STORAGE : StorageLocation.LOCAL;
+        dispatch(exportActions.exportDatasetSuccess(
+            instance, instanceType, format, resource, target,
+        ));
     } catch (error) {
-        dispatch(exportActions.exportBackupFailed(instance, instanceType, error as Error));
+        dispatch(exportActions.exportDatasetFailed(instance, instanceType, format, resource, error));
     }
 }
 
-export const exportBackupAsync = (
-    instance: Exclude<ProjectOrTaskOrJob, Job>,
-    targetStorage: Storage,
-    useDefaultSetting: boolean,
-    fileName: string,
-): ThunkAction => async (dispatch, getState) => {
-    const state = getState();
-
+/** *
+ * Function is supposed to be used when application starts listening to existing backup export request
+** */
+export async function listenExportBackupAsync(
+    rqID: string,
+    dispatch: (action: ExportActions | RequestsActions) => void,
+    params: {
+        instance: Exclude<ProjectOrTaskOrJob, Job> | RequestInstanceType,
+    },
+): Promise<void> {
+    const { instance } = params;
     const instanceType = getInstanceType(instance) as 'project' | 'task';
 
     try {
-        const rqID = await instance
-            .backup(targetStorage, useDefaultSetting, fileName);
-        if (shouldListenForProgress(rqID, state.requests)) {
-            await listenExportBackupAsync(rqID, dispatch, { instance });
-        }
-        if (!rqID) {
-            dispatch(exportActions.exportBackupSuccess(instance, instanceType));
-        }
+        const result = await listen(rqID, dispatch);
+        const target = !result?.url ? StorageLocation.CLOUD_STORAGE : StorageLocation.LOCAL;
+        dispatch(exportActions.exportBackupSuccess(instance, instanceType, target));
     } catch (error) {
         dispatch(exportActions.exportBackupFailed(instance, instanceType, error as Error));
     }
-};
+}
 
 export type ExportActions = ActionUnion<typeof exportActions>;
diff --git a/cvat-ui/src/actions/import-actions.ts b/cvat-ui/src/actions/import-actions.ts
index e47db0b47818..d7e3a548bb3b 100644
--- a/cvat-ui/src/actions/import-actions.ts
+++ b/cvat-ui/src/actions/import-actions.ts
@@ -4,15 +4,14 @@
 // SPDX-License-Identifier: MIT
 
 import { createAction, ActionUnion, ThunkAction } from 'utils/redux';
-import { CombinedState } from 'reducers';
 import {
     getCore, Storage, Job, Task, Project, ProjectOrTaskOrJob,
 } from 'cvat-core-wrapper';
 import { getProjectsAsync } from './projects-actions';
 import { AnnotationActionTypes, fetchAnnotationsAsync } from './annotation-actions';
 import {
-    getInstanceType, listen, RequestInstanceType, RequestsActions,
-    shouldListenForProgress,
+    getInstanceType, listen, RequestInstanceType,
+    RequestsActions, updateRequestProgress,
 } from './requests-actions';
 
 const core = getCore();
@@ -69,25 +68,9 @@ export const importActions = {
     ),
 };
 
-export async function listenImportDatasetAsync(
-    rqID: string,
-    dispatch: (action: ImportActions | RequestsActions) => void,
-    params: {
-        instance: ProjectOrTaskOrJob | RequestInstanceType,
-    },
-): Promise<void> {
-    const { instance } = params;
-
-    const instanceType = getInstanceType(instance);
-    const resource = instanceType === 'project' ? 'dataset' : 'annotation';
-    try {
-        await listen(rqID, dispatch);
-        dispatch(importActions.importDatasetSuccess(instance, resource));
-    } catch (error) {
-        dispatch(importActions.importDatasetFailed(instance, resource, error));
-    }
-}
-
+/** *
+ * Function is supposed to be used when a new dataset import request initiated by a user
+** */
 export const importDatasetAsync = (
     instance: ProjectOrTaskOrJob,
     format: string,
@@ -100,55 +83,63 @@ export const importDatasetAsync = (
         const instanceType = getInstanceType(instance);
         const resource = instanceType === 'project' ? 'dataset' : 'annotation';
 
-        try {
-            const state: CombinedState = getState();
+        const listenForImport = (rqID: string) => core.requests.listen(rqID, {
+            callback: (updatedRequest) => updateRequestProgress(updatedRequest, dispatch),
+        });
 
+        try {
             if (instanceType === 'project') {
                 dispatch(importActions.importDataset(instance, format));
-                const rqID = await (instance as Project).annotations
-                    .importDataset(format, useDefaultSettings, sourceStorage, file, {
+                const rqID = await (instance as Project).annotations.importDataset(
+                    format,
+                    useDefaultSettings,
+                    sourceStorage,
+                    file,
+                    {
                         convMaskToPoly,
                         updateStatusCallback: (message: string, progress: number) => (
                             dispatch(importActions.importDatasetUpdateStatus(
                                 instance, Math.floor(progress * 100), message,
                             ))
                         ),
-                    });
-                if (shouldListenForProgress(rqID, state.requests)) {
-                    await listen(rqID, dispatch);
-                }
+                    },
+                );
+
+                await listenForImport(rqID);
             } else if (instanceType === 'task') {
                 dispatch(importActions.importDataset(instance, format));
-                const rqID = await (instance as Task).annotations
-                    .upload(format, useDefaultSettings, sourceStorage, file, {
-                        convMaskToPoly,
-                    });
-                if (shouldListenForProgress(rqID, state.requests)) {
-                    await listen(rqID, dispatch);
-                }
+                const rqID = await (instance as Task).annotations.upload(
+                    format,
+                    useDefaultSettings,
+                    sourceStorage,
+                    file,
+                    { convMaskToPoly },
+                );
+                await listenForImport(rqID);
             } else { // job
                 dispatch(importActions.importDataset(instance, format));
-                const rqID = await (instance as Job).annotations
-                    .upload(format, useDefaultSettings, sourceStorage, file, {
-                        convMaskToPoly,
+                const rqID = await (instance as Job).annotations.upload(
+                    format,
+                    useDefaultSettings,
+                    sourceStorage,
+                    file,
+                    { convMaskToPoly },
+                );
+
+                await listenForImport(rqID);
+                await (instance as Job).annotations.clear({ reload: true });
+                await (instance as Job).actions.clear();
+
+                // first set empty objects list
+                // to escape some problems in canvas when shape with the same
+                // clientID has different type (polygon, rectangle) for example
+                dispatch({ type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS });
+
+                const relevantInstance = getState().annotation.job.instance;
+                if (relevantInstance && relevantInstance.id === instance.id) {
+                    setTimeout(() => {
+                        dispatch(fetchAnnotationsAsync());
                     });
-                if (shouldListenForProgress(rqID, state.requests)) {
-                    await listen(rqID, dispatch);
-
-                    await (instance as Job).annotations.clear({ reload: true });
-                    await (instance as Job).actions.clear();
-
-                    // first set empty objects list
-                    // to escape some problems in canvas when shape with the same
-                    // clientID has different type (polygon, rectangle) for example
-                    dispatch({ type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS });
-
-                    const relevantInstance = getState().annotation.job.instance;
-                    if (relevantInstance && relevantInstance.id === instance.id) {
-                        setTimeout(() => {
-                            dispatch(fetchAnnotationsAsync());
-                        });
-                    }
                 }
             }
         } catch (error) {
@@ -163,6 +154,28 @@ export const importDatasetAsync = (
     }
 );
 
+/** *
+ * Function is supposed to be used when a new backup import request initiated by a user
+** */
+export const importBackupAsync = (instanceType: 'project' | 'task', storage: Storage, file: File | string): ThunkAction => (
+    async (dispatch) => {
+        dispatch(importActions.importBackup());
+        try {
+            const instanceClass = (instanceType === 'task') ? core.classes.Task : core.classes.Project;
+            const rqID = await instanceClass.restore(storage, file);
+            const result = await core.requests.listen(rqID, {
+                callback: (updatedRequest) => updateRequestProgress(updatedRequest, dispatch),
+            });
+            dispatch(importActions.importBackupSuccess(result?.resultID as number, instanceType));
+        } catch (error) {
+            dispatch(importActions.importBackupFailed(instanceType, error));
+        }
+    }
+);
+
+/** *
+ * Function is supposed to be used when application starts listening to existing backup import request
+** */
 export async function listenImportBackupAsync(
     rqID: string,
     dispatch: (action: ImportActions | RequestsActions) => void,
@@ -171,32 +184,34 @@ export async function listenImportBackupAsync(
     },
 ): Promise<void> {
     const { instanceType } = params;
-
     try {
         const result = await listen(rqID, dispatch);
-
-        dispatch(importActions.importBackupSuccess(result?.resultID, instanceType));
+        dispatch(importActions.importBackupSuccess(result?.resultID as number, instanceType));
     } catch (error) {
         dispatch(importActions.importBackupFailed(instanceType, error));
     }
 }
 
-export const importBackupAsync = (instanceType: 'project' | 'task', storage: Storage, file: File | string): ThunkAction => (
-    async (dispatch, getState) => {
-        const state: CombinedState = getState();
-
-        dispatch(importActions.importBackup());
+/** *
+ * Function is supposed to be used when application starts listening to existing dataset import request
+** */
+export async function listenImportDatasetAsync(
+    rqID: string,
+    dispatch: (action: ImportActions | RequestsActions) => void,
+    params: {
+        instance: ProjectOrTaskOrJob | RequestInstanceType,
+    },
+): Promise<void> {
+    const { instance } = params;
 
-        try {
-            const instanceClass = (instanceType === 'task') ? core.classes.Task : core.classes.Project;
-            const rqID = await instanceClass.restore(storage, file);
-            if (shouldListenForProgress(rqID, state.requests)) {
-                await listenImportBackupAsync(rqID, dispatch, { instanceType });
-            }
-        } catch (error) {
-            dispatch(importActions.importBackupFailed(instanceType, error));
-        }
+    const instanceType = getInstanceType(instance);
+    const resource = instanceType === 'project' ? 'dataset' : 'annotation';
+    try {
+        await listen(rqID, dispatch);
+        dispatch(importActions.importDatasetSuccess(instance, resource));
+    } catch (error) {
+        dispatch(importActions.importDatasetFailed(instance, resource, error));
     }
-);
+}
 
 export type ImportActions = ActionUnion<typeof importActions>;
diff --git a/cvat-ui/src/actions/jobs-actions.ts b/cvat-ui/src/actions/jobs-actions.ts
index 7c2df71a9270..e7d13e23b7f1 100644
--- a/cvat-ui/src/actions/jobs-actions.ts
+++ b/cvat-ui/src/actions/jobs-actions.ts
@@ -1,5 +1,5 @@
 // Copyright (C) 2022 Intel Corporation
-// Copyright (C) 2023 CVAT.ai Corporation
+// Copyright (C) 2023-2024 CVAT.ai Corporation
 //
 // SPDX-License-Identifier: MIT
 
@@ -34,7 +34,9 @@ interface JobsList extends Array<any> {
 }
 
 const jobsActions = {
-    getJobs: (query: Partial<JobsQuery>) => createAction(JobsActionTypes.GET_JOBS, { query }),
+    getJobs: (query: Partial<JobsQuery>, fetchingTimestamp: number) => (
+        createAction(JobsActionTypes.GET_JOBS, { query, fetchingTimestamp })
+    ),
     getJobsSuccess: (jobs: JobsList) => (
         createAction(JobsActionTypes.GET_JOBS_SUCCESS, { jobs })
     ),
@@ -73,16 +75,26 @@ const jobsActions = {
 
 export type JobsActions = ActionUnion<typeof jobsActions>;
 
-export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => {
+export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch, getState) => {
+    const requestedOn = Date.now();
+    const isRequestRelevant = (): boolean => (
+        getState().jobs.fetchingTimestamp === requestedOn
+    );
+
     try {
         // We remove all keys with null values from the query
         const filteredQuery = filterNull(query);
 
-        dispatch(jobsActions.getJobs(filteredQuery as JobsQuery));
+        dispatch(jobsActions.getJobs(filteredQuery as JobsQuery, requestedOn));
         const jobs = await cvat.jobs.get(filteredQuery);
-        dispatch(jobsActions.getJobsSuccess(jobs));
+
+        if (isRequestRelevant()) {
+            dispatch(jobsActions.getJobsSuccess(jobs));
+        }
     } catch (error) {
-        dispatch(jobsActions.getJobsFailed(error));
+        if (isRequestRelevant()) {
+            dispatch(jobsActions.getJobsFailed(error));
+        }
     }
 };
 
@@ -96,10 +108,20 @@ export const getJobPreviewAsync = (job: Job): ThunkAction => async (dispatch) =>
     }
 };
 
-export const createJobAsync = (data: JobData): ThunkAction => async (dispatch) => {
-    const jobInstance = new cvat.classes.Job(data);
+export const createJobAsync = (data: JobData): ThunkAction<Promise<Job>> => async (dispatch) => {
+    const initialData = {
+        type: data.type,
+        task_id: data.taskID,
+    };
+    const jobInstance = new cvat.classes.Job(initialData);
     try {
-        const savedJob = await jobInstance.save(data);
+        const extras = {
+            frame_selection_method: data.frameSelectionMethod,
+            seed: data.seed,
+            frame_count: data.frameCount,
+            frames_per_job_count: data.framesPerJobCount,
+        };
+        const savedJob = await jobInstance.save(extras);
         return savedJob;
     } catch (error) {
         dispatch(jobsActions.createJobFailed(error));
diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts
index 6ab08543caf4..7dab31145895 100644
--- a/cvat-ui/src/actions/projects-actions.ts
+++ b/cvat-ui/src/actions/projects-actions.ts
@@ -33,7 +33,7 @@ export enum ProjectsActionTypes {
 }
 
 const projectActions = {
-    getProjects: () => createAction(ProjectsActionTypes.GET_PROJECTS),
+    getProjects: (fetchingTimestamp: number) => createAction(ProjectsActionTypes.GET_PROJECTS, { fetchingTimestamp }),
     getProjectsSuccess: (array: any[], count: number) => (
         createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, count })
     ),
@@ -86,8 +86,13 @@ export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): Thun
 export function getProjectsAsync(
     query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {},
 ): ThunkAction {
-    return async (dispatch: ThunkDispatch): Promise<void> => {
-        dispatch(projectActions.getProjects());
+    return async (dispatch: ThunkDispatch, getState): Promise<void> => {
+        const requestedOn = Date.now();
+        const isRequestRelevant = (): boolean => (
+            getState().projects.fetchingTimestamp === requestedOn
+        );
+
+        dispatch(projectActions.getProjects(requestedOn));
         dispatch(projectActions.updateProjectsGettingQuery(query, tasksQuery));
 
         // Clear query object from null fields
@@ -100,20 +105,22 @@ export function getProjectsAsync(
         try {
             result = await cvat.projects.get(filteredQuery);
         } catch (error) {
-            dispatch(projectActions.getProjectsFailed(error));
+            if (isRequestRelevant()) {
+                dispatch(projectActions.getProjectsFailed(error));
+            }
             return;
         }
 
-        const array = Array.from(result);
-
-        dispatch(projectActions.getProjectsSuccess(array, result.count));
-
-        // Appropriate tasks fetching process needs with retrieving only a single project
-        if (Object.keys(filteredQuery).includes('id') && typeof filteredQuery.id === 'number') {
-            dispatch(getProjectTasksAsync({
-                ...tasksQuery,
-                projectId: filteredQuery.id,
-            }));
+        if (isRequestRelevant()) {
+            const array = Array.from(result);
+            dispatch(projectActions.getProjectsSuccess(array, result.count));
+            // Appropriate tasks fetching process needs with retrieving only a single project
+            if (Object.keys(filteredQuery).includes('id') && typeof filteredQuery.id === 'number') {
+                dispatch(getProjectTasksAsync({
+                    ...tasksQuery,
+                    projectId: filteredQuery.id,
+                }));
+            }
         }
     };
 }
diff --git a/cvat-ui/src/actions/requests-actions.ts b/cvat-ui/src/actions/requests-actions.ts
index 1f3972746e7e..f0e2d2f6adc1 100644
--- a/cvat-ui/src/actions/requests-actions.ts
+++ b/cvat-ui/src/actions/requests-actions.ts
@@ -3,10 +3,8 @@
 // SPDX-License-Identifier: MIT
 
 import { ActionUnion, createAction } from 'utils/redux';
-import { CombinedState, RequestsQuery, RequestsState } from 'reducers';
-import {
-    Request, ProjectOrTaskOrJob, getCore, RQStatus,
-} from 'cvat-core-wrapper';
+import { CombinedState, RequestsQuery } from 'reducers';
+import { Request, ProjectOrTaskOrJob, getCore } from 'cvat-core-wrapper';
 import { Store } from 'redux';
 import { getCVATStore } from 'cvat-store';
 
@@ -88,23 +86,15 @@ export function updateRequestProgress(request: Request, dispatch: (action: Reque
     );
 }
 
-export function shouldListenForProgress(rqID: string | void, state: RequestsState): boolean {
-    return (
-        typeof rqID === 'string' &&
-        (!state.requests[rqID] || [RQStatus.FINISHED, RQStatus.FAILED].includes(state.requests[rqID]?.status))
-    );
-}
-
 export function listen(
     requestID: string,
     dispatch: (action: RequestsActions) => void,
 ) : Promise<Request> {
     const { requests } = getStore().getState().requests;
-    return core.requests
-        .listen(requestID, {
-            callback: (updatedRequest) => {
-                updateRequestProgress(updatedRequest, dispatch);
-            },
-            initialRequest: requests[requestID],
-        });
+    return core.requests.listen(requestID, {
+        callback: (updatedRequest) => {
+            updateRequestProgress(updatedRequest, dispatch);
+        },
+        initialRequest: requests[requestID],
+    });
 }
diff --git a/cvat-ui/src/actions/requests-async-actions.ts b/cvat-ui/src/actions/requests-async-actions.ts
index 06a137eafd28..86151cbd076e 100644
--- a/cvat-ui/src/actions/requests-async-actions.ts
+++ b/cvat-ui/src/actions/requests-async-actions.ts
@@ -37,12 +37,14 @@ export function getRequestsAsync(query: RequestsQuery): ThunkAction {
                 .forEach((request: Request): void => {
                     const {
                         id: rqID,
+                        status,
                         operation: {
                             type, target, format, taskID, projectID, jobID,
                         },
                     } = request;
 
-                    if (state.requests.requests[rqID]) {
+                    const isRequestFinished = [RQStatus.FINISHED, RQStatus.FAILED].includes(status);
+                    if (state.requests.requests[rqID] || isRequestFinished) {
                         return;
                     }
 
diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts
index d15f033f1e6f..644f5aa7b021 100644
--- a/cvat-ui/src/actions/tasks-actions.ts
+++ b/cvat-ui/src/actions/tasks-actions.ts
@@ -32,10 +32,11 @@ export enum TasksActionTypes {
     UPDATE_TASK_IN_STATE = 'UPDATE_TASK_IN_STATE',
 }
 
-function getTasks(query: Partial<TasksQuery>, updateQuery: boolean): AnyAction {
+function getTasks(query: Partial<TasksQuery>, updateQuery: boolean, fetchingTimestamp: number): AnyAction {
     const action = {
         type: TasksActionTypes.GET_TASKS,
         payload: {
+            fetchingTimestamp,
             updateQuery,
             query,
         },
@@ -69,23 +70,30 @@ export function getTasksAsync(
     query: Partial<TasksQuery>,
     updateQuery = true,
 ): ThunkAction {
-    return async (dispatch: ThunkDispatch): Promise<void> => {
-        dispatch(getTasks(query, updateQuery));
+    return async (dispatch: ThunkDispatch, getState): Promise<void> => {
+        const requestedOn = Date.now();
+        const isRequestRelevant = (): boolean => (
+            getState().tasks.fetchingTimestamp === requestedOn
+        );
 
+        dispatch(getTasks(query, updateQuery, requestedOn));
         const filteredQuery = filterNull(query);
 
         let result = null;
         try {
             result = await cvat.tasks.get(filteredQuery);
         } catch (error) {
-            dispatch(getTasksFailed(error));
+            if (isRequestRelevant()) {
+                dispatch(getTasksFailed(error));
+            }
             return;
         }
 
-        const array = Array.from(result);
-
-        dispatch(getInferenceStatusAsync());
-        dispatch(getTasksSuccess(array, result.count));
+        if (isRequestRelevant()) {
+            const array = Array.from(result);
+            dispatch(getInferenceStatusAsync());
+            dispatch(getTasksSuccess(array, result.count));
+        }
     };
 }
 
diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx
index 37ba42116711..a04734b9c371 100644
--- a/cvat-ui/src/components/annotation-page/annotation-page.tsx
+++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx
@@ -5,7 +5,6 @@
 
 import React, { useEffect } from 'react';
 import Layout from 'antd/lib/layout';
-import Result from 'antd/lib/result';
 import Spin from 'antd/lib/spin';
 import notification from 'antd/lib/notification';
 import Button from 'antd/lib/button';
@@ -19,6 +18,7 @@ import StandardWorkspaceComponent from 'components/annotation-page/standard-work
 import StandardWorkspace3DComponent from 'components/annotation-page/standard3D-workspace/standard3D-workspace';
 import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace';
 import FiltersModalComponent from 'components/annotation-page/top-bar/filters-modal';
+import { JobNotFoundComponent } from 'components/common/not-found';
 import StatisticsModalComponent from 'components/annotation-page/top-bar/statistics-modal';
 import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
 import { Workspace } from 'reducers';
@@ -139,14 +139,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
     }
 
     if (typeof job === 'undefined') {
-        return (
-            <Result
-                className='cvat-not-found'
-                status='404'
-                title='Sorry, but this job was not found'
-                subTitle='Please, be sure information you tried to get exist and you have access'
-            />
-        );
+        return <JobNotFoundComponent />;
     }
 
     return (
diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx
index 6c140438c20e..b6a43ce20cf6 100644
--- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx
+++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx
@@ -6,9 +6,9 @@ import './brush-toolbox-styles.scss';
 
 import React, { useCallback, useEffect, useState } from 'react';
 import ReactDOM from 'react-dom';
-import { useDispatch, useSelector } from 'react-redux';
+import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 import Button from 'antd/lib/button';
-import Icon, { VerticalAlignBottomOutlined } from '@ant-design/icons';
+import Icon, { EyeInvisibleFilled, EyeOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
 import InputNumber from 'antd/lib/input-number';
 import Select from 'antd/lib/select';
 import notification from 'antd/lib/notification';
@@ -23,7 +23,7 @@ import {
 import CVATTooltip from 'components/common/cvat-tooltip';
 import { CombinedState, ObjectType, ShapeType } from 'reducers';
 import LabelSelector from 'components/label-selector/label-selector';
-import { rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions';
+import { changeHideActiveObjectAsync, rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions';
 import { ShortcutScope } from 'utils/enums';
 import GlobalHotKeys from 'utils/mousetrap-react';
 import { subKeyMap } from 'utils/component-subkeymap';
@@ -71,12 +71,17 @@ registerComponentShortcuts(componentShortcuts);
 const MIN_BRUSH_SIZE = 1;
 function BrushTools(): React.ReactPortal | null {
     const dispatch = useDispatch();
-    const defaultLabelID = useSelector((state: CombinedState) => state.annotation.drawing.activeLabelID);
-    const config = useSelector((state: CombinedState) => state.annotation.canvas.brushTools);
-    const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
-    const labels = useSelector((state: CombinedState) => state.annotation.job.labels);
-    const { keyMap, normalizedKeyMap } = useSelector((state: CombinedState) => state.shortcuts);
-    const { visible } = config;
+    const {
+        defaultLabelID, visible, canvasInstance, labels, activeObjectHidden, keyMap, normalizedKeyMap,
+    } = useSelector((state: CombinedState) => ({
+        defaultLabelID: state.annotation.drawing.activeLabelID,
+        visible: state.annotation.canvas.brushTools.visible,
+        canvasInstance: state.annotation.canvas.instance,
+        labels: state.annotation.job.labels,
+        activeObjectHidden: state.annotation.canvas.activeObjectHidden,
+        keyMap: state.shortcuts.keyMap,
+        normalizedKeyMap: state.shortcuts.normalizedKeyMap,
+    }), shallowEqual);
 
     const [editableState, setEditableState] = useState<any | null>(null);
     const [currentTool, setCurrentTool] = useState<'brush' | 'eraser' | 'polygon-plus' | 'polygon-minus'>('brush');
@@ -103,6 +108,10 @@ function BrushTools(): React.ReactPortal | null {
         }
     }, [setCurrentTool, blockedTools['polygon-minus']]);
 
+    const hideMask = useCallback((hide: boolean) => {
+        dispatch(changeHideActiveObjectAsync(hide));
+    }, []);
+
     const handlers: Record<keyof typeof componentShortcuts, (event?: KeyboardEvent) => void> = {
         ACTIVATE_BRUSH_TOOL_STANDARD_CONTROLS: setBrushTool,
         ACTIVATE_ERASER_TOOL_STANDARD_CONTROLS: setEraserTool,
@@ -365,6 +374,14 @@ function BrushTools(): React.ReactPortal | null {
                 icon={<VerticalAlignBottomOutlined />}
                 onClick={() => setRemoveUnderlyingPixels(!removeUnderlyingPixels)}
             />
+            <CVATTooltip title={`Hide mask ${normalizedKeyMap.SWITCH_HIDDEN}`}>
+                <Button
+                    type='text'
+                    className={['cvat-brush-tools-hide', ...(activeObjectHidden ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
+                    icon={activeObjectHidden ? <EyeInvisibleFilled /> : <EyeOutlined />}
+                    onClick={() => hideMask(!activeObjectHidden)}
+                />
+            </CVATTooltip>
             { !editableState && !!applicableLabels.length && (
                 <LabelSelector
                     labels={applicableLabels}
diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx
index e37f9e9b238c..df98f6b5c4cc 100644
--- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx
+++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx
@@ -45,6 +45,7 @@ import {
     fetchAnnotationsAsync,
     getDataFailed,
     canvasErrorOccurred,
+    updateEditedStateAsync,
 } from 'actions/annotation-actions';
 import {
     switchGrid,
@@ -119,6 +120,7 @@ interface StateToProps {
     highlightedConflict: QualityConflict | null;
     imageFilters: ImageFilter[];
     activeControl: ActiveControl;
+    activeObjectHidden: boolean;
 }
 
 interface DispatchToProps {
@@ -146,12 +148,15 @@ interface DispatchToProps {
     onGetDataFailed(error: Error): void;
     onCanvasErrorOccurred(error: Error): void;
     onStartIssue(position: number[]): void;
+    onUpdateEditedObject(editedState: ObjectState | null): void;
 }
 
 function mapStateToProps(state: CombinedState): StateToProps {
     const {
         annotation: {
-            canvas: { activeControl, instance: canvasInstance, ready: canvasIsReady },
+            canvas: {
+                activeControl, instance: canvasInstance, ready: canvasIsReady, activeObjectHidden,
+            },
             drawing: { activeLabelID, activeObjectType },
             job: { instance: jobInstance },
             player: {
@@ -255,6 +260,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
         showGroundTruth,
         highlightedConflict,
         imageFilters,
+        activeObjectHidden,
     };
 }
 
@@ -347,6 +353,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
         onStartIssue(position: number[]): void {
             dispatch(reviewActions.startIssue(position));
         },
+        onUpdateEditedObject(editedState: ObjectState | null): void {
+            dispatch(updateEditedStateAsync(editedState));
+        },
     };
 }
 
@@ -634,6 +643,7 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
     private onCanvasShapeDrawn = (event: any): void => {
         const {
             jobInstance, activeLabelID, activeObjectType, frame, updateActiveControl, onCreateAnnotations,
+            onUpdateEditedObject, activeObjectHidden, workspace,
         } = this.props;
 
         if (!event.detail.continue) {
@@ -649,6 +659,7 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
         state.rotation = state.rotation || 0;
         state.occluded = state.occluded || false;
         state.outside = state.outside || false;
+        state.hidden = state.hidden || (activeObjectHidden && workspace !== Workspace.SINGLE_SHAPE);
         if (state.shapeType === ShapeType.SKELETON && Array.isArray(state.elements)) {
             state.elements.forEach((element: Record<string, any>) => {
                 element.objectType = state.objectType;
@@ -669,6 +680,7 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
 
         const objectState = new cvat.classes.ObjectState(state);
         onCreateAnnotations([objectState]);
+        onUpdateEditedObject(null);
     };
 
     private onCanvasObjectsMerged = (event: any): void => {
@@ -829,13 +841,16 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
         }
     };
 
-    private onCanvasEditStart = (): void => {
-        const { updateActiveControl } = this.props;
+    private onCanvasEditStart = (event: any): void => {
+        const { updateActiveControl, onUpdateEditedObject } = this.props;
         updateActiveControl(ActiveControl.EDIT);
+        onUpdateEditedObject(event.detail.state);
     };
 
     private onCanvasEditDone = (event: any): void => {
-        const { activeControl, onUpdateAnnotations, updateActiveControl } = this.props;
+        const {
+            activeControl, onUpdateAnnotations, updateActiveControl, onUpdateEditedObject,
+        } = this.props;
         const { state, points, rotation } = event.detail;
         if (state.rotation !== rotation) {
             state.rotation = rotation;
@@ -848,6 +863,7 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
             updateActiveControl(ActiveControl.CURSOR);
         }
         onUpdateAnnotations([state]);
+        onUpdateEditedObject(null);
     };
 
     private onCanvasSliceDone = (event: any): void => {
@@ -887,8 +903,9 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
     };
 
     private onCanvasCancel = (): void => {
-        const { onResetCanvas } = this.props;
+        const { onResetCanvas, onUpdateEditedObject } = this.props;
         onResetCanvas();
+        onUpdateEditedObject(null);
     };
 
     private onCanvasFindObject = async (e: any): Promise<void> => {
diff --git a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx
index 0a587c812b82..b7f7d60097af 100644
--- a/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx
+++ b/cvat-ui/src/components/annotation-page/single-shape-workspace/single-shape-sidebar/single-shape-sidebar.tsx
@@ -18,13 +18,18 @@ import Alert from 'antd/lib/alert';
 import Button from 'antd/lib/button';
 import message from 'antd/lib/message';
 
-import { CombinedState, NavigationType, ObjectType } from 'reducers';
+import {
+    ActiveControl, CombinedState, NavigationType, ObjectType,
+} from 'reducers';
 import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
-import { Job, Label, LabelType } from 'cvat-core-wrapper';
+import {
+    Job, Label, LabelType, ShapeType,
+} from 'cvat-core-wrapper';
 import { ActionUnion, createAction } from 'utils/redux';
 import {
     rememberObject, changeFrameAsync, setNavigationType,
     removeObjectAsync, finishCurrentJobAsync,
+    changeHideActiveObjectAsync, updateActiveControl, ShapeTypeToControl,
 } from 'actions/annotation-actions';
 import LabelSelector from 'components/label-selector/label-selector';
 import GlobalHotKeys from 'utils/mousetrap-react';
@@ -185,6 +190,12 @@ const componentShortcuts = {
         sequences: ['del', 'shift+del'],
         scope: ShortcutScope.SINGLE_SHAPE_ANNOTATION_WORKSPACE,
     },
+    HIDE_MASK_SINGLE_SHAPE: {
+        name: 'Hide mask',
+        description: 'Hide currently edited mask',
+        sequences: ['h'],
+        scope: ShortcutScope.SINGLE_SHAPE_ANNOTATION_WORKSPACE,
+    },
 };
 
 registerComponentShortcuts(componentShortcuts);
@@ -203,6 +214,9 @@ function SingleShapeSidebar(): JSX.Element {
         navigationType,
         annotations,
         activatedStateID,
+        editedState,
+        activeControl,
+        activeObjectHidden,
     } = useSelector((_state: CombinedState) => ({
         isCanvasReady: _state.annotation.canvas.ready,
         jobInstance: _state.annotation.job.instance as Job,
@@ -214,14 +228,17 @@ function SingleShapeSidebar(): JSX.Element {
         navigationType: _state.annotation.player.navigationType,
         annotations: _state.annotation.annotations.states,
         activatedStateID: _state.annotation.annotations.activatedStateID,
+        editedState: _state.annotation.editing.objectState,
+        activeControl: _state.annotation.canvas.activeControl,
+        activeObjectHidden: _state.annotation.canvas.activeObjectHidden,
     }), shallowEqual);
 
     const [state, dispatch] = useReducer(reducer, {
         autoNextFrame: true,
         nextFrame: null,
         saveOnFinish: true,
-        pointsCountIsPredefined: true,
-        pointsCount: defaultPointsCount || 1,
+        pointsCountIsPredefined: defaultPointsCount !== null,
+        pointsCount: defaultPointsCount ?? 1,
         labels: jobInstance.labels.filter((label) => label.type !== LabelType.TAG && label.type !== LabelType.SKELETON),
         label: null,
         labelType: LabelType.ANY,
@@ -234,6 +251,9 @@ function SingleShapeSidebar(): JSX.Element {
     canvasInitializerRef.current = (): void => {
         const canvas = store.getState().annotation.canvas.instance as Canvas;
         if (isCanvasReady && canvas.mode() !== CanvasMode.DRAW && state.label && state.labelType !== LabelType.ANY) {
+            appDispatch(updateActiveControl(
+                ShapeTypeToControl[state.labelType],
+            ));
             // we remember active object type and active label
             // to assign these values in default drawdone event listener
             appDispatch(rememberObject({
@@ -388,6 +408,13 @@ function SingleShapeSidebar(): JSX.Element {
                 appDispatch(removeObjectAsync(objectStateToRemove, event?.shiftKey || false));
             }
         },
+        HIDE_MASK_SINGLE_SHAPE: (event: KeyboardEvent | undefined) => {
+            event?.preventDefault();
+            if (editedState?.shapeType === ShapeType.MASK || activeControl === ActiveControl.DRAW_MASK) {
+                const hide = editedState ? !editedState.hidden : !activeObjectHidden;
+                appDispatch(changeHideActiveObjectAsync(hide));
+            }
+        },
     };
 
     if (!state.labels.length) {
@@ -484,7 +511,7 @@ function SingleShapeSidebar(): JSX.Element {
                                                 Press
                                                 <Text strong>
                                                     {` ${
-                                                        normalizedKeyMap.SWITCH_DRAW_MODE_SINGLE_SHAPE_CONTROLS
+                                                        normalizedKeyMap.SWITCH_DRAW_MODE_SINGLE_SHAPE
                                                     } `}
                                                 </Text>
                                                 to finish drawing process
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy.tsx
index 0db8796a1f4a..6a981793515f 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy.tsx
@@ -35,7 +35,7 @@ export function thresholdFromAccuracy(approxPolyAccuracy: number): number {
     let threshold = 0;
     if (approxPolyMaxDistance > 0) {
         if (approxPolyMaxDistance <= 8) {
-            // −2.75x+7y+1=0 linear made from two points (1; 0.25) and (8; 3)
+            // -2.75x+7y+1=0 linear made from two points (1; 0.25) and (8; 3)
             threshold = (2.75 * approxPolyMaxDistance - 1) / 7;
         } else {
             // 4 for 9, 8 for 10, 16 for 11, 32 for 12, 64 for 13
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/propagate-confirm.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/propagate-confirm.tsx
index 511d4454bce2..4d57ed9faf05 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/propagate-confirm.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/propagate-confirm.tsx
@@ -4,7 +4,7 @@
 // SPDX-License-Identifier: MIT
 
 import React, { useEffect, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 import Modal from 'antd/lib/modal';
 import InputNumber from 'antd/lib/input-number';
 import Text from 'antd/lib/typography/Text';
@@ -23,12 +23,19 @@ export enum PropagateDirection {
 
 function PropagateConfirmComponent(): JSX.Element {
     const dispatch = useDispatch();
-    const visible = useSelector((state: CombinedState) => state.annotation.propagate.visible);
-    const frameNumber = useSelector((state: CombinedState) => state.annotation.player.frame.number);
-    const startFrame = useSelector((state: CombinedState) => state.annotation.job.instance.startFrame);
-    const stopFrame = useSelector((state: CombinedState) => state.annotation.job.instance.stopFrame);
-    const [targetFrame, setTargetFrame] = useState<number>(frameNumber);
+    const {
+        visible,
+        frameNumber,
+        frameNumbers,
+    } = useSelector((state: CombinedState) => ({
+        visible: state.annotation.propagate.visible,
+        frameNumber: state.annotation.player.frame.number,
+        frameNumbers: state.annotation.job.frameNumbers,
+    }), shallowEqual);
 
+    const [targetFrame, setTargetFrame] = useState<number>(frameNumber);
+    const startFrame = frameNumbers[0];
+    const stopFrame = frameNumbers[frameNumbers.length - 1];
     const propagateFrames = Math.abs(targetFrame - frameNumber);
     const propagateDirection = targetFrame >= frameNumber ? PropagateDirection.FORWARD : PropagateDirection.BACKWARD;
 
@@ -93,9 +100,9 @@ function PropagateConfirmComponent(): JSX.Element {
                             size='small'
                             min={0}
                             value={propagateFrames}
-                            onChange={(value: number) => {
-                                if (typeof value !== 'undefined') {
-                                    updateTargetFrame(propagateDirection, +value);
+                            onChange={(value: number | null) => {
+                                if (typeof value === 'number') {
+                                    updateTargetFrame(propagateDirection, value);
                                 }
                             }}
                         />
@@ -115,7 +122,7 @@ function PropagateConfirmComponent(): JSX.Element {
                                 [frameNumber]: 'FROM',
                                 [targetFrame]: 'TO',
                             } : undefined}
-                            onChange={([value1, value2]: [number, number]) => {
+                            onChange={([value1, value2]: number[]) => {
                                 const value = value1 === frameNumber || value1 === targetFrame ? value2 : value1;
                                 if (value < frameNumber) {
                                     setTargetFrame(clamp(value, startFrame, frameNumber));
@@ -133,8 +140,8 @@ function PropagateConfirmComponent(): JSX.Element {
                             min={startFrame}
                             max={stopFrame}
                             value={targetFrame}
-                            onChange={(value: number) => {
-                                if (typeof value !== 'undefined') {
+                            onChange={(value: number | null) => {
+                                if (typeof value === 'number') {
                                     if (value > frameNumber) {
                                         setTargetFrame(clamp(+value, frameNumber, stopFrame));
                                     } else if (value < frameNumber) {
diff --git a/cvat-ui/src/components/common/not-found.tsx b/cvat-ui/src/components/common/not-found.tsx
new file mode 100644
index 000000000000..a9b9a15787f7
--- /dev/null
+++ b/cvat-ui/src/components/common/not-found.tsx
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 CVAT.ai Corporation
+//
+// SPDX-License-Identifier: MIT
+
+import React from 'react';
+import Result from 'antd/lib/result';
+
+export const JobNotFoundComponent = React.memo((): JSX.Element => (
+    <Result
+        className='cvat-not-found'
+        status='404'
+        title='Sorry, but this job was not found'
+        subTitle='Please, be sure information you tried to get exist and you have access'
+    />
+));
+
+export const TaskNotFoundComponent = React.memo((): JSX.Element => (
+    <Result
+        className='cvat-not-found'
+        status='404'
+        title='There was something wrong during getting the task'
+        subTitle='Please, be sure, that information you tried to get exist and you are eligible to access it'
+    />
+));
+
+export const ProjectNotFoundComponent = React.memo((): JSX.Element => (
+    <Result
+        className='cvat-not-found'
+        status='404'
+        title='There was something wrong during getting the project'
+        subTitle='Please, be sure, that information you tried to get exist and you are eligible to access it'
+    />
+));
+
+export const CloudStorageNotFoundComponent = React.memo((): JSX.Element => (
+    <Result
+        className='cvat-not-found'
+        status='404'
+        title='Sorry, but the requested cloud storage was not found'
+        subTitle='Please, be sure id you requested exists and you have appropriate permissions'
+    />
+));
diff --git a/cvat-ui/src/components/common/preview.tsx b/cvat-ui/src/components/common/preview.tsx
index b96066b7fa40..ce0a7bbea979 100644
--- a/cvat-ui/src/components/common/preview.tsx
+++ b/cvat-ui/src/components/common/preview.tsx
@@ -11,9 +11,8 @@ import { getJobPreviewAsync } from 'actions/jobs-actions';
 import { getTaskPreviewAsync } from 'actions/tasks-actions';
 import { getProjectsPreviewAsync } from 'actions/projects-actions';
 import { getCloudStoragePreviewAsync } from 'actions/cloud-storage-actions';
-import {
-    CombinedState, Job, Task, Project, CloudStorage,
-} from 'reducers';
+import { CombinedState, CloudStorage } from 'reducers';
+import { Job, Task, Project } from 'cvat-core-wrapper';
 import MLModel from 'cvat-core/src/ml-model';
 import { getModelPreviewAsync } from 'actions/models-actions';
 
diff --git a/cvat-ui/src/components/create-job-page/create-job-page.tsx b/cvat-ui/src/components/create-job-page/create-job-page.tsx
index 4c46e1b99fb6..7555d0fade3d 100644
--- a/cvat-ui/src/components/create-job-page/create-job-page.tsx
+++ b/cvat-ui/src/components/create-job-page/create-job-page.tsx
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 CVAT.ai Corporation
+// Copyright (C) 2023-2024 CVAT.ai Corporation
 //
 // SPDX-License-Identifier: MIT
 
@@ -10,9 +10,9 @@ import { Row, Col } from 'antd/lib/grid';
 import Text from 'antd/lib/typography/Text';
 import Spin from 'antd/lib/spin';
 import notification from 'antd/lib/notification';
-import { Task } from 'reducers';
+import { TaskNotFoundComponent } from 'components/common/not-found';
 import { useIsMounted } from 'utils/hooks';
-import { getCore } from 'cvat-core-wrapper';
+import { getCore, Task } from 'cvat-core-wrapper';
 import JobForm from './job-form';
 
 const core = getCore();
@@ -50,6 +50,15 @@ function CreateJobPage(): JSX.Element {
             setFetchingTask(false);
         }
     }, []);
+
+    if (fetchingTask) {
+        return <Spin size='large' className='cvat-spinner' />;
+    }
+
+    if (!taskInstance) {
+        return <TaskNotFoundComponent />;
+    }
+
     return (
         <div className='cvat-create-job-page'>
             <Row justify='center' align='middle'>
@@ -57,19 +66,11 @@ function CreateJobPage(): JSX.Element {
                     <Text className='cvat-title'>Add a new job</Text>
                 </Col>
             </Row>
-            {
-                fetchingTask ? (
-                    <div className='cvat-create-job-loding'>
-                        <Spin size='large' className='cvat-spinner' />
-                    </div>
-                ) : (
-                    <Row justify='center' align='top'>
-                        <Col md={20} lg={16} xl={14} xxl={9}>
-                            <JobForm task={taskInstance} />
-                        </Col>
-                    </Row>
-                )
-            }
+            <Row justify='center' align='top'>
+                <Col md={20} lg={16} xl={14} xxl={9}>
+                    <JobForm task={taskInstance} />
+                </Col>
+            </Row>
         </div>
     );
 }
diff --git a/cvat-ui/src/components/create-job-page/job-form.tsx b/cvat-ui/src/components/create-job-page/job-form.tsx
index b94d8a5f3fac..a6d4a63c7103 100644
--- a/cvat-ui/src/components/create-job-page/job-form.tsx
+++ b/cvat-ui/src/components/create-job-page/job-form.tsx
@@ -4,7 +4,7 @@
 
 import './styles.scss';
 
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 import { useHistory } from 'react-router';
 import { useDispatch } from 'react-redux';
 import { Row, Col } from 'antd/lib/grid';
@@ -26,19 +26,20 @@ export enum FrameSelectionMethod {
 }
 
 interface JobDataMutual {
-    task_id: number;
-    frame_selection_method: FrameSelectionMethod;
+    taskID: number;
+    frameSelectionMethod: FrameSelectionMethod;
     type: JobType;
     seed?: number;
 }
 
 export interface JobData extends JobDataMutual {
-    frame_count: number;
+    frameCount?: number;
+    framesPerJobCount?: number;
 }
 
 export interface JobFormData extends JobDataMutual {
     quantity: number;
-    frame_count: number;
+    frameCount: number;
 }
 
 interface Props {
@@ -49,21 +50,24 @@ const defaultQuantity = 5;
 
 function JobForm(props: Props): JSX.Element {
     const { task } = props;
-    const { size: taskSize } = task;
+    const { size: taskSize, segmentSize } = task;
     const [form] = Form.useForm();
     const dispatch = useDispatch();
     const history = useHistory();
     const [fetching, setFetching] = useState(false);
+    const [frameSelectionMethod, setFrameSelectionMethod] = useState(FrameSelectionMethod.RANDOM);
 
     const submit = useCallback(async (): Promise<any> => {
         try {
             const values: JobFormData = await form.validateFields();
             const data: JobData = {
-                frame_selection_method: values.frame_selection_method,
+                taskID: task.id,
+                frameSelectionMethod: values.frameSelectionMethod,
                 type: values.type,
                 seed: values.seed,
-                frame_count: values.frame_count,
-                task_id: task.id,
+                ...(values.frameSelectionMethod === FrameSelectionMethod.RANDOM ?
+                    { frameCount: values.frameCount } : { framesPerJobCount: values.frameCount }
+                ),
             };
 
             const createdJob = await dispatch(createJobAsync(data));
@@ -86,24 +90,40 @@ function JobForm(props: Props): JSX.Element {
         }
     };
 
+    const sizeBase = (): number => {
+        if (frameSelectionMethod === FrameSelectionMethod.RANDOM) {
+            return taskSize;
+        }
+        return segmentSize;
+    };
+
+    const quantityFromFrameCount = (value: number): number => Math.floor((value / sizeBase()) * 100);
+    const frameCountFromQuantity = (value: number): number => Math.round((value * sizeBase()) / 100);
+
     const onQuantityChange = useCallback((value: number | null) => {
         if (value) {
-            const newFrameCount = Math.round((value * taskSize) / 100);
+            const newFrameCount = frameCountFromQuantity(value);
             form.setFieldsValue({
-                frame_count: newFrameCount,
+                frameCount: newFrameCount,
             });
         }
-    }, [taskSize]);
+    }, [taskSize, frameSelectionMethod, segmentSize]);
 
     const onFrameCountChange = useCallback((value: number | null) => {
         if (value) {
-            const newQuantity = Math.floor((value / taskSize) * 100);
+            const newQuantity = quantityFromFrameCount(value);
             form.setFieldsValue({
                 quantity: newQuantity,
             });
         }
-    }, [taskSize]);
-    const frameCountDescription = 'A representative set, 5-15% of randomly chosen frames is recommended';
+    }, [taskSize, frameSelectionMethod, segmentSize]);
+
+    useEffect(() => {
+        const currentQuantity = form.getFieldValue('quantity');
+        onQuantityChange(currentQuantity);
+    }, [form, frameSelectionMethod]);
+
+    const description = 'A representative set, 5-15% of randomly chosen frames is recommended';
 
     return (
         <Row className='cvat-create-job-form-wrapper'>
@@ -113,9 +133,9 @@ function JobForm(props: Props): JSX.Element {
                     layout='vertical'
                     initialValues={{
                         type: JobType.GROUND_TRUTH,
-                        frame_selection_method: FrameSelectionMethod.RANDOM,
+                        frameSelectionMethod: FrameSelectionMethod.RANDOM,
                         quantity: defaultQuantity,
-                        frame_count: Math.floor((defaultQuantity * taskSize) / 100),
+                        frameCount: frameCountFromQuantity(defaultQuantity),
                     }}
                 >
                     <Col>
@@ -134,13 +154,14 @@ function JobForm(props: Props): JSX.Element {
                             </Select>
                         </Form.Item>
                         <Form.Item
-                            name='frame_selection_method'
+                            name='frameSelectionMethod'
                             label='Frame selection method'
                             rules={[{ required: true, message: 'Please, specify frame selection method' }]}
                         >
                             <Select
                                 virtual={false}
                                 className='cvat-select-frame-selection-method'
+                                onChange={setFrameSelectionMethod}
                             >
                                 <Select.Option value={FrameSelectionMethod.RANDOM}>
                                     Random
@@ -158,8 +179,9 @@ function JobForm(props: Props): JSX.Element {
                                     name='quantity'
                                     label={(
                                         <Space>
-                                            Quantity %
-                                            <CVATTooltip title={frameCountDescription}>
+                                            {frameSelectionMethod === FrameSelectionMethod.RANDOM ?
+                                                'Quantity' : 'Quantity per job'}
+                                            <CVATTooltip title={description}>
                                                 <QuestionCircleOutlined
                                                     style={{ opacity: 0.5 }}
                                                 />
@@ -182,11 +204,11 @@ function JobForm(props: Props): JSX.Element {
                                 <Row>
                                     <Col>
                                         <Form.Item
-                                            name='frame_count'
+                                            name='frameCount'
                                             label={(
                                                 <Space>
                                                     Frame count
-                                                    <CVATTooltip title={frameCountDescription}>
+                                                    <CVATTooltip title={description}>
                                                         <QuestionCircleOutlined
                                                             style={{ opacity: 0.5 }}
                                                         />
diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx
index a9be65a055d1..ff9d2e1637b3 100644
--- a/cvat-ui/src/components/cvat-app.tsx
+++ b/cvat-ui/src/components/cvat-app.tsx
@@ -398,18 +398,21 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
         function showError(title: string, _error: Error, shouldLog?: boolean, className?: string): void {
             const error = _error?.message || _error.toString();
             const dynamicProps = typeof className === 'undefined' ? {} : { className };
+
             let errorLength = error.length;
             // Do not count the length of the link in the Markdown error message
             if (/]\(.+\)/.test(error)) {
                 errorLength = error.replace(/]\(.+\)/, ']').length;
             }
+
             notification.error({
                 ...dynamicProps,
                 message: (
                     <CVATMarkdown history={history}>{title}</CVATMarkdown>
                 ),
                 duration: null,
-                description: errorLength > 300 ? 'Open the Browser Console to get details' : <CVATMarkdown history={history}>{error}</CVATMarkdown>,
+                description: errorLength > appConfig.MAXIMUM_NOTIFICATION_MESSAGE_LENGTH ?
+                    'Open the Browser Console to get details' : <CVATMarkdown history={history}>{error}</CVATMarkdown>,
             });
 
             if (shouldLog) {
diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx
index 010f39d2862d..73031c7e0307 100644
--- a/cvat-ui/src/components/project-page/project-page.tsx
+++ b/cvat-ui/src/components/project-page/project-page.tsx
@@ -9,7 +9,6 @@ import { useSelector, useDispatch } from 'react-redux';
 import { useHistory, useParams } from 'react-router';
 import Spin from 'antd/lib/spin';
 import { Row, Col } from 'antd/lib/grid';
-import Result from 'antd/lib/result';
 import Button from 'antd/lib/button';
 import Popover from 'antd/lib/popover';
 import Title from 'antd/lib/typography/Title';
@@ -31,6 +30,7 @@ import {
     SortingComponent, ResourceFilterHOC, defaultVisibility, updateHistoryFromQuery,
 } from 'components/resource-sorting-filtering';
 import CvatDropdownMenuPaper from 'components/common/cvat-dropdown-menu-paper';
+import { ProjectNotFoundComponent } from 'components/common/not-found';
 
 import DetailsComponent from './details';
 import ProjectTopBar from './top-bar';
@@ -126,14 +126,7 @@ export default function ProjectPageComponent(): JSX.Element {
     }
 
     if (!projectInstance) {
-        return (
-            <Result
-                className='cvat-not-found'
-                status='404'
-                title='There was something wrong during getting the project'
-                subTitle='Please, be sure, that information you tried to get exist and you are eligible to access it'
-            />
-        );
+        return <ProjectNotFoundComponent />;
     }
 
     const subsets = Array.from(
diff --git a/cvat-ui/src/components/project-page/top-bar.tsx b/cvat-ui/src/components/project-page/top-bar.tsx
index 2f4baff827ce..28e92a996d9e 100644
--- a/cvat-ui/src/components/project-page/top-bar.tsx
+++ b/cvat-ui/src/components/project-page/top-bar.tsx
@@ -11,7 +11,7 @@ import Button from 'antd/lib/button';
 import Dropdown from 'antd/lib/dropdown';
 import Text from 'antd/lib/typography/Text';
 
-import { Project } from 'reducers';
+import { Project } from 'cvat-core-wrapper';
 import ActionsMenu from 'components/projects-page/actions-menu';
 
 interface DetailsComponentProps {
diff --git a/cvat-ui/src/components/projects-page/project-list.tsx b/cvat-ui/src/components/projects-page/project-list.tsx
index 3f7d624ba0b4..a629ba8277cf 100644
--- a/cvat-ui/src/components/projects-page/project-list.tsx
+++ b/cvat-ui/src/components/projects-page/project-list.tsx
@@ -9,7 +9,8 @@ import { Row, Col } from 'antd/lib/grid';
 import Pagination from 'antd/lib/pagination';
 
 import { getProjectsAsync } from 'actions/projects-actions';
-import { CombinedState, Project } from 'reducers';
+import { CombinedState } from 'reducers';
+import { Project } from 'cvat-core-wrapper';
 import dimensions from 'utils/dimensions';
 import ProjectItem from './project-item';
 
diff --git a/cvat-ui/src/components/quality-control/quality-control-page.tsx b/cvat-ui/src/components/quality-control/quality-control-page.tsx
index 09f7cfe5e4d8..cbaa26a8dd09 100644
--- a/cvat-ui/src/components/quality-control/quality-control-page.tsx
+++ b/cvat-ui/src/components/quality-control/quality-control-page.tsx
@@ -223,8 +223,10 @@ function QualityControlPage(): JSX.Element {
                 settings.lowOverlapThreshold = values.lowOverlapThreshold / 100;
                 settings.iouThreshold = values.iouThreshold / 100;
                 settings.compareAttributes = values.compareAttributes;
+                settings.matchEmptyFrames = values.matchEmptyFrames;
 
                 settings.oksSigma = values.oksSigma / 100;
+                settings.pointSizeBase = values.pointSizeBase;
 
                 settings.lineThickness = values.lineThickness / 100;
                 settings.lineOrientationThreshold = values.lineOrientationThreshold / 100;
diff --git a/cvat-ui/src/components/quality-control/task-quality/quality-settings-form.tsx b/cvat-ui/src/components/quality-control/task-quality/quality-settings-form.tsx
index f1746fe675ac..87a727f9772b 100644
--- a/cvat-ui/src/components/quality-control/task-quality/quality-settings-form.tsx
+++ b/cvat-ui/src/components/quality-control/task-quality/quality-settings-form.tsx
@@ -14,6 +14,7 @@ import Button from 'antd/lib/button';
 import Select from 'antd/lib/select';
 import CVATTooltip from 'components/common/cvat-tooltip';
 import { QualitySettings, TargetMetric } from 'cvat-core-wrapper';
+import { PointSizeBase } from 'cvat-core/src/quality-settings';
 
 interface Props {
     form: FormInstance;
@@ -33,8 +34,10 @@ export default function QualitySettingsForm(props: Readonly<Props>): JSX.Element
         lowOverlapThreshold: settings.lowOverlapThreshold * 100,
         iouThreshold: settings.iouThreshold * 100,
         compareAttributes: settings.compareAttributes,
+        matchEmptyFrames: settings.matchEmptyFrames,
 
         oksSigma: settings.oksSigma * 100,
+        pointSizeBase: settings.pointSizeBase,
 
         lineThickness: settings.lineThickness * 100,
         lineOrientationThreshold: settings.lineOrientationThreshold * 100,
@@ -50,7 +53,13 @@ export default function QualitySettingsForm(props: Readonly<Props>): JSX.Element
 
     const targetMetricDescription = `${settings.descriptions.targetMetric
         .replaceAll(/\* [a-z` -]+[A-Z]+/g, '')
-        .replaceAll(/\n/g, '')}.`;
+        .replaceAll(/\n/g, '')
+    }`;
+
+    const pointSizeBaseDescription = `${settings.descriptions.pointSizeBase
+        .substring(0, settings.descriptions.pointSizeBase.indexOf('\n\n\n'))
+        .replaceAll(/\n/g, ' ')
+    }`;
 
     const makeTooltipFragment = (metric: string, description: string): JSX.Element => (
         <div>
@@ -71,6 +80,8 @@ export default function QualitySettingsForm(props: Readonly<Props>): JSX.Element
         <>
             {makeTooltipFragment('Target metric', targetMetricDescription)}
             {makeTooltipFragment('Target metric threshold', settings.descriptions.targetMetricThreshold)}
+            {makeTooltipFragment('Compare attributes', settings.descriptions.compareAttributes)}
+            {makeTooltipFragment('Match empty frames', settings.descriptions.matchEmptyFrames)}
         </>,
     );
 
@@ -89,6 +100,10 @@ export default function QualitySettingsForm(props: Readonly<Props>): JSX.Element
         makeTooltipFragment('Object Keypoint Similarity (OKS)', settings.descriptions.oksSigma),
     );
 
+    const pointTooltip = makeTooltip(
+        makeTooltipFragment('Point size base', pointSizeBaseDescription),
+    );
+
     const linesTooltip = makeTooltip(
         <>
             {makeTooltipFragment('Line thickness', settings.descriptions.lineThickness)}
@@ -169,6 +184,30 @@ export default function QualitySettingsForm(props: Readonly<Props>): JSX.Element
                     </Form.Item>
                 </Col>
             </Row>
+            <Row>
+                <Col span={12}>
+                    <Form.Item
+                        name='compareAttributes'
+                        valuePropName='checked'
+                        rules={[{ required: true }]}
+                    >
+                        <Checkbox>
+                            <Text className='cvat-text-color'>Compare attributes</Text>
+                        </Checkbox>
+                    </Form.Item>
+                </Col>
+                <Col span={12}>
+                    <Form.Item
+                        name='matchEmptyFrames'
+                        valuePropName='checked'
+                        rules={[{ required: true }]}
+                    >
+                        <Checkbox>
+                            <Text className='cvat-text-color'>Match empty frames</Text>
+                        </Checkbox>
+                    </Form.Item>
+                </Col>
+            </Row>
             <Divider />
             <Row className='cvat-quality-settings-title'>
                 <Text strong>
@@ -249,6 +288,38 @@ export default function QualitySettingsForm(props: Readonly<Props>): JSX.Element
                 </Col>
             </Row>
             <Divider />
+            <Row className='cvat-quality-settings-title'>
+                <Text strong>
+                    Point Comparison
+                </Text>
+                <CVATTooltip title={pointTooltip} className='cvat-analytics-tooltip' overlayStyle={{ maxWidth: '500px' }}>
+                    <QuestionCircleOutlined
+                        style={{ opacity: 0.5 }}
+                    />
+                </CVATTooltip>
+            </Row>
+            <Row>
+                <Col span={12}>
+                    <Form.Item
+                        name='pointSizeBase'
+                        label='Point size base'
+                        rules={[{ required: true }]}
+                    >
+                        <Select
+                            style={{ width: '70%' }}
+                            virtual={false}
+                        >
+                            <Select.Option value={PointSizeBase.IMAGE_SIZE}>
+                                Image size
+                            </Select.Option>
+                            <Select.Option value={PointSizeBase.GROUP_BBOX_SIZE}>
+                                Group bbox size
+                            </Select.Option>
+                        </Select>
+                    </Form.Item>
+                </Col>
+            </Row>
+            <Divider />
             <Row className='cvat-quality-settings-title'>
                 <Text strong>
                     Line Comparison
diff --git a/cvat-ui/src/components/requests-page/request-card.tsx b/cvat-ui/src/components/requests-page/request-card.tsx
index 52c109e3822c..dd2a8886941a 100644
--- a/cvat-ui/src/components/requests-page/request-card.tsx
+++ b/cvat-ui/src/components/requests-page/request-card.tsx
@@ -149,7 +149,7 @@ function RequestCard(props: Props): JSX.Element {
     const dispatch = useDispatch();
 
     const linkToEntity = constructLink(request);
-    const percent = request.status === RQStatus.FINISHED ? 100 : request.progress;
+    const percent = request.status === RQStatus.FINISHED ? 100 : (request.progress ?? 0) * 100;
     const timestamps = constructTimestamps(request);
 
     const name = constructName(operation);
diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx
index 1d92a5e4eb59..a371a113c3d6 100644
--- a/cvat-ui/src/components/task-page/task-page.tsx
+++ b/cvat-ui/src/components/task-page/task-page.tsx
@@ -9,12 +9,12 @@ import { useHistory, useParams } from 'react-router';
 import { useDispatch, useSelector } from 'react-redux';
 import { Row, Col } from 'antd/lib/grid';
 import Spin from 'antd/lib/spin';
-import Result from 'antd/lib/result';
 import notification from 'antd/lib/notification';
 
 import { getInferenceStatusAsync } from 'actions/models-actions';
 import { updateJobAsync } from 'actions/jobs-actions';
 import { getCore, Task, Job } from 'cvat-core-wrapper';
+import { TaskNotFoundComponent } from 'components/common/not-found';
 import JobListComponent from 'components/task-page/job-list';
 import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog';
 import CVATLoadingSpinner from 'components/common/loading-spinner';
@@ -78,14 +78,7 @@ function TaskPageComponent(): JSX.Element {
     }
 
     if (!taskInstance) {
-        return (
-            <Result
-                className='cvat-not-found'
-                status='404'
-                title='There was something wrong during getting the task'
-                subTitle='Please, be sure, that information you tried to get exist and you are eligible to access it'
-            />
-        );
+        return <TaskNotFoundComponent />;
     }
 
     const onUpdateTask = (task: Task): Promise<void> => (
diff --git a/cvat-ui/src/components/update-cloud-storage-page/update-cloud-storage-page.tsx b/cvat-ui/src/components/update-cloud-storage-page/update-cloud-storage-page.tsx
index 52afa4c97377..ef1631545b48 100644
--- a/cvat-ui/src/components/update-cloud-storage-page/update-cloud-storage-page.tsx
+++ b/cvat-ui/src/components/update-cloud-storage-page/update-cloud-storage-page.tsx
@@ -8,12 +8,12 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 import { useParams } from 'react-router-dom';
 import { Row, Col } from 'antd/lib/grid';
 import Spin from 'antd/lib/spin';
-import Result from 'antd/lib/result';
 import Text from 'antd/lib/typography/Text';
 
 import { CombinedState } from 'reducers';
 import { getCloudStoragesAsync } from 'actions/cloud-storage-actions';
 import CreateCloudStorageForm from 'components/create-cloud-storage-page/cloud-storage-form';
+import { CloudStorageNotFoundComponent } from 'components/common/not-found';
 
 interface ParamType {
     id: string;
@@ -45,14 +45,7 @@ export default function UpdateCloudStoragePageComponent(): JSX.Element {
     }
 
     if (!cloudStorage) {
-        return (
-            <Result
-                className='cvat-not-found'
-                status='404'
-                title='Sorry, but the requested cloud storage was not found'
-                subTitle='Please, be sure id you requested exists and you have appropriate permissions'
-            />
-        );
+        return <CloudStorageNotFoundComponent />;
     }
 
     return (
diff --git a/cvat-ui/src/config.tsx b/cvat-ui/src/config.tsx
index 7e0b404b093a..05732d9e83ce 100644
--- a/cvat-ui/src/config.tsx
+++ b/cvat-ui/src/config.tsx
@@ -108,6 +108,7 @@ const DEFAULT_GOOGLE_CLOUD_STORAGE_LOCATIONS: string[][] = [
     ['NAM4', 'US-CENTRAL1 and US-EAST1'],
 ];
 
+const MAXIMUM_NOTIFICATION_MESSAGE_LENGTH = 600; // all above will be sent to console
 const HEALTH_CHECK_RETRIES = 10;
 const HEALTH_CHECK_PERIOD = 3000; // ms
 const HEALTH_CHECK_REQUEST_TIMEOUT = 15000; // ms
@@ -190,4 +191,5 @@ export default {
     REQUEST_SUCCESS_NOTIFICATION_DURATION,
     BLACKLISTED_GO_BACK_PATHS,
     PAID_PLACEHOLDER_CONFIG,
+    MAXIMUM_NOTIFICATION_MESSAGE_LENGTH,
 };
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx
index 61f6c1064c61..9e2748047294 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx
@@ -9,7 +9,7 @@ import { connect } from 'react-redux';
 import { ObjectState, Job } from 'cvat-core-wrapper';
 import isAbleToChangeFrame from 'utils/is-able-to-change-frame';
 import { ThunkDispatch } from 'utils/redux';
-import { updateAnnotationsAsync, changeFrameAsync } from 'actions/annotation-actions';
+import { updateAnnotationsAsync, changeFrameAsync, changeHideActiveObjectAsync } from 'actions/annotation-actions';
 import { CombinedState } from 'reducers';
 import ItemButtonsComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item-buttons';
 
@@ -29,11 +29,13 @@ interface StateToProps {
     outsideDisabled: boolean;
     hiddenDisabled: boolean;
     keyframeDisabled: boolean;
+    editedState: ObjectState | null,
 }
 
 interface DispatchToProps {
     updateAnnotations(statesToUpdate: any[]): void;
     changeFrame(frame: number): void;
+    changeHideEditedState(value: boolean): void;
 }
 
 function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@@ -44,6 +46,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
             player: {
                 frame: { number: frameNumber },
             },
+            editing: { objectState: editedState },
         },
         shortcuts: { normalizedKeyMap },
     } = state;
@@ -61,6 +64,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
         objectState,
         normalizedKeyMap,
         frameNumber,
+        editedState,
         jobInstance: jobInstance as Job,
         outsideDisabled: typeof outsideDisabled === 'undefined' ? false : outsideDisabled,
         hiddenDisabled: typeof hiddenDisabled === 'undefined' ? false : hiddenDisabled,
@@ -76,6 +80,9 @@ function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps {
         changeFrame(frame: number): void {
             dispatch(changeFrameAsync(frame));
         },
+        changeHideEditedState(value: boolean): void {
+            dispatch(changeHideActiveObjectAsync(value));
+        },
     };
 }
 
@@ -145,15 +152,23 @@ class ItemButtonsWrapper extends React.PureComponent<StateToProps & DispatchToPr
     };
 
     private show = (): void => {
-        const { objectState } = this.props;
-        objectState.hidden = false;
-        this.commit();
+        const { objectState, editedState, changeHideEditedState } = this.props;
+        if (objectState.clientID === editedState?.clientID) {
+            changeHideEditedState(false);
+        } else {
+            objectState.hidden = false;
+            this.commit();
+        }
     };
 
     private hide = (): void => {
-        const { objectState } = this.props;
-        objectState.hidden = true;
-        this.commit();
+        const { objectState, editedState, changeHideEditedState } = this.props;
+        if (objectState.clientID === editedState?.clientID) {
+            changeHideEditedState(true);
+        } else {
+            objectState.hidden = true;
+            this.commit();
+        }
     };
 
     private setOccluded = (): void => {
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx
index 7f120cc981d6..16ccdc08bff7 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx
@@ -19,6 +19,7 @@ import {
     switchPropagateVisibility as switchPropagateVisibilityAction,
     removeObject as removeObjectAction,
     fetchAnnotationsAsync,
+    changeHideActiveObjectAsync,
 } from 'actions/annotation-actions';
 import {
     changeShowGroundTruth as changeShowGroundTruthAction,
@@ -26,6 +27,7 @@ import {
 import isAbleToChangeFrame from 'utils/is-able-to-change-frame';
 import {
     CombinedState, StatesOrdering, ObjectType, ColorBy, Workspace,
+    ActiveControl,
 } from 'reducers';
 import { ObjectState, ShapeType } from 'cvat-core-wrapper';
 import { filterAnnotations } from 'utils/filter-annotations';
@@ -56,6 +58,9 @@ interface StateToProps {
     normalizedKeyMap: Record<string, string>;
     showGroundTruth: boolean;
     workspace: Workspace;
+    editedState: ObjectState | null,
+    activeControl: ActiveControl,
+    activeObjectHidden: boolean,
 }
 
 interface DispatchToProps {
@@ -67,6 +72,7 @@ interface DispatchToProps {
     changeFrame(frame: number): void;
     changeGroupColor(group: number, color: string): void;
     changeShowGroundTruth(value: boolean): void;
+    changeHideEditedState(value: boolean): void;
 }
 
 const componentShortcuts = {
@@ -186,6 +192,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
             player: {
                 frame: { number: frameNumber },
             },
+            canvas: {
+                activeControl, activeObjectHidden,
+            },
+            editing: { objectState: editedState },
             colors,
             workspace,
         },
@@ -233,6 +243,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
         normalizedKeyMap,
         showGroundTruth,
         workspace,
+        editedState,
+        activeControl,
+        activeObjectHidden,
     };
 }
 
@@ -263,6 +276,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
             dispatch(changeShowGroundTruthAction(value));
             dispatch(fetchAnnotationsAsync());
         },
+        changeHideEditedState(value: boolean): void {
+            dispatch(changeHideActiveObjectAsync(value));
+        },
     };
 }
 
@@ -388,9 +404,13 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
     }
 
     private hideAllStates(hidden: boolean): void {
-        const { updateAnnotations } = this.props;
+        const { updateAnnotations, editedState, changeHideEditedState } = this.props;
         const { filteredStates } = this.state;
 
+        if (editedState?.shapeType === ShapeType.MASK) {
+            changeHideEditedState(hidden);
+        }
+
         for (const objectState of filteredStates) {
             objectState.hidden = hidden;
         }
@@ -475,6 +495,13 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
             SWITCH_HIDDEN: (event: KeyboardEvent | undefined) => {
                 preventDefault(event);
                 const state = activatedState();
+                const {
+                    editedState, changeHideEditedState, activeControl, activeObjectHidden,
+                } = this.props;
+                if (editedState?.shapeType === ShapeType.MASK || activeControl === ActiveControl.DRAW_MASK) {
+                    const hide = editedState ? !editedState.hidden : !activeObjectHidden;
+                    changeHideEditedState(hide);
+                }
                 if (state) {
                     state.hidden = !state.hidden;
                     updateAnnotations([state]);
diff --git a/cvat-ui/src/containers/tasks-page/task-item.tsx b/cvat-ui/src/containers/tasks-page/task-item.tsx
index 2131fd643417..b8950cd60f5b 100644
--- a/cvat-ui/src/containers/tasks-page/task-item.tsx
+++ b/cvat-ui/src/containers/tasks-page/task-item.tsx
@@ -6,11 +6,9 @@
 import { connect } from 'react-redux';
 
 import { Task, Request } from 'cvat-core-wrapper';
-import {
-    TasksQuery, CombinedState, ActiveInference, PluginComponent,
-} from 'reducers';
+import { CombinedState, ActiveInference, PluginComponent } from 'reducers';
 import TaskItemComponent from 'components/tasks-page/task-item';
-import { getTasksAsync, updateTaskInState as updateTaskInStateAction, getTaskPreviewAsync } from 'actions/tasks-actions';
+import { updateTaskInState as updateTaskInStateAction, getTaskPreviewAsync } from 'actions/tasks-actions';
 import { cancelInferenceAsync } from 'actions/models-actions';
 
 interface StateToProps {
@@ -22,7 +20,6 @@ interface StateToProps {
 }
 
 interface DispatchToProps {
-    getTasks(query: TasksQuery): void;
     updateTaskInState(task: Task): void;
     cancelAutoAnnotation(): void;
 }
@@ -53,9 +50,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
 
 function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
     return {
-        getTasks(query: TasksQuery): void {
-            dispatch(getTasksAsync(query));
-        },
         cancelAutoAnnotation(): void {
             dispatch(cancelInferenceAsync(own.taskID));
         },
diff --git a/cvat-ui/src/containers/tasks-page/tasks-list.tsx b/cvat-ui/src/containers/tasks-page/tasks-list.tsx
index b32cfaf7186a..630e81647691 100644
--- a/cvat-ui/src/containers/tasks-page/tasks-list.tsx
+++ b/cvat-ui/src/containers/tasks-page/tasks-list.tsx
@@ -1,39 +1,24 @@
 // Copyright (C) 2020-2022 Intel Corporation
-// Copyright (C) 2022 CVAT.ai Corporation
+// Copyright (C) 2022-2024 CVAT.ai Corporation
 //
 // SPDX-License-Identifier: MIT
 
 import React from 'react';
 import { connect } from 'react-redux';
-import { TasksState, TasksQuery, CombinedState } from 'reducers';
+import { TasksState, CombinedState } from 'reducers';
 import TasksListComponent from 'components/tasks-page/task-list';
-import { getTasksAsync } from 'actions/tasks-actions';
 
 interface StateToProps {
     tasks: TasksState;
 }
 
-interface DispatchToProps {
-    getTasks: (query: TasksQuery) => void;
-}
-
 function mapStateToProps(state: CombinedState): StateToProps {
     return {
         tasks: state.tasks,
     };
 }
 
-function mapDispatchToProps(dispatch: any): DispatchToProps {
-    return {
-        getTasks: (query: TasksQuery): void => {
-            dispatch(getTasksAsync(query));
-        },
-    };
-}
-
-type TasksListContainerProps = StateToProps & DispatchToProps;
-
-function TasksListContainer(props: TasksListContainerProps): JSX.Element {
+function TasksListContainer(props: StateToProps): JSX.Element {
     const { tasks } = props;
 
     return (
@@ -43,4 +28,4 @@ function TasksListContainer(props: TasksListContainerProps): JSX.Element {
     );
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(TasksListContainer);
+export default connect(mapStateToProps)(TasksListContainer);
diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts
index 94b70373a1c7..52f71d6044bc 100644
--- a/cvat-ui/src/cvat-core-wrapper.ts
+++ b/cvat-ui/src/cvat-core-wrapper.ts
@@ -28,7 +28,7 @@ import { ServerError, RequestError } from 'cvat-core/src/exceptions';
 import {
     ShapeType, LabelType, ModelKind, ModelProviders,
     ModelReturnType, DimensionType, JobType,
-    JobStage, JobState, RQStatus,
+    JobStage, JobState, RQStatus, StorageLocation,
 } from 'cvat-core/src/enums';
 import { Storage, StorageData } from 'cvat-core/src/storage';
 import Issue from 'cvat-core/src/issue';
@@ -109,6 +109,7 @@ export {
     Request,
     JobValidationLayout,
     TaskValidationLayout,
+    StorageLocation,
 };
 
 export type {
diff --git a/cvat-ui/src/index.html b/cvat-ui/src/index.html
index 65e4812fe512..f23b42af30d9 100644
--- a/cvat-ui/src/index.html
+++ b/cvat-ui/src/index.html
@@ -16,7 +16,6 @@
             name="description"
             content="Computer Vision Annotation Tool (CVAT) is a free, open source, web-based image and video annotation tool which is used for labeling data for computer vision algorithms. CVAT supports the primary tasks of supervised machine learning: object detection, image classification, and image segmentation. CVAT allows users to annotate data for each of these cases"
         />
-        <meta name="”robots”" content="index, follow" />
         <link rel="preconnect" href="https://fonts.googleapis.com">
         <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
         <link href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,400;8..144,500;8..144,600;8..144,700;8..144,800&family=Sora&display=swap" rel="stylesheet">    </head>
diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts
index 847a0b84d93d..c21aff497548 100644
--- a/cvat-ui/src/reducers/annotation-reducer.ts
+++ b/cvat-ui/src/reducers/annotation-reducer.ts
@@ -51,6 +51,7 @@ const defaultState: AnnotationState = {
         instance: null,
         ready: false,
         activeControl: ActiveControl.CURSOR,
+        activeObjectHidden: false,
     },
     job: {
         openTime: null,
@@ -66,6 +67,7 @@ const defaultState: AnnotationState = {
             groundTruthJobFramesMeta: null,
             groundTruthInstance: null,
         },
+        frameNumbers: [],
         instance: null,
         meta: null,
         attributes: {},
@@ -94,6 +96,9 @@ const defaultState: AnnotationState = {
         activeLabelID: null,
         activeObjectType: ObjectType.SHAPE,
     },
+    editing: {
+        objectState: null,
+    },
     annotations: {
         activatedStateID: null,
         activatedElementID: null,
@@ -161,6 +166,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
                 job,
                 jobMeta,
                 openTime,
+                frameNumbers,
                 frameNumber: number,
                 frameFilename: filename,
                 relatedFiles,
@@ -205,6 +211,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
                 job: {
                     ...state.job,
                     openTime,
+                    frameNumbers,
                     fetching: false,
                     instance: job,
                     meta: jobMeta,
@@ -633,6 +640,26 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
                 },
             };
         }
+        case AnnotationActionTypes.UPDATE_EDITED_STATE: {
+            const { objectState } = action.payload;
+            return {
+                ...state,
+                editing: {
+                    ...state.editing,
+                    objectState,
+                },
+            };
+        }
+        case AnnotationActionTypes.HIDE_ACTIVE_OBJECT: {
+            const { hide } = action.payload;
+            return {
+                ...state,
+                canvas: {
+                    ...state.canvas,
+                    activeObjectHidden: hide,
+                },
+            };
+        }
         case AnnotationActionTypes.REMOVE_OBJECT_SUCCESS: {
             const { objectState, history } = action.payload;
             const contextMenuClientID = state.canvas.contextMenu.clientID;
@@ -980,7 +1007,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
         }
         case AnnotationActionTypes.CHANGE_WORKSPACE: {
             const { workspace } = action.payload;
-            if (state.canvas.activeControl !== ActiveControl.CURSOR) {
+            if (state.canvas.activeControl !== ActiveControl.CURSOR && state.workspace !== Workspace.SINGLE_SHAPE) {
                 return state;
             }
 
@@ -992,6 +1019,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
                     states: state.annotations.states.filter((_state) => !_state.isGroundTruth),
                     activatedStateID: null,
                     activatedAttributeID: null,
+
+                },
+                canvas: {
+                    ...state.canvas,
+                    activeControl: ActiveControl.CURSOR,
                 },
             };
         }
diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts
index 6c297cd5f4ac..14196846a393 100644
--- a/cvat-ui/src/reducers/index.ts
+++ b/cvat-ui/src/reducers/index.ts
@@ -8,7 +8,7 @@ import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrap
 import {
     Webhook, MLModel, Organization, Job, Task, Project, Label, User,
     QualityConflict, FramesMetaData, RQStatus, Event, Invitation, SerializedAPISchema,
-    Request, JobValidationLayout, QualitySettings, TaskValidationLayout,
+    Request, JobValidationLayout, QualitySettings, TaskValidationLayout, ObjectState,
 } from 'cvat-core-wrapper';
 import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
 import { KeyMap, KeyMapItem } from 'utils/mousetrap-react';
@@ -38,6 +38,7 @@ interface Preview {
 }
 
 export interface ProjectsState {
+    fetchingTimestamp: number;
     initialized: boolean;
     fetching: boolean;
     count: number;
@@ -75,6 +76,7 @@ export interface JobsQuery {
 }
 
 export interface JobsState {
+    fetchingTimestamp: number;
     query: JobsQuery;
     fetching: boolean;
     count: number;
@@ -90,6 +92,7 @@ export interface JobsState {
 }
 
 export interface TasksState {
+    fetchingTimestamp: number;
     initialized: boolean;
     fetching: boolean;
     moveTask: {
@@ -694,6 +697,10 @@ export enum NavigationType {
     EMPTY = 'empty',
 }
 
+export interface EditingState {
+    objectState: ObjectState | null;
+}
+
 export interface AnnotationState {
     activities: {
         loads: {
@@ -719,6 +726,7 @@ export interface AnnotationState {
         instance: Canvas | Canvas3d | null;
         ready: boolean;
         activeControl: ActiveControl;
+        activeObjectHidden: boolean;
     };
     job: {
         openTime: null | number;
@@ -726,6 +734,7 @@ export interface AnnotationState {
         requestedId: number | null;
         meta: FramesMetaData | null;
         instance: Job | null | undefined;
+        frameNumbers: number[];
         queryParameters: {
             initialOpenGuide: boolean;
             defaultLabel: string | null;
@@ -768,6 +777,7 @@ export interface AnnotationState {
         activeObjectType: ObjectType;
         activeInitialState?: any;
     };
+    editing: EditingState;
     annotations: {
         activatedStateID: number | null;
         activatedElementID: number | null;
diff --git a/cvat-ui/src/reducers/jobs-reducer.ts b/cvat-ui/src/reducers/jobs-reducer.ts
index c7b07fc1fa30..4be7c5c285f8 100644
--- a/cvat-ui/src/reducers/jobs-reducer.ts
+++ b/cvat-ui/src/reducers/jobs-reducer.ts
@@ -7,6 +7,7 @@ import { JobsActions, JobsActionTypes } from 'actions/jobs-actions';
 import { JobsState } from '.';
 
 const defaultState: JobsState = {
+    fetchingTimestamp: Date.now(),
     fetching: false,
     count: 0,
     query: {
@@ -27,6 +28,7 @@ export default (state: JobsState = defaultState, action: JobsActions): JobsState
         case JobsActionTypes.GET_JOBS: {
             return {
                 ...state,
+                fetchingTimestamp: action.payload.fetchingTimestamp,
                 fetching: true,
                 query: {
                     ...defaultState.query,
diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts
index 5bc4becb223e..6bb56f50017c 100644
--- a/cvat-ui/src/reducers/notifications-reducer.ts
+++ b/cvat-ui/src/reducers/notifications-reducer.ts
@@ -5,7 +5,7 @@
 
 import { AnyAction } from 'redux';
 
-import { ServerError, RequestError } from 'cvat-core-wrapper';
+import { ServerError, RequestError, StorageLocation } from 'cvat-core-wrapper';
 import { AuthActionTypes } from 'actions/auth-actions';
 import { FormatsActionTypes } from 'actions/formats-actions';
 import { ModelsActionTypes } from 'actions/models-actions';
@@ -355,7 +355,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
                         ...state.messages.auth,
                         requestPasswordResetDone: {
                             message: `Check your email for a link to reset your password.
-                            If it doesn’t appear within a few minutes, check your spam folder.`,
+                            If it doesn't appear within a few minutes, check your spam folder.`,
                         },
                     },
                 },
@@ -546,9 +546,9 @@ export default function (state = defaultState, action: AnyAction): Notifications
                 instance, instanceType, resource, target,
             } = action.payload;
             let description = `Export ${resource} for ${instanceType} ${instance.id} is finished. `;
-            if (target === 'local') {
+            if (target === StorageLocation.LOCAL) {
                 description += 'You can [download it here](/requests).';
-            } else if (target === 'cloudstorage') {
+            } else if (target === StorageLocation.CLOUD_STORAGE) {
                 description =
                     `Export ${resource} for ${instanceType} ${instance.id} has been uploaded to cloud storage.`;
             }
@@ -590,9 +590,9 @@ export default function (state = defaultState, action: AnyAction): Notifications
                 instance, instanceType, target,
             } = action.payload;
             let description = `Backup for the ${instanceType} ${instance.id} is finished. `;
-            if (target === 'local') {
+            if (target === StorageLocation.LOCAL) {
                 description += 'You can [download it here](/requests).';
-            } else if (target === 'cloudstorage') {
+            } else if (target === StorageLocation.CLOUD_STORAGE) {
                 description =
                     `Backup for the ${instanceType} ${instance.id} has been uploaded to cloud storage.`;
             }
diff --git a/cvat-ui/src/reducers/projects-reducer.ts b/cvat-ui/src/reducers/projects-reducer.ts
index 7c2db7cd80ac..5f74f1c5c620 100644
--- a/cvat-ui/src/reducers/projects-reducer.ts
+++ b/cvat-ui/src/reducers/projects-reducer.ts
@@ -12,6 +12,7 @@ import { AuthActionTypes } from 'actions/auth-actions';
 import { ProjectsState } from '.';
 
 const defaultState: ProjectsState = {
+    fetchingTimestamp: Date.now(),
     initialized: false,
     fetching: false,
     count: 0,
@@ -59,6 +60,7 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project
         case ProjectsActionTypes.GET_PROJECTS:
             return {
                 ...state,
+                fetchingTimestamp: action.payload.fetchingTimestamp,
                 initialized: false,
                 fetching: true,
                 count: 0,
diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts
index ce2e88258c0c..b686bbc7f26c 100644
--- a/cvat-ui/src/reducers/tasks-reducer.ts
+++ b/cvat-ui/src/reducers/tasks-reducer.ts
@@ -12,6 +12,7 @@ import { ProjectsActionTypes } from 'actions/projects-actions';
 import { TasksState } from '.';
 
 const defaultState: TasksState = {
+    fetchingTimestamp: Date.now(),
     initialized: false,
     fetching: false,
     moveTask: {
@@ -43,6 +44,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
                     ...state.activities,
                     deletes: {},
                 },
+                fetchingTimestamp: action.payload.fetchingTimestamp,
                 initialized: false,
                 fetching: true,
                 count: 0,
diff --git a/cvat/__init__.py b/cvat/__init__.py
index ac47316ad0a1..10ef426963f5 100644
--- a/cvat/__init__.py
+++ b/cvat/__init__.py
@@ -4,6 +4,6 @@
 
 from cvat.utils.version import get_version
 
-VERSION = (2, 21, 3, 'final', 0)
+VERSION = (2, 22, 0, "final", 0)
 
 __version__ = get_version(VERSION)
diff --git a/cvat/apps/analytics_report/rules/analytics_reports.rego b/cvat/apps/analytics_report/rules/analytics_reports.rego
index 706d6e701dbe..87910192779b 100644
--- a/cvat/apps/analytics_report/rules/analytics_reports.rego
+++ b/cvat/apps/analytics_report/rules/analytics_reports.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py
index 35d4b902a53a..1c70520a7090 100644
--- a/cvat/apps/dataset_manager/bindings.py
+++ b/cvat/apps/dataset_manager/bindings.py
@@ -285,6 +285,7 @@ def __init__(self,
         self._db_data: models.Data = db_task.data
         self._use_server_track_ids = use_server_track_ids
         self._required_frames = included_frames
+        self._initialized_included_frames: Optional[Set[int]] = None
         self._db_subset = db_task.subset
 
         super().__init__(db_task)
@@ -536,12 +537,14 @@ def shapes(self):
                 yield self._export_labeled_shape(shape)
 
     def get_included_frames(self):
-        return set(
-            i for i in self.rel_range
-            if not self._is_frame_deleted(i)
-            and not self._is_frame_excluded(i)
-            and self._is_frame_required(i)
-        )
+        if self._initialized_included_frames is None:
+            self._initialized_included_frames = set(
+                i for i in self.rel_range
+                if not self._is_frame_deleted(i)
+                and not self._is_frame_excluded(i)
+                and self._is_frame_required(i)
+            )
+        return self._initialized_included_frames
 
     def _is_frame_deleted(self, frame):
         return frame in self._deleted_frames
@@ -1112,7 +1115,10 @@ def _init_frame_info(self):
                 } for frame in range(task.data.size)})
             else:
                 self._frame_info.update({(task.id, self.rel_frame_id(task.id, db_image.frame)): {
-                    "path": mangle_image_name(db_image.path, defaulted_subset, original_names),
+                    # do not modify honeypot names since they will be excluded from the dataset
+                    # and their quantity should not affect the validation frame name
+                    "path": mangle_image_name(db_image.path, defaulted_subset, original_names) \
+                        if not db_image.is_placeholder else db_image.path,
                     "id": db_image.id,
                     "width": db_image.width,
                     "height": db_image.height,
@@ -1271,25 +1277,36 @@ def get_frame(task_id: int, idx: int) -> ProjectData.Frame:
             return frames[(frame_info["subset"], abs_frame)]
 
         if include_empty:
-            for ident in sorted(self._frame_info):
-                if ident not in self._deleted_frames:
-                    get_frame(*ident)
+            for task_id, frame in sorted(self._frame_info):
+                if not self._tasks_data.get(task_id):
+                    self.init_task_data(task_id)
+
+                task_included_frames = self._tasks_data[task_id].get_included_frames()
+                if frame in task_included_frames:
+                    get_frame(task_id, frame)
+
+        for task_data in self.task_data:
+            task: Task = task_data.db_instance
 
-        for task in self._db_tasks.values():
             anno_manager = AnnotationManager(
                 self._annotation_irs[task.id], dimension=self._annotation_irs[task.id].dimension
             )
+            task_included_frames = task_data.get_included_frames()
+
             for shape in sorted(
                 anno_manager.to_shapes(
                     task.data.size,
+                    included_frames=task_included_frames,
                     include_outside=False,
                     use_server_track_ids=self._use_server_track_ids
                 ),
                 key=lambda shape: shape.get("z_order", 0)
             ):
-                if (task.id, shape['frame']) not in self._frame_info or (task.id, shape['frame']) in self._deleted_frames:
+                if shape['frame'] in task_data.deleted_frames:
                     continue
 
+                assert (task.id, shape['frame']) in self._frame_info
+
                 if 'track_id' in shape:
                     if shape['outside']:
                         continue
@@ -1368,23 +1385,33 @@ def soft_attribute_import(self, value: bool):
         for task_data in self._tasks_data.values():
             task_data.soft_attribute_import = value
 
+
+    def init_task_data(self, task_id: int) -> TaskData:
+        try:
+            task = self._db_tasks[task_id]
+        except KeyError as ex:
+            raise Exception("There is no such task in the project") from ex
+
+        task_data = TaskData(
+            annotation_ir=self._annotation_irs[task_id],
+            db_task=task,
+            host=self._host,
+            create_callback=self._task_annotations[task_id].create \
+                if self._task_annotations is not None else None,
+        )
+        task_data._MAX_ANNO_SIZE //= len(self._db_tasks)
+        task_data.soft_attribute_import = self.soft_attribute_import
+        self._tasks_data[task_id] = task_data
+
+        return task_data
+
     @property
     def task_data(self):
-        for task_id, task in self._db_tasks.items():
+        for task_id in self._db_tasks.keys():
             if task_id in self._tasks_data:
                 yield self._tasks_data[task_id]
             else:
-                task_data = TaskData(
-                    annotation_ir=self._annotation_irs[task_id],
-                    db_task=task,
-                    host=self._host,
-                    create_callback=self._task_annotations[task_id].create \
-                        if self._task_annotations is not None else None,
-                )
-                task_data._MAX_ANNO_SIZE //= len(self._db_tasks)
-                task_data.soft_attribute_import = self.soft_attribute_import
-                self._tasks_data[task_id] = task_data
-                yield task_data
+                yield self.init_task_data(task_id)
 
     @staticmethod
     def _get_filename(path):
diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py
index 4651fd398451..03ef389599e8 100644
--- a/cvat/apps/dataset_manager/formats/cvat.py
+++ b/cvat/apps/dataset_manager/formats/cvat.py
@@ -9,7 +9,7 @@
 from collections import OrderedDict
 from glob import glob
 from io import BufferedWriter
-from typing import Callable
+from typing import Callable, Union
 
 from datumaro.components.annotation import (AnnotationType, Bbox, Label,
                                             LabelCategories, Points, Polygon,
@@ -22,7 +22,7 @@
 from datumaro.util.image import Image
 from defusedxml import ElementTree
 
-from cvat.apps.dataset_manager.bindings import (ProjectData, CommonData, detect_dataset,
+from cvat.apps.dataset_manager.bindings import (ProjectData, TaskData, JobData, detect_dataset,
                                                 get_defaulted_subset,
                                                 import_dm_annotations,
                                                 match_dm_item)
@@ -1370,7 +1370,7 @@ def dump_project_anno(dst_file: BufferedWriter, project_data: ProjectData, callb
     callback(dumper, project_data)
     dumper.close_document()
 
-def dump_media_files(instance_data: CommonData, img_dir: str, project_data: ProjectData = None):
+def dump_media_files(instance_data: Union[TaskData, JobData], img_dir: str, project_data: ProjectData = None):
     frame_provider = make_frame_provider(instance_data.db_instance)
 
     ext = ''
@@ -1383,9 +1383,11 @@ def dump_media_files(instance_data: CommonData, img_dir: str, project_data: Proj
         quality=FrameQuality.ORIGINAL,
         out_type=FrameOutputType.BUFFER,
     )
+    included_frames = instance_data.get_included_frames()
+
     for frame_id, frame in zip(instance_data.rel_range, frames):
-        if (project_data is not None and (instance_data.db_instance.id, frame_id) in project_data.deleted_frames) \
-            or frame_id in instance_data.deleted_frames:
+        # exclude deleted frames and honeypots
+        if frame_id not in included_frames:
             continue
         frame_name = instance_data.frame_info[frame_id]['path'] if project_data is None \
             else project_data.frame_info[(instance_data.db_instance.id, frame_id)]['path']
diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py
index 990b2b31009a..295e405a41da 100644
--- a/cvat/apps/engine/cache.py
+++ b/cvat/apps/engine/cache.py
@@ -54,7 +54,7 @@
     ZipChunkWriter,
     ZipCompressedChunkWriter,
 )
-from cvat.apps.engine.utils import md5_hash, load_image
+from cvat.apps.engine.utils import load_image, md5_hash
 from utils.dataset_manifest import ImageManifestManager
 
 slogger = ServerLogManager(__name__)
diff --git a/cvat/apps/engine/frame_provider.py b/cvat/apps/engine/frame_provider.py
index f397f0d568b1..1787d84aac40 100644
--- a/cvat/apps/engine/frame_provider.py
+++ b/cvat/apps/engine/frame_provider.py
@@ -369,7 +369,7 @@ def iterate_frames(
         quality: FrameQuality = FrameQuality.ORIGINAL,
         out_type: FrameOutputType = FrameOutputType.BUFFER,
     ) -> Iterator[DataWithMeta[AnyFrame]]:
-        frame_range = itertools.count(start_frame, self._db_task.data.get_frame_step())
+        frame_range = itertools.count(start_frame)
         if stop_frame:
             frame_range = itertools.takewhile(lambda x: x <= stop_frame, frame_range)
 
@@ -377,7 +377,10 @@ def iterate_frames(
         db_segment_frame_set = None
         db_segment_frame_provider = None
         for idx in frame_range:
-            if db_segment and idx not in db_segment_frame_set:
+            if (
+                db_segment
+                and self._get_abs_frame_number(self._db_task.data, idx) not in db_segment_frame_set
+            ):
                 db_segment = None
                 db_segment_frame_set = None
                 db_segment_frame_provider = None
diff --git a/cvat/apps/engine/rules/annotationguides.rego b/cvat/apps/engine/rules/annotationguides.rego
index dd512af6d79a..6429eecb23a4 100644
--- a/cvat/apps/engine/rules/annotationguides.rego
+++ b/cvat/apps/engine/rules/annotationguides.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/engine/rules/cloudstorages.rego b/cvat/apps/engine/rules/cloudstorages.rego
index 3e278a35a7d5..04f8e0e45369 100644
--- a/cvat/apps/engine/rules/cloudstorages.rego
+++ b/cvat/apps/engine/rules/cloudstorages.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/engine/rules/comments.rego b/cvat/apps/engine/rules/comments.rego
index 019a5ebcecc4..9384d829b091 100644
--- a/cvat/apps/engine/rules/comments.rego
+++ b/cvat/apps/engine/rules/comments.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/engine/rules/issues.rego b/cvat/apps/engine/rules/issues.rego
index 803dab16c019..d8a487cbcdb1 100644
--- a/cvat/apps/engine/rules/issues.rego
+++ b/cvat/apps/engine/rules/issues.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/engine/rules/jobs.rego b/cvat/apps/engine/rules/jobs.rego
index 8068f7d6fdf9..7980a08d1bc0 100644
--- a/cvat/apps/engine/rules/jobs.rego
+++ b/cvat/apps/engine/rules/jobs.rego
@@ -12,7 +12,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/engine/rules/labels.rego b/cvat/apps/engine/rules/labels.rego
index a50296377683..1d4344da7fe4 100644
--- a/cvat/apps/engine/rules/labels.rego
+++ b/cvat/apps/engine/rules/labels.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/engine/rules/projects.rego b/cvat/apps/engine/rules/projects.rego
index 8e40ddc43c8d..bdaabb120135 100644
--- a/cvat/apps/engine/rules/projects.rego
+++ b/cvat/apps/engine/rules/projects.rego
@@ -12,7 +12,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
@@ -59,19 +59,6 @@ allow if {
     organizations.has_perm(organizations.SUPERVISOR)
 }
 
-allow if {
-    input.scope in {utils.CREATE, utils.IMPORT_BACKUP}
-    utils.is_sandbox
-    utils.has_perm(utils.BUSINESS)
-}
-
-allow if {
-    input.scope in {utils.CREATE, utils.IMPORT_BACKUP}
-    input.auth.organization.id == input.resource.organization.id
-    utils.has_perm(utils.BUSINESS)
-    organizations.has_perm(organizations.SUPERVISOR)
-}
-
 allow if {
     input.scope == utils.LIST
     utils.is_sandbox
diff --git a/cvat/apps/engine/rules/server.rego b/cvat/apps/engine/rules/server.rego
index bfe3b47a0d46..6833826a0762 100644
--- a/cvat/apps/engine/rules/server.rego
+++ b/cvat/apps/engine/rules/server.rego
@@ -9,7 +9,7 @@ import data.utils
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/engine/rules/tasks.rego b/cvat/apps/engine/rules/tasks.rego
index 99d126d2b443..f020cf4ac976 100644
--- a/cvat/apps/engine/rules/tasks.rego
+++ b/cvat/apps/engine/rules/tasks.rego
@@ -13,7 +13,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
@@ -93,19 +93,6 @@ allow if {
     organizations.has_perm(organizations.SUPERVISOR)
 }
 
-allow if {
-    input.scope in {utils.CREATE, utils.IMPORT_BACKUP}
-    utils.is_sandbox
-    utils.has_perm(utils.BUSINESS)
-}
-
-allow if {
-    input.scope in {utils.CREATE, utils.IMPORT_BACKUP}
-    input.auth.organization.id == input.resource.organization.id
-    utils.has_perm(utils.BUSINESS)
-    organizations.has_perm(organizations.SUPERVISOR)
-}
-
 allow if {
     input.scope == utils.CREATE_IN_PROJECT
     utils.is_sandbox
@@ -128,20 +115,6 @@ allow if {
     is_project_staff
 }
 
-allow if {
-    input.scope == utils.CREATE_IN_PROJECT
-    utils.is_sandbox
-    utils.has_perm(utils.BUSINESS)
-    is_project_staff
-}
-
-allow if {
-    input.scope == utils.CREATE_IN_PROJECT
-    input.auth.organization.id == input.resource.organization.id
-    utils.has_perm(utils.BUSINESS)
-    organizations.has_perm(organizations.SUPERVISOR)
-}
-
 allow if {
     input.scope == utils.LIST
     utils.is_sandbox
diff --git a/cvat/apps/engine/rules/users.rego b/cvat/apps/engine/rules/users.rego
index 63469228e11a..34cb0f4866d3 100644
--- a/cvat/apps/engine/rules/users.rego
+++ b/cvat/apps/engine/rules/users.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/events/rules/events.rego b/cvat/apps/events/rules/events.rego
index 0152ec721ba8..58ec43763b2f 100644
--- a/cvat/apps/events/rules/events.rego
+++ b/cvat/apps/events/rules/events.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/iam/migrations/0001_remove_business_group.py b/cvat/apps/iam/migrations/0001_remove_business_group.py
new file mode 100644
index 000000000000..2bf1a56b4065
--- /dev/null
+++ b/cvat/apps/iam/migrations/0001_remove_business_group.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.16 on 2024-10-30 12:03
+from django.conf import settings
+from django.db import migrations
+
+
+BUSINESS_GROUP_NAME = "business"
+USER_GROUP_NAME = "user"
+
+
+def delete_business_group(apps, schema_editor):
+    Group = apps.get_model('auth', 'Group')
+    User = apps.get_model(settings.AUTH_USER_MODEL)
+
+    if user_group := Group.objects.filter(name=USER_GROUP_NAME).first():
+        user_group.user_set.add(*User.objects.filter(groups__name=BUSINESS_GROUP_NAME))
+
+    Group.objects.filter(name=BUSINESS_GROUP_NAME).delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            delete_business_group,
+            reverse_code=migrations.RunPython.noop,
+        ),
+    ]
diff --git a/cvat/apps/iam/migrations/__init__.py b/cvat/apps/iam/migrations/__init__.py
new file mode 100644
index 000000000000..bd6d6576ecf2
--- /dev/null
+++ b/cvat/apps/iam/migrations/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (C) 2024 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py
index 55a91cc570b9..bb2ab44a414f 100644
--- a/cvat/apps/iam/permissions.py
+++ b/cvat/apps/iam/permissions.py
@@ -198,9 +198,9 @@ def filter(self, queryset):
             q_objects.append(Q())
 
         # By default, a QuerySet will not eliminate duplicate rows. If your
-        # query spans multiple tables (e.g. members__user_id, owner_id), it’s
+        # query spans multiple tables (e.g. members__user_id, owner_id), it's
         # possible to get duplicate results when a QuerySet is evaluated.
-        # That’s when you’d use distinct().
+        # That's when you'd use distinct().
         return queryset.filter(q_objects[0]).distinct()
 
     @classmethod
diff --git a/cvat/apps/iam/rules/utils.rego b/cvat/apps/iam/rules/utils.rego
index 4bd64f0ae108..148b2ec1a2a1 100644
--- a/cvat/apps/iam/rules/utils.rego
+++ b/cvat/apps/iam/rules/utils.rego
@@ -4,7 +4,6 @@ import rego.v1
 
 # Groups
 ADMIN := "admin"
-BUSINESS := "business"
 USER := "user"
 WORKER := "worker"
 
@@ -65,7 +64,6 @@ UPDATE_VALIDATION_LAYOUT := "update:validation_layout"
 
 get_priority(privilege) := {
     ADMIN: 0,
-    BUSINESS: 50,
     USER: 75,
     WORKER: 100,
     null: 1000
@@ -79,10 +77,6 @@ is_admin if {
     input.auth.user.privilege == ADMIN
 }
 
-is_business if {
-    input.auth.user.privilege == BUSINESS
-}
-
 is_user if {
     input.auth.user.privilege == USER
 }
diff --git a/cvat/apps/lambda_manager/rules/lambda.rego b/cvat/apps/lambda_manager/rules/lambda.rego
index 2829860c0932..7b3b6c828975 100644
--- a/cvat/apps/lambda_manager/rules/lambda.rego
+++ b/cvat/apps/lambda_manager/rules/lambda.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/log_viewer/permissions.py b/cvat/apps/log_viewer/permissions.py
index c50cf9302c61..d25aa7fe275a 100644
--- a/cvat/apps/log_viewer/permissions.py
+++ b/cvat/apps/log_viewer/permissions.py
@@ -49,6 +49,4 @@ def get_scopes(request, view, obj):
         }[view.action]]
 
     def get_resource(self):
-        return {
-            'visibility': 'public' if settings.RESTRICTIONS['analytics_visibility'] else 'private',
-        }
+        return None
diff --git a/cvat/apps/log_viewer/rules/analytics.rego b/cvat/apps/log_viewer/rules/analytics.rego
index b43a9c1b1115..f40653f63e2b 100644
--- a/cvat/apps/log_viewer/rules/analytics.rego
+++ b/cvat/apps/log_viewer/rules/analytics.rego
@@ -9,7 +9,7 @@ import data.utils
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null,
+#             "privilege": <"admin"|"user"|"worker"> or null,
 #             "has_analytics_access": <true|false>
 #         },
 #         "organization": {
@@ -22,19 +22,10 @@ import data.utils
 #             }
 #         } or null,
 #     },
-#     "resource": {
-#         "visibility": <"public"|"private"> or null,
-#     }
 # }
 
 default allow := false
 
-allow if {
-    input.resource.visibility == utils.PUBLIC
-    input.scope == utils.VIEW
-    utils.has_perm(utils.BUSINESS)
-}
-
 allow if {
     input.auth.user.has_analytics_access
 }
diff --git a/cvat/apps/organizations/rules/invitations.rego b/cvat/apps/organizations/rules/invitations.rego
index 3a51f76128e5..2e15ba4a8637 100644
--- a/cvat/apps/organizations/rules/invitations.rego
+++ b/cvat/apps/organizations/rules/invitations.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/organizations/rules/memberships.rego b/cvat/apps/organizations/rules/memberships.rego
index c23f3039ff16..09752e4b7007 100644
--- a/cvat/apps/organizations/rules/memberships.rego
+++ b/cvat/apps/organizations/rules/memberships.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/organizations/rules/organizations.rego b/cvat/apps/organizations/rules/organizations.rego
index 24643feab703..6d0a8c29c19b 100644
--- a/cvat/apps/organizations/rules/organizations.rego
+++ b/cvat/apps/organizations/rules/organizations.rego
@@ -9,7 +9,7 @@ import data.utils
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": null,
 #     },
@@ -69,11 +69,6 @@ allow if {
     utils.has_perm(utils.USER)
 }
 
-allow if {
-    input.scope == utils.CREATE
-    utils.has_perm(utils.BUSINESS)
-}
-
 filter := [] if { # Django Q object to filter list of entries
     utils.is_admin
 } else := qobject if {
diff --git a/cvat/apps/profiler.py b/cvat/apps/profiler.py
index 45ddbc95f478..0a3885bb00a5 100644
--- a/cvat/apps/profiler.py
+++ b/cvat/apps/profiler.py
@@ -1,13 +1,16 @@
 from django.apps import apps
 
-if apps.is_installed('silk'):
+if apps.is_installed("silk"):
     from silk.profiling.profiler import silk_profile  # pylint: disable=unused-import
 else:
     from functools import wraps
+
     def silk_profile(name=None):
         def profile(f):
             @wraps(f)
             def wrapped(*args, **kwargs):
                 return f(*args, **kwargs)
+
             return wrapped
+
         return profile
diff --git a/cvat/apps/quality_control/migrations/0004_qualitysettings_point_size_base.py b/cvat/apps/quality_control/migrations/0004_qualitysettings_point_size_base.py
new file mode 100644
index 000000000000..024d263356ac
--- /dev/null
+++ b/cvat/apps/quality_control/migrations/0004_qualitysettings_point_size_base.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.15 on 2024-11-06 15:39
+
+from django.db import migrations, models
+
+import cvat.apps.quality_control.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("quality_control", "0003_qualityreport_assignee_last_updated_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="qualitysettings",
+            name="point_size_base",
+            field=models.CharField(
+                choices=[("image_size", "IMAGE_SIZE"), ("group_bbox_size", "GROUP_BBOX_SIZE")],
+                default=cvat.apps.quality_control.models.PointSizeBase["GROUP_BBOX_SIZE"],
+                max_length=32,
+            ),
+        ),
+    ]
diff --git a/cvat/apps/quality_control/migrations/0005_qualitysettings_match_empty.py b/cvat/apps/quality_control/migrations/0005_qualitysettings_match_empty.py
new file mode 100644
index 000000000000..dba6a0c9bc43
--- /dev/null
+++ b/cvat/apps/quality_control/migrations/0005_qualitysettings_match_empty.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.15 on 2024-11-05 14:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("quality_control", "0004_qualitysettings_point_size_base"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="qualitysettings",
+            name="match_empty_frames",
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/cvat/apps/quality_control/models.py b/cvat/apps/quality_control/models.py
index 37f0f1f9612d..b8cf76873597 100644
--- a/cvat/apps/quality_control/models.py
+++ b/cvat/apps/quality_control/models.py
@@ -196,6 +196,18 @@ def clean(self) -> None:
             raise ValidationError(f"Unexpected type value '{self.type}'")
 
 
+class PointSizeBase(str, Enum):
+    IMAGE_SIZE = "image_size"
+    GROUP_BBOX_SIZE = "group_bbox_size"
+
+    def __str__(self) -> str:
+        return self.value
+
+    @classmethod
+    def choices(cls):
+        return tuple((x.value, x.name) for x in cls)
+
+
 class QualitySettings(models.Model):
     task = models.OneToOneField(Task, on_delete=models.CASCADE, related_name="quality_settings")
 
@@ -205,6 +217,10 @@ class QualitySettings(models.Model):
 
     low_overlap_threshold = models.FloatField()
 
+    point_size_base = models.CharField(
+        max_length=32, choices=PointSizeBase.choices(), default=PointSizeBase.GROUP_BBOX_SIZE
+    )
+
     compare_line_orientation = models.BooleanField()
     line_orientation_threshold = models.FloatField()
 
@@ -218,6 +234,8 @@ class QualitySettings(models.Model):
 
     compare_attributes = models.BooleanField()
 
+    match_empty_frames = models.BooleanField(default=False)
+
     target_metric = models.CharField(
         max_length=32,
         choices=QualityTargetMetricType.choices(),
diff --git a/cvat/apps/quality_control/quality_reports.py b/cvat/apps/quality_control/quality_reports.py
index 437cdb72615f..f5e527468aa3 100644
--- a/cvat/apps/quality_control/quality_reports.py
+++ b/cvat/apps/quality_control/quality_reports.py
@@ -187,6 +187,9 @@ class ComparisonParameters(_Serializable):
     oks_sigma: float = 0.09
     "Like IoU threshold, but for points, % of the bbox area to match a pair of points"
 
+    point_size_base: models.PointSizeBase = models.PointSizeBase.GROUP_BBOX_SIZE
+    "Determines how to obtain the object size for point comparisons"
+
     line_thickness: float = 0.01
     "Thickness of polylines, relatively to the (image area) ^ 0.5"
 
@@ -214,6 +217,13 @@ class ComparisonParameters(_Serializable):
     panoptic_comparison: bool = True
     "Use only the visible part of the masks and polygons in comparisons"
 
+    match_empty_frames: bool = False
+    """
+    Consider unannotated (empty) frames as matching. If disabled, quality metrics, such as accuracy,
+    will be 0 if both GT and DS frames have no annotations. When enabled, they will be 1 instead.
+    This will also add virtual annotations to empty frames in the comparison results.
+    """
+
     def _value_serializer(self, v):
         if isinstance(v, dm.AnnotationType):
             return str(v.name)
@@ -229,11 +239,11 @@ def from_dict(cls, d: dict):
 @define(kw_only=True)
 class ConfusionMatrix(_Serializable):
     labels: List[str]
-    rows: np.array
-    precision: np.array
-    recall: np.array
-    accuracy: np.array
-    jaccard_index: Optional[np.array]
+    rows: np.ndarray
+    precision: np.ndarray
+    recall: np.ndarray
+    accuracy: np.ndarray
+    jaccard_index: Optional[np.ndarray]
 
     @property
     def axes(self):
@@ -955,6 +965,7 @@ def __init__(
         # https://cocodataset.org/#keypoints-eval
         # https://github.com/cocodataset/cocoapi/blob/8c9bcc3cf640524c4c20a9c40e89cb6a2f2fa0e9/PythonAPI/pycocotools/cocoeval.py#L523
         oks_sigma: float = 0.09,
+        point_size_base: models.PointSizeBase = models.PointSizeBase.GROUP_BBOX_SIZE,
         compare_line_orientation: bool = False,
         line_torso_radius: float = 0.01,
         panoptic_comparison: bool = False,
@@ -968,6 +979,9 @@ def __init__(
         self.oks_sigma = oks_sigma
         "% of the shape area"
 
+        self.point_size_base = point_size_base
+        "Compare point groups using the group bbox size or the image size"
+
         self.compare_line_orientation = compare_line_orientation
         "Whether lines are oriented or not"
 
@@ -1293,13 +1307,20 @@ def _distance(a: dm.Points, b: dm.Points) -> float:
             else:
                 # Complex case: multiple points, grouped points, points with a bbox
                 # Try to align points and then return the metric
-                # match them in their bbox space
 
-                if dm.ops.bbox_iou(a_bbox, b_bbox) <= 0:
-                    return 0
+                if self.point_size_base == models.PointSizeBase.IMAGE_SIZE:
+                    scale = img_h * img_w
+                elif self.point_size_base == models.PointSizeBase.GROUP_BBOX_SIZE:
+                    # match points in their bbox space
 
-                bbox = dm.ops.mean_bbox([a_bbox, b_bbox])
-                scale = bbox[2] * bbox[3]
+                    if dm.ops.bbox_iou(a_bbox, b_bbox) <= 0:
+                        # this early exit may not work for points forming an axis-aligned line
+                        return 0
+
+                    bbox = dm.ops.mean_bbox([a_bbox, b_bbox])
+                    scale = bbox[2] * bbox[3]
+                else:
+                    assert False, f"Unknown point size base {self.point_size_base}"
 
                 a_points = np.reshape(a.points, (-1, 2))
                 b_points = np.reshape(b.points, (-1, 2))
@@ -1525,6 +1546,7 @@ def __init__(self, categories: dm.CategoriesInfo, *, settings: ComparisonParamet
             panoptic_comparison=settings.panoptic_comparison,
             iou_threshold=settings.iou_threshold,
             oks_sigma=settings.oks_sigma,
+            point_size_base=settings.point_size_base,
             line_torso_radius=settings.line_thickness,
             compare_line_orientation=False,  # should not be taken from outside, handled differently
         )
@@ -1957,8 +1979,18 @@ def _find_closest_unmatched_shape(shape: dm.Annotation):
             gt_label_idx = label_id_map[gt_ann.label] if gt_ann else self._UNMATCHED_IDX
             confusion_matrix[ds_label_idx, gt_label_idx] += 1
 
+        if self.settings.match_empty_frames and not gt_item.annotations and not ds_item.annotations:
+            # Add virtual annotations for empty frames
+            valid_labels_count = 1
+            total_labels_count = 1
+
+            valid_shapes_count = 1
+            total_shapes_count = 1
+            ds_shapes_count = 1
+            gt_shapes_count = 1
+
         self._frame_results[frame_id] = ComparisonReportFrameSummary(
-            annotations=self._generate_annotations_summary(
+            annotations=self._generate_frame_annotations_summary(
                 confusion_matrix, confusion_matrix_labels
             ),
             annotation_components=ComparisonReportAnnotationComponentsSummary(
@@ -2000,9 +2032,8 @@ def _make_zero_confusion_matrix(self) -> Tuple[List[str], np.ndarray, Dict[int,
 
         return label_names, confusion_matrix, label_id_idx_map
 
-    @classmethod
-    def _generate_annotations_summary(
-        cls, confusion_matrix: np.ndarray, confusion_matrix_labels: List[str]
+    def _compute_annotations_summary(
+        self, confusion_matrix: np.ndarray, confusion_matrix_labels: List[str]
     ) -> ComparisonReportAnnotationsSummary:
         matched_ann_counts = np.diag(confusion_matrix)
         ds_ann_counts = np.sum(confusion_matrix, axis=1)
@@ -2022,10 +2053,10 @@ def _generate_annotations_summary(
         ) / (total_annotations_count or 1)
 
         valid_annotations_count = np.sum(matched_ann_counts)
-        missing_annotations_count = np.sum(confusion_matrix[cls._UNMATCHED_IDX, :])
-        extra_annotations_count = np.sum(confusion_matrix[:, cls._UNMATCHED_IDX])
-        ds_annotations_count = np.sum(ds_ann_counts[: cls._UNMATCHED_IDX])
-        gt_annotations_count = np.sum(gt_ann_counts[: cls._UNMATCHED_IDX])
+        missing_annotations_count = np.sum(confusion_matrix[self._UNMATCHED_IDX, :])
+        extra_annotations_count = np.sum(confusion_matrix[:, self._UNMATCHED_IDX])
+        ds_annotations_count = np.sum(ds_ann_counts[: self._UNMATCHED_IDX])
+        gt_annotations_count = np.sum(gt_ann_counts[: self._UNMATCHED_IDX])
 
         return ComparisonReportAnnotationsSummary(
             valid_count=valid_annotations_count,
@@ -2044,12 +2075,24 @@ def _generate_annotations_summary(
             ),
         )
 
-    def generate_report(self) -> ComparisonReport:
-        self._find_gt_conflicts()
+    def _generate_frame_annotations_summary(
+        self, confusion_matrix: np.ndarray, confusion_matrix_labels: List[str]
+    ) -> ComparisonReportAnnotationsSummary:
+        summary = self._compute_annotations_summary(confusion_matrix, confusion_matrix_labels)
+
+        if self.settings.match_empty_frames and summary.total_count == 0:
+            # Add virtual annotations for empty frames
+            summary.valid_count = 1
+            summary.total_count = 1
+            summary.ds_count = 1
+            summary.gt_count = 1
 
+        return summary
+
+    def _generate_dataset_annotations_summary(
+        self, frame_summaries: Dict[int, ComparisonReportFrameSummary]
+    ) -> Tuple[ComparisonReportAnnotationsSummary, ComparisonReportAnnotationComponentsSummary]:
         # accumulate stats
-        intersection_frames = []
-        conflicts = []
         annotation_components = ComparisonReportAnnotationComponentsSummary(
             shape=ComparisonReportAnnotationShapeSummary(
                 valid_count=0,
@@ -2067,19 +2110,52 @@ def generate_report(self) -> ComparisonReport:
             ),
         )
         mean_ious = []
+        empty_frame_count = 0
         confusion_matrix_labels, confusion_matrix, _ = self._make_zero_confusion_matrix()
 
-        for frame_id, frame_result in self._frame_results.items():
-            intersection_frames.append(frame_id)
-            conflicts += frame_result.conflicts
+        for frame_result in frame_summaries.values():
             confusion_matrix += frame_result.annotations.confusion_matrix.rows
 
+            if not np.any(frame_result.annotations.confusion_matrix.rows):
+                empty_frame_count += 1
+
             if annotation_components is None:
                 annotation_components = deepcopy(frame_result.annotation_components)
             else:
                 annotation_components.accumulate(frame_result.annotation_components)
+
             mean_ious.append(frame_result.annotation_components.shape.mean_iou)
 
+        annotation_summary = self._compute_annotations_summary(
+            confusion_matrix, confusion_matrix_labels
+        )
+
+        if self.settings.match_empty_frames and empty_frame_count:
+            # Add virtual annotations for empty frames,
+            # they are not included in the confusion matrix
+            annotation_summary.valid_count += empty_frame_count
+            annotation_summary.total_count += empty_frame_count
+            annotation_summary.ds_count += empty_frame_count
+            annotation_summary.gt_count += empty_frame_count
+
+        # Cannot be computed in accumulate()
+        annotation_components.shape.mean_iou = np.mean(mean_ious)
+
+        return annotation_summary, annotation_components
+
+    def generate_report(self) -> ComparisonReport:
+        self._find_gt_conflicts()
+
+        intersection_frames = []
+        conflicts = []
+        for frame_id, frame_result in self._frame_results.items():
+            intersection_frames.append(frame_id)
+            conflicts += frame_result.conflicts
+
+        annotation_summary, annotations_component_summary = (
+            self._generate_dataset_annotations_summary(self._frame_results)
+        )
+
         return ComparisonReport(
             parameters=self.settings,
             comparison_summary=ComparisonReportComparisonSummary(
@@ -2095,25 +2171,8 @@ def generate_report(self) -> ComparisonReport:
                     [c for c in conflicts if c.severity == AnnotationConflictSeverity.ERROR]
                 ),
                 conflicts_by_type=Counter(c.type for c in conflicts),
-                annotations=self._generate_annotations_summary(
-                    confusion_matrix, confusion_matrix_labels
-                ),
-                annotation_components=ComparisonReportAnnotationComponentsSummary(
-                    shape=ComparisonReportAnnotationShapeSummary(
-                        valid_count=annotation_components.shape.valid_count,
-                        missing_count=annotation_components.shape.missing_count,
-                        extra_count=annotation_components.shape.extra_count,
-                        total_count=annotation_components.shape.total_count,
-                        ds_count=annotation_components.shape.ds_count,
-                        gt_count=annotation_components.shape.gt_count,
-                        mean_iou=np.mean(mean_ious),
-                    ),
-                    label=ComparisonReportAnnotationLabelSummary(
-                        valid_count=annotation_components.label.valid_count,
-                        invalid_count=annotation_components.label.invalid_count,
-                        total_count=annotation_components.label.total_count,
-                    ),
-                ),
+                annotations=annotation_summary,
+                annotation_components=annotations_component_summary,
             ),
             frame_results=self._frame_results,
         )
diff --git a/cvat/apps/quality_control/rules/conflicts.rego b/cvat/apps/quality_control/rules/conflicts.rego
index f8e570b58826..883491128209 100644
--- a/cvat/apps/quality_control/rules/conflicts.rego
+++ b/cvat/apps/quality_control/rules/conflicts.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/quality_control/rules/quality_reports.rego b/cvat/apps/quality_control/rules/quality_reports.rego
index e9dd28b3ec32..98626a5f0ca3 100644
--- a/cvat/apps/quality_control/rules/quality_reports.rego
+++ b/cvat/apps/quality_control/rules/quality_reports.rego
@@ -11,7 +11,7 @@ import data.quality_utils
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/quality_control/rules/quality_settings.rego b/cvat/apps/quality_control/rules/quality_settings.rego
index 1fc587159ee7..0b2f6b149e79 100644
--- a/cvat/apps/quality_control/rules/quality_settings.rego
+++ b/cvat/apps/quality_control/rules/quality_settings.rego
@@ -10,7 +10,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>,
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         },
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/apps/quality_control/serializers.py b/cvat/apps/quality_control/serializers.py
index fe6b372d9cb4..6164abc12200 100644
--- a/cvat/apps/quality_control/serializers.py
+++ b/cvat/apps/quality_control/serializers.py
@@ -81,6 +81,7 @@ class Meta:
             "max_validations_per_job",
             "iou_threshold",
             "oks_sigma",
+            "point_size_base",
             "line_thickness",
             "low_overlap_threshold",
             "compare_line_orientation",
@@ -91,6 +92,7 @@ class Meta:
             "object_visibility_threshold",
             "panoptic_comparison",
             "compare_attributes",
+            "match_empty_frames",
         )
         read_only_fields = (
             "id",
@@ -98,6 +100,7 @@ class Meta:
         )
 
         extra_kwargs = {k: {"required": False} for k in fields}
+        extra_kwargs.setdefault("match_empty_frames", {}).setdefault("default", False)
 
         for field_name, help_text in {
             "target_metric": "The primary metric used for quality estimation",
@@ -115,10 +118,26 @@ class Meta:
             """,
             "oks_sigma": """
                 Like IoU threshold, but for points.
-                The percent of the bbox area, used as the radius of the circle around the GT point,
-                where the checked point is expected to be.
+                The percent of the bbox side, used as the radius of the circle around the GT point,
+                where the checked point is expected to be. For boxes with different width and
+                height, the "side" is computed as a geometric mean of the width and height.
                 Read more: https://cocodataset.org/#keypoints-eval
             """,
+            "point_size_base": """
+                When comparing point annotations (including both separate points and point groups),
+                the OKS sigma parameter defines matching area for each GT point based to the
+                object size. The point size base parameter allows to configure how to determine
+                the object size.
+                If {image_size}, the image size is used. Useful if each point
+                annotation represents a separate object or boxes grouped with points do not
+                represent object boundaries.
+                If {group_bbox_size}, the object size is based on
+                the point group bbox size. Useful if each point group represents an object
+                or there is a bbox grouped with points, representing the object size.
+            """.format(
+                image_size=models.PointSizeBase.IMAGE_SIZE,
+                group_bbox_size=models.PointSizeBase.GROUP_BBOX_SIZE,
+            ),
             "line_thickness": """
                 Thickness of polylines, relatively to the (image area) ^ 0.5.
                 The distance to the boundary around the GT line,
@@ -147,6 +166,12 @@ class Meta:
                 Use only the visible part of the masks and polygons in comparisons
             """,
             "compare_attributes": "Enables or disables annotation attribute comparison",
+            "match_empty_frames": """
+                Count empty frames as matching. This affects target metrics like accuracy in cases
+                there are no annotations. If disabled, frames without annotations
+                are counted as not matching (accuracy is 0). If enabled, accuracy will be 1 instead.
+                This will also add virtual annotations to empty frames in the comparison results.
+            """,
         }.items():
             extra_kwargs.setdefault(field_name, {}).setdefault(
                 "help_text", textwrap.dedent(help_text.lstrip("\n"))
diff --git a/cvat/apps/webhooks/rules/webhooks.rego b/cvat/apps/webhooks/rules/webhooks.rego
index a74a88c6a965..85d577a21ee7 100644
--- a/cvat/apps/webhooks/rules/webhooks.rego
+++ b/cvat/apps/webhooks/rules/webhooks.rego
@@ -11,7 +11,7 @@ import data.organizations
 #     "auth": {
 #         "user": {
 #             "id": <num>
-#             "privilege": <"admin"|"business"|"user"|"worker"> or null
+#             "privilege": <"admin"|"user"|"worker"> or null
 #         }
 #         "organization": {
 #             "id": <num>,
diff --git a/cvat/asgi.py b/cvat/asgi.py
index 44ddd0d87131..2fbe40a8d4c6 100644
--- a/cvat/asgi.py
+++ b/cvat/asgi.py
@@ -24,6 +24,7 @@
 
 
 if debug.is_debugging_enabled():
+
     class DebuggerApp(ASGIHandler):
         """
         Support for VS code debugger
diff --git a/cvat/rq_patching.py b/cvat/rq_patching.py
index cd8c1ac74225..a12bcaaaedd3 100644
--- a/cvat/rq_patching.py
+++ b/cvat/rq_patching.py
@@ -32,18 +32,25 @@ def custom_started_job_registry_cleanup(self, timestamp: Optional[float] = None)
     job_ids = self.get_expired_job_ids(score)
 
     if job_ids:
-        failed_job_registry = rq.registry.FailedJobRegistry(self.name, self.connection, serializer=self.serializer)
+        failed_job_registry = rq.registry.FailedJobRegistry(
+            self.name, self.connection, serializer=self.serializer
+        )
         queue = self.get_queue()
 
         with self.connection.pipeline() as pipeline:
             for job_id in job_ids:
                 try:
-                    job = self.job_class.fetch(job_id, connection=self.connection, serializer=self.serializer)
+                    job = self.job_class.fetch(
+                        job_id, connection=self.connection, serializer=self.serializer
+                    )
                 except NoSuchJobError:
                     continue
 
                 job.execute_failure_callback(
-                    self.death_penalty_class, AbandonedJobError, AbandonedJobError(), traceback.extract_stack()
+                    self.death_penalty_class,
+                    AbandonedJobError,
+                    AbandonedJobError(),
+                    traceback.extract_stack(),
                 )
 
                 retry = job.retries_left and job.retries_left > 0
@@ -54,8 +61,8 @@ def custom_started_job_registry_cleanup(self, timestamp: Optional[float] = None)
                 else:
                     exc_string = f"due to {AbandonedJobError.__name__}"
                     rq.registry.logger.warning(
-                        f'{self.__class__.__name__} cleanup: Moving job to {rq.registry.FailedJobRegistry.__name__} '
-                        f'({exc_string})'
+                        f"{self.__class__.__name__} cleanup: Moving job to {rq.registry.FailedJobRegistry.__name__} "
+                        f"({exc_string})"
                     )
                     job.set_status(JobStatus.FAILED)
                     job._exc_info = f"Moved to {rq.registry.FailedJobRegistry.__name__}, {exc_string}, at {datetime.now()}"
@@ -69,7 +76,8 @@ def custom_started_job_registry_cleanup(self, timestamp: Optional[float] = None)
 
     return job_ids
 
+
 def update_started_job_registry_cleanup() -> None:
     # don't forget to check if the issue https://github.com/rq/rq/issues/2006 has been resolved in upstream
-    assert VERSION == '1.16.0'
+    assert VERSION == "1.16.0"
     rq.registry.StartedJobRegistry.cleanup = custom_started_job_registry_cleanup
diff --git a/cvat/rqworker.py b/cvat/rqworker.py
index d368a1ef2629..8a3e187b74b0 100644
--- a/cvat/rqworker.py
+++ b/cvat/rqworker.py
@@ -42,12 +42,14 @@ def execute_job(self, *args, **kwargs):
         # errors during debugging
         # https://stackoverflow.com/questions/8242837/django-multiprocessing-and-database-connections/10684672#10684672
         from django import db
+
         db.connections.close_all()
 
         return self.perform_job(*args, **kwargs)
 
 
 if debug.is_debugging_enabled():
+
     class RemoteDebugWorker(SimpleWorker):
         """
         Support for VS code debugger
@@ -68,6 +70,7 @@ def execute_job(self, *args, **kwargs):
 
 if os.environ.get("COVERAGE_PROCESS_START"):
     import coverage
+
     default_exit = os._exit
 
     def coverage_exit(*args, **kwargs):
diff --git a/cvat/schema.yml b/cvat/schema.yml
index 3af7944889b4..1938cabc5071 100644
--- a/cvat/schema.yml
+++ b/cvat/schema.yml
@@ -1,7 +1,7 @@
 openapi: 3.0.3
 info:
   title: CVAT REST API
-  version: 2.21.3
+  version: 2.22.0
   description: REST API for Computer Vision Annotation Tool (CVAT)
   termsOfService: https://www.google.com/policies/terms/
   contact:
@@ -9658,9 +9658,28 @@ components:
           format: double
           description: |
             Like IoU threshold, but for points.
-            The percent of the bbox area, used as the radius of the circle around the GT point,
-            where the checked point is expected to be.
+            The percent of the bbox side, used as the radius of the circle around the GT point,
+            where the checked point is expected to be. For boxes with different width and
+            height, the "side" is computed as a geometric mean of the width and height.
             Read more: https://cocodataset.org/#keypoints-eval
+        point_size_base:
+          allOf:
+          - $ref: '#/components/schemas/PointSizeBaseEnum'
+          description: |-
+            When comparing point annotations (including both separate points and point groups),
+            the OKS sigma parameter defines matching area for each GT point based to the
+            object size. The point size base parameter allows to configure how to determine
+            the object size.
+            If image_size, the image size is used. Useful if each point
+            annotation represents a separate object or boxes grouped with points do not
+            represent object boundaries.
+            If group_bbox_size, the object size is based on
+            the point group bbox size. Useful if each point group represents an object
+            or there is a bbox grouped with points, representing the object size.
+
+
+            * `image_size` - IMAGE_SIZE
+            * `group_bbox_size` - GROUP_BBOX_SIZE
         line_thickness:
           type: number
           format: double
@@ -9710,6 +9729,14 @@ components:
         compare_attributes:
           type: boolean
           description: Enables or disables annotation attribute comparison
+        match_empty_frames:
+          type: boolean
+          default: false
+          description: |
+            Count empty frames as matching. This affects target metrics like accuracy in cases
+            there are no annotations. If disabled, frames without annotations
+            are counted as not matching (accuracy is 0). If enabled, accuracy will be 1 instead.
+            This will also add virtual annotations to empty frames in the comparison results.
     PatchedTaskValidationLayoutWriteRequest:
       type: object
       properties:
@@ -9854,6 +9881,14 @@ components:
       - GIT_INTEGRATION
       - MODELS
       - PREDICT
+    PointSizeBaseEnum:
+      enum:
+      - image_size
+      - group_bbox_size
+      type: string
+      description: |-
+        * `image_size` - IMAGE_SIZE
+        * `group_bbox_size` - GROUP_BBOX_SIZE
     ProjectFileRequest:
       type: object
       properties:
@@ -10138,9 +10173,28 @@ components:
           format: double
           description: |
             Like IoU threshold, but for points.
-            The percent of the bbox area, used as the radius of the circle around the GT point,
-            where the checked point is expected to be.
+            The percent of the bbox side, used as the radius of the circle around the GT point,
+            where the checked point is expected to be. For boxes with different width and
+            height, the "side" is computed as a geometric mean of the width and height.
             Read more: https://cocodataset.org/#keypoints-eval
+        point_size_base:
+          allOf:
+          - $ref: '#/components/schemas/PointSizeBaseEnum'
+          description: |-
+            When comparing point annotations (including both separate points and point groups),
+            the OKS sigma parameter defines matching area for each GT point based to the
+            object size. The point size base parameter allows to configure how to determine
+            the object size.
+            If image_size, the image size is used. Useful if each point
+            annotation represents a separate object or boxes grouped with points do not
+            represent object boundaries.
+            If group_bbox_size, the object size is based on
+            the point group bbox size. Useful if each point group represents an object
+            or there is a bbox grouped with points, representing the object size.
+
+
+            * `image_size` - IMAGE_SIZE
+            * `group_bbox_size` - GROUP_BBOX_SIZE
         line_thickness:
           type: number
           format: double
@@ -10190,6 +10244,14 @@ components:
         compare_attributes:
           type: boolean
           description: Enables or disables annotation attribute comparison
+        match_empty_frames:
+          type: boolean
+          default: false
+          description: |
+            Count empty frames as matching. This affects target metrics like accuracy in cases
+            there are no annotations. If disabled, frames without annotations
+            are counted as not matching (accuracy is 0). If enabled, accuracy will be 1 instead.
+            This will also add virtual annotations to empty frames in the comparison results.
     RegisterSerializerEx:
       type: object
       properties:
diff --git a/cvat/settings/base.py b/cvat/settings/base.py
index f439000f9d21..404628fa555e 100644
--- a/cvat/settings/base.py
+++ b/cvat/settings/base.py
@@ -236,7 +236,7 @@ def generate_secret_key():
 
 IAM_ADMIN_ROLE = 'admin'
 # Index in the list below corresponds to the priority (0 has highest priority)
-IAM_ROLES = [IAM_ADMIN_ROLE, 'business', 'user', 'worker']
+IAM_ROLES = [IAM_ADMIN_ROLE, 'user', 'worker']
 IAM_OPA_HOST = 'http://opa:8181'
 IAM_OPA_DATA_URL = f'{IAM_OPA_HOST}/v1/data'
 LOGIN_URL = 'rest_login'
@@ -532,12 +532,6 @@ class CVAT_QUEUES(Enum):
 DATA_UPLOAD_MAX_NUMBER_FIELDS = None   # this django check disabled
 DATA_UPLOAD_MAX_NUMBER_FILES = None
 
-RESTRICTIONS = {
-    # allow access to analytics component to users with business role
-    # otherwise, only the administrator has access
-    'analytics_visibility': True,
-}
-
 redis_ondisk_host = os.getenv('CVAT_REDIS_ONDISK_HOST', 'localhost')
 # The default port is not Redis's default port (6379).
 # This is so that a developer can run both in-mem Redis and on-disk Kvrocks on their machine
diff --git a/cvat/urls.py b/cvat/urls.py
index 144ed619f766..08257a14b811 100644
--- a/cvat/urls.py
+++ b/cvat/urls.py
@@ -23,31 +23,31 @@
 from django.urls import path, include
 
 urlpatterns = [
-    path('admin/', admin.site.urls),
-    path('', include('cvat.apps.engine.urls')),
-    path('django-rq/', include('django_rq.urls')),
+    path("admin/", admin.site.urls),
+    path("", include("cvat.apps.engine.urls")),
+    path("django-rq/", include("django_rq.urls")),
 ]
 
-if apps.is_installed('cvat.apps.log_viewer'):
-    urlpatterns.append(path('', include('cvat.apps.log_viewer.urls')))
+if apps.is_installed("cvat.apps.log_viewer"):
+    urlpatterns.append(path("", include("cvat.apps.log_viewer.urls")))
 
-if apps.is_installed('cvat.apps.events'):
-    urlpatterns.append(path('api/', include('cvat.apps.events.urls')))
+if apps.is_installed("cvat.apps.events"):
+    urlpatterns.append(path("api/", include("cvat.apps.events.urls")))
 
-if apps.is_installed('cvat.apps.lambda_manager'):
-    urlpatterns.append(path('', include('cvat.apps.lambda_manager.urls')))
+if apps.is_installed("cvat.apps.lambda_manager"):
+    urlpatterns.append(path("", include("cvat.apps.lambda_manager.urls")))
 
-if apps.is_installed('cvat.apps.webhooks'):
-    urlpatterns.append(path('api/', include('cvat.apps.webhooks.urls')))
+if apps.is_installed("cvat.apps.webhooks"):
+    urlpatterns.append(path("api/", include("cvat.apps.webhooks.urls")))
 
-if apps.is_installed('cvat.apps.quality_control'):
-    urlpatterns.append(path('api/', include('cvat.apps.quality_control.urls')))
+if apps.is_installed("cvat.apps.quality_control"):
+    urlpatterns.append(path("api/", include("cvat.apps.quality_control.urls")))
 
-if apps.is_installed('silk'):
-    urlpatterns.append(path('profiler/', include('silk.urls')))
+if apps.is_installed("silk"):
+    urlpatterns.append(path("profiler/", include("silk.urls")))
 
-if apps.is_installed('health_check'):
-    urlpatterns.append(path('api/server/health/', include('health_check.urls')))
+if apps.is_installed("health_check"):
+    urlpatterns.append(path("api/server/health/", include("health_check.urls")))
 
-if apps.is_installed('cvat.apps.analytics_report'):
-    urlpatterns.append(path('api/', include('cvat.apps.analytics_report.urls')))
+if apps.is_installed("cvat.apps.analytics_report"):
+    urlpatterns.append(path("api/", include("cvat.apps.analytics_report.urls")))
diff --git a/cvat/utils/background_jobs.py b/cvat/utils/background_jobs.py
index caf2e859a530..72c93eaeaf8e 100644
--- a/cvat/utils/background_jobs.py
+++ b/cvat/utils/background_jobs.py
@@ -7,12 +7,9 @@
 
 import django_rq
 
+
 def schedule_job_with_throttling(
-    queue_name: str,
-    job_id_base: str,
-    scheduled_time: datetime,
-    func: Callable,
-    **func_kwargs
+    queue_name: str, job_id_base: str, scheduled_time: datetime, func: Callable, **func_kwargs
 ) -> None:
     """
     This function schedules an RQ job to run at `scheduled_time`,
diff --git a/cvat/utils/http.py b/cvat/utils/http.py
index b2ed89a5d555..2cb1b7498b32 100644
--- a/cvat/utils/http.py
+++ b/cvat/utils/http.py
@@ -19,11 +19,12 @@
 
 if settings.SMOKESCREEN_ENABLED:
     PROXIES_FOR_UNTRUSTED_URLS = {
-        'http': 'http://localhost:4750',
-        'https': 'http://localhost:4750',
+        "http": "http://localhost:4750",
+        "https": "http://localhost:4750",
     }
 
+
 def make_requests_session() -> requests.Session:
     session = requests.Session()
-    session.headers['User-Agent'] = _CVAT_USER_AGENT
+    session.headers["User-Agent"] = _CVAT_USER_AGENT
     return session
diff --git a/cvat/utils/remote_debugger.py b/cvat/utils/remote_debugger.py
index b4d01baf3c31..bc6ef40ae0e2 100644
--- a/cvat/utils/remote_debugger.py
+++ b/cvat/utils/remote_debugger.py
@@ -6,7 +6,8 @@
 
 
 def is_debugging_enabled() -> bool:
-    return os.environ.get('CVAT_DEBUG_ENABLED') == 'yes'
+    return os.environ.get("CVAT_DEBUG_ENABLED") == "yes"
+
 
 if is_debugging_enabled():
     import debugpy
@@ -21,8 +22,8 @@ class RemoteDebugger:
         Read more: https://modwsgi.readthedocs.io/en/develop/user-guides/debugging-techniques.html
         """
 
-        ENV_VAR_PORT = 'CVAT_DEBUG_PORT'
-        ENV_VAR_WAIT = 'CVAT_DEBUG_WAIT'
+        ENV_VAR_PORT = "CVAT_DEBUG_PORT"
+        ENV_VAR_WAIT = "CVAT_DEBUG_WAIT"
         __debugger_initialized = False
 
         @classmethod
@@ -35,7 +36,7 @@ def _singleton_init(cls):
 
                 # The only intended use is in Docker.
                 # Using 127.0.0.1 will not allow host connections
-                addr = ('0.0.0.0', port)  # nosec - B104:hardcoded_bind_all_interfaces
+                addr = ("0.0.0.0", port)  # nosec - B104:hardcoded_bind_all_interfaces
 
                 # Debugpy is a singleton
                 # We put it in the main thread of the process and then report new threads
@@ -45,7 +46,7 @@ def _singleton_init(cls):
                 # Feel free to enable if needed.
                 debugpy.configure({"subProcess": False})
 
-                if os.environ.get(cls.ENV_VAR_WAIT) == 'yes':
+                if os.environ.get(cls.ENV_VAR_WAIT) == "yes":
                     debugpy.wait_for_client()
             except Exception as ex:
                 raise Exception("failed to set debugger") from ex
diff --git a/cvat/utils/version.py b/cvat/utils/version.py
index ecc79eea7051..8b1b53a10384 100644
--- a/cvat/utils/version.py
+++ b/cvat/utils/version.py
@@ -11,6 +11,7 @@
 import os
 import subprocess
 
+
 def get_version(version):
     """Return a PEP 440-compliant version number from VERSION."""
     # Now build the two parts of the version number:
@@ -20,21 +21,23 @@ def get_version(version):
 
     main = get_main_version(version)
 
-    sub = ''
-    if version[3] == 'alpha' and version[4] == 0:
+    sub = ""
+    if version[3] == "alpha" and version[4] == 0:
         git_changeset = get_git_changeset()
         if git_changeset:
-            sub = '.dev%s' % git_changeset
+            sub = ".dev%s" % git_changeset
 
-    elif version[3] != 'final':
-        mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
+    elif version[3] != "final":
+        mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
         sub = mapping[version[3]] + str(version[4])
 
     return main + sub
 
+
 def get_main_version(version):
     """Return main version (X.Y.Z) from VERSION."""
-    return '.'.join(str(x) for x in version[:3])
+    return ".".join(str(x) for x in version[:3])
+
 
 def get_git_changeset():
     """Return a numeric identifier of the latest git changeset.
@@ -44,14 +47,16 @@ def get_git_changeset():
     so it's sufficient for generating the development version numbers.
     """
     repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-    git_log = subprocess.Popen( # nosec: B603, B607
-        ['git', 'log', '--pretty=format:%ct', '--quiet', '-1', 'HEAD'],
-        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
-        cwd=repo_dir, universal_newlines=True,
+    git_log = subprocess.Popen(  # nosec: B603, B607
+        ["git", "log", "--pretty=format:%ct", "--quiet", "-1", "HEAD"],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        cwd=repo_dir,
+        universal_newlines=True,
     )
     timestamp = git_log.communicate()[0]
     try:
         timestamp = datetime.datetime.fromtimestamp(int(timestamp), tz=datetime.timezone.utc)
     except ValueError:
         return None
-    return timestamp.strftime('%Y%m%d%H%M%S')
+    return timestamp.strftime("%Y%m%d%H%M%S")
diff --git a/dev/check_changelog_fragments.py b/dev/check_changelog_fragments.py
index d417bcd669f5..e837842efaf0 100755
--- a/dev/check_changelog_fragments.py
+++ b/dev/check_changelog_fragments.py
@@ -6,17 +6,18 @@
 
 REPO_ROOT = Path(__file__).resolve().parents[1]
 
+
 def main():
     scriv_config = configparser.ConfigParser()
-    scriv_config.read(REPO_ROOT / 'changelog.d/scriv.ini')
+    scriv_config.read(REPO_ROOT / "changelog.d/scriv.ini")
 
-    scriv_section = scriv_config['scriv']
-    assert scriv_section['format'] == 'md'
+    scriv_section = scriv_config["scriv"]
+    assert scriv_section["format"] == "md"
 
-    md_header_level = int(scriv_section['md_header_level'])
-    md_header_prefix = '#' * md_header_level + '# '
+    md_header_level = int(scriv_section["md_header_level"])
+    md_header_prefix = "#" * md_header_level + "# "
 
-    categories = {s.strip() for s in scriv_section['categories'].split(',')}
+    categories = {s.strip() for s in scriv_section["categories"].split(",")}
 
     success = True
 
@@ -25,12 +26,12 @@ def complain(message):
         success = False
         print(f"{fragment_path.relative_to(REPO_ROOT)}:{line_index+1}: {message}", file=sys.stderr)
 
-    for fragment_path in REPO_ROOT.glob('changelog.d/*.md'):
+    for fragment_path in REPO_ROOT.glob("changelog.d/*.md"):
         with open(fragment_path) as fragment_file:
             for line_index, line in enumerate(fragment_file):
                 if not line.startswith(md_header_prefix):
                     # The first line should be a header, and all headers should be of appropriate level.
-                    if line_index == 0 or line.startswith('#'):
+                    if line_index == 0 or line.startswith("#"):
                         complain(f"line should start with {md_header_prefix!r}")
                     continue
 
@@ -40,4 +41,5 @@ def complain(message):
 
     sys.exit(0 if success else 1)
 
+
 main()
diff --git a/dev/update_version.py b/dev/update_version.py
index 6cdaf313f968..ed8d08a40f42 100755
--- a/dev/update_version.py
+++ b/dev/update_version.py
@@ -9,40 +9,43 @@
 from typing import Callable, Match, Pattern
 
 
-SUCCESS_CHAR = '\u2714'
-FAIL_CHAR = '\u2716'
+SUCCESS_CHAR = "\u2714"
+FAIL_CHAR = "\u2716"
 
-CVAT_VERSION_PATTERN = re.compile(r'VERSION\s*=\s*\((\d+),\s*(\d*),\s*(\d+),\s*[\',\"](\w+)[\',\"],\s*(\d+)\)')
+CVAT_VERSION_PATTERN = re.compile(
+    r"VERSION\s*=\s*\((\d+),\s*(\d*),\s*(\d+),\s*[\',\"](\w+)[\',\"],\s*(\d+)\)"
+)
 
 REPO_ROOT_DIR = Path(__file__).resolve().parents[1]
 
-CVAT_INIT_PY_REL_PATH = 'cvat/__init__.py'
+CVAT_INIT_PY_REL_PATH = "cvat/__init__.py"
 CVAT_INIT_PY_PATH = REPO_ROOT_DIR / CVAT_INIT_PY_REL_PATH
 
+
 @dataclass()
 class Version:
     major: int = 0
     minor: int = 0
     patch: int = 0
-    prerelease: str = ''
+    prerelease: str = ""
     prerelease_number: int = 0
 
     def __str__(self) -> str:
-        return f'{self.major}.{self.minor}.{self.patch}-{self.prerelease}.{self.prerelease_number}'
+        return f"{self.major}.{self.minor}.{self.patch}-{self.prerelease}.{self.prerelease_number}"
 
     def cvat_repr(self):
-        return f"({self.major}, {self.minor}, {self.patch}, '{self.prerelease}', {self.prerelease_number})"
+        return f'({self.major}, {self.minor}, {self.patch}, "{self.prerelease}", {self.prerelease_number})'
 
     def compose_repr(self):
-        if self.prerelease != 'final':
-            return 'dev'
-        return f'v{self.major}.{self.minor}.{self.patch}'
+        if self.prerelease != "final":
+            return "dev"
+        return f"v{self.major}.{self.minor}.{self.patch}"
 
     def increment_prerelease_number(self) -> None:
         self.prerelease_number += 1
 
     def increment_prerelease(self) -> None:
-        flow = ('alpha', 'beta', 'rc', 'final')
+        flow = ("alpha", "beta", "rc", "final")
         idx = flow.index(self.prerelease)
         if idx == len(flow) - 1:
             raise ValueError(f"Cannot increment current '{self.prerelease}' prerelease version")
@@ -51,9 +54,9 @@ def increment_prerelease(self) -> None:
         self._set_default_prerelease_number()
 
     def set_prerelease(self, value: str) -> None:
-        values = ('alpha', 'beta', 'rc', 'final')
+        values = ("alpha", "beta", "rc", "final")
         if value not in values:
-            raise ValueError(f'{value} is a wrong, must be one of {values}')
+            raise ValueError(f"{value} is a wrong, must be one of {values}")
 
         self.prerelease = value
         self._set_default_prerelease_number()
@@ -71,15 +74,15 @@ def increment_major(self) -> None:
         self._set_default_minor()
 
     def set(self, v: str) -> None:
-        self.major, self.minor, self.patch = map(int, v.split('.'))
-        self.prerelease = 'final'
+        self.major, self.minor, self.patch = map(int, v.split("."))
+        self.prerelease = "final"
         self.prerelease_number = 0
 
     def _set_default_prerelease_number(self) -> None:
         self.prerelease_number = 0
 
     def _set_default_prerelease(self) -> None:
-        self.prerelease = 'alpha'
+        self.prerelease = "alpha"
         self._set_default_prerelease_number()
 
     def _set_default_patch(self) -> None:
@@ -90,6 +93,7 @@ def _set_default_minor(self) -> None:
         self.minor = 0
         self._set_default_patch()
 
+
 @dataclass(frozen=True)
 class ReplacementRule:
     rel_path: str
@@ -101,89 +105,113 @@ def apply(self, new_version: Version, *, verify_only: bool) -> bool:
         text = path.read_text()
 
         new_text, num_replacements = self.pattern.subn(
-            functools.partial(self.replacement, new_version), text)
+            functools.partial(self.replacement, new_version), text
+        )
 
         if not num_replacements:
-            print(f'{FAIL_CHAR} {self.rel_path}: failed to match version pattern.')
+            print(f"{FAIL_CHAR} {self.rel_path}: failed to match version pattern.")
             return False
 
         if text == new_text:
             if verify_only:
-                print(f'{SUCCESS_CHAR} {self.rel_path}: verified.')
+                print(f"{SUCCESS_CHAR} {self.rel_path}: verified.")
             else:
-                print(f'{SUCCESS_CHAR} {self.rel_path}: no need to update.')
+                print(f"{SUCCESS_CHAR} {self.rel_path}: no need to update.")
         else:
             if verify_only:
-                print(f'{FAIL_CHAR} {self.rel_path}: verification failed.')
+                print(f"{FAIL_CHAR} {self.rel_path}: verification failed.")
                 return False
             else:
                 path.write_text(new_text)
-                print(f'{SUCCESS_CHAR} {self.rel_path}: updated.')
+                print(f"{SUCCESS_CHAR} {self.rel_path}: updated.")
 
         return True
 
-REPLACEMENT_RULES = [
-    ReplacementRule(CVAT_INIT_PY_REL_PATH, CVAT_VERSION_PATTERN,
-        lambda v, m: f'VERSION = {v.cvat_repr()}'),
-
-    ReplacementRule('docker-compose.yml',
-        re.compile(r'(\$\{CVAT_VERSION:-)([\w.]+)(\})'),
-        lambda v, m: m[1] + v.compose_repr() + m[3]),
-
-    ReplacementRule('helm-chart/values.yaml',
-        re.compile(r'(^    image: cvat/(?:ui|server)\n    tag: )([\w.]+)', re.M),
-        lambda v, m: m[1] + v.compose_repr()),
 
-    ReplacementRule('cvat-sdk/gen/generate.sh',
+REPLACEMENT_RULES = [
+    ReplacementRule(
+        CVAT_INIT_PY_REL_PATH, CVAT_VERSION_PATTERN, lambda v, m: f"VERSION = {v.cvat_repr()}"
+    ),
+    ReplacementRule(
+        "docker-compose.yml",
+        re.compile(r"(\$\{CVAT_VERSION:-)([\w.]+)(\})"),
+        lambda v, m: m[1] + v.compose_repr() + m[3],
+    ),
+    ReplacementRule(
+        "helm-chart/values.yaml",
+        re.compile(r"(^    image: cvat/(?:ui|server)\n    tag: )([\w.]+)", re.M),
+        lambda v, m: m[1] + v.compose_repr(),
+    ),
+    ReplacementRule(
+        "cvat-sdk/gen/generate.sh",
         re.compile(r'^VERSION="[\d.]+"$', re.M),
-        lambda v, m: f'VERSION="{v.major}.{v.minor}.{v.patch}"'),
-
-    ReplacementRule('cvat/schema.yml',
+        lambda v, m: f'VERSION="{v.major}.{v.minor}.{v.patch}"',
+    ),
+    ReplacementRule(
+        "cvat/schema.yml",
         re.compile(r"^  version: [\d.]+$", re.M),
-        lambda v, m:  f'  version: {v.major}.{v.minor}.{v.patch}'),
-
-    ReplacementRule('cvat-cli/src/cvat_cli/version.py',
+        lambda v, m: f"  version: {v.major}.{v.minor}.{v.patch}",
+    ),
+    ReplacementRule(
+        "cvat-cli/src/cvat_cli/version.py",
         re.compile(r'^VERSION = "[\d.]+"$', re.M),
-        lambda v, m: f'VERSION = "{v.major}.{v.minor}.{v.patch}"'),
-
-    ReplacementRule('cvat-cli/requirements/base.txt',
-        re.compile(r'^cvat-sdk~=[\d.]+$', re.M),
-        lambda v, m: f'cvat-sdk~={v.major}.{v.minor}.{v.patch}'),
+        lambda v, m: f'VERSION = "{v.major}.{v.minor}.{v.patch}"',
+    ),
+    ReplacementRule(
+        "cvat-cli/requirements/base.txt",
+        re.compile(r"^cvat-sdk~=[\d.]+$", re.M),
+        lambda v, m: f"cvat-sdk~={v.major}.{v.minor}.{v.patch}",
+    ),
 ]
 
+
 def get_current_version() -> Version:
     version_text = CVAT_INIT_PY_PATH.read_text()
 
     match = re.search(CVAT_VERSION_PATTERN, version_text)
     if not match:
-        raise RuntimeError(f'Failed to find version in {CVAT_INIT_PY_PATH}')
+        raise RuntimeError(f"Failed to find version in {CVAT_INIT_PY_PATH}")
 
     return Version(int(match[1]), int(match[2]), int(match[3]), match[4], int(match[5]))
 
+
 def main() -> None:
-    parser = argparse.ArgumentParser(description='Bump CVAT version')
+    parser = argparse.ArgumentParser(description="Bump CVAT version")
 
     action_group = parser.add_mutually_exclusive_group(required=True)
 
-    action_group.add_argument('--major', action='store_true',
-        help='Increment the existing major version by 1')
-    action_group.add_argument('--minor', action='store_true',
-        help='Increment the existing minor version by 1')
-    action_group.add_argument('--patch', action='store_true',
-        help='Increment the existing patch version by 1')
-    action_group.add_argument('--prerelease', nargs='?', const='increment',
-        help='''Increment prerelease version alpha->beta->rc->final,
-                Also it's possible to pass value explicitly''')
-    action_group.add_argument('--prerelease_number', action='store_true',
-        help='Increment prerelease number by 1')
-
-    action_group.add_argument('--current', '--show-current',
-        action='store_true', help='Display current version')
-    action_group.add_argument('--verify-current',
-        action='store_true', help='Check that all version numbers are consistent')
-
-    action_group.add_argument('--set', metavar='X.Y.Z',
-        help='Set the version to the specified version')
+    action_group.add_argument(
+        "--major", action="store_true", help="Increment the existing major version by 1"
+    )
+    action_group.add_argument(
+        "--minor", action="store_true", help="Increment the existing minor version by 1"
+    )
+    action_group.add_argument(
+        "--patch", action="store_true", help="Increment the existing patch version by 1"
+    )
+    action_group.add_argument(
+        "--prerelease",
+        nargs="?",
+        const="increment",
+        help="""Increment prerelease version alpha->beta->rc->final,
+                Also it's possible to pass value explicitly""",
+    )
+    action_group.add_argument(
+        "--prerelease_number", action="store_true", help="Increment prerelease number by 1"
+    )
+
+    action_group.add_argument(
+        "--current", "--show-current", action="store_true", help="Display current version"
+    )
+    action_group.add_argument(
+        "--verify-current",
+        action="store_true",
+        help="Check that all version numbers are consistent",
+    )
+
+    action_group.add_argument(
+        "--set", metavar="X.Y.Z", help="Set the version to the specified version"
+    )
 
     args = parser.parse_args()
 
@@ -201,7 +229,7 @@ def main() -> None:
         version.increment_prerelease_number()
 
     elif args.prerelease:
-        if args.prerelease  == 'increment':
+        if args.prerelease == "increment":
             version.increment_prerelease()
         else:
             version.set_prerelease(args.prerelease)
@@ -222,9 +250,9 @@ def main() -> None:
         assert False, "Unreachable code"
 
     if verify_only:
-        print(f'Verifying that version is {version}...')
+        print(f"Verifying that version is {version}...")
     else:
-        print(f'Bumping version to {version}...')
+        print(f"Bumping version to {version}...")
     print()
 
     success = True
@@ -239,5 +267,6 @@ def main() -> None:
         else:
             sys.exit("\nFailed to update one or more files!")
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     main()
diff --git a/docker-compose.yml b/docker-compose.yml
index b51e38fce7d5..bec741c5536f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -79,7 +79,7 @@ services:
 
   cvat_server:
     container_name: cvat_server
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on:
       <<: *backend-deps
@@ -113,7 +113,7 @@ services:
 
   cvat_utils:
     container_name: cvat_utils
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on: *backend-deps
     environment:
@@ -130,7 +130,7 @@ services:
 
   cvat_worker_import:
     container_name: cvat_worker_import
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on: *backend-deps
     environment:
@@ -146,7 +146,7 @@ services:
 
   cvat_worker_export:
     container_name: cvat_worker_export
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on: *backend-deps
     environment:
@@ -162,7 +162,7 @@ services:
 
   cvat_worker_annotation:
     container_name: cvat_worker_annotation
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on: *backend-deps
     environment:
@@ -178,7 +178,7 @@ services:
 
   cvat_worker_webhooks:
     container_name: cvat_worker_webhooks
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on: *backend-deps
     environment:
@@ -194,7 +194,7 @@ services:
 
   cvat_worker_quality_reports:
     container_name: cvat_worker_quality_reports
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on: *backend-deps
     environment:
@@ -210,7 +210,7 @@ services:
 
   cvat_worker_analytics_reports:
     container_name: cvat_worker_analytics_reports
-    image: cvat/server:${CVAT_VERSION:-v2.21.3}
+    image: cvat/server:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on: *backend-deps
     environment:
@@ -226,7 +226,7 @@ services:
 
   cvat_ui:
     container_name: cvat_ui
-    image: cvat/ui:${CVAT_VERSION:-v2.21.3}
+    image: cvat/ui:${CVAT_VERSION:-v2.22.0}
     restart: always
     depends_on:
       - cvat_server
diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml
index d0906952f96d..b99625f1a104 100644
--- a/helm-chart/values.yaml
+++ b/helm-chart/values.yaml
@@ -129,7 +129,7 @@ cvat:
       additionalVolumeMounts: []
     replicas: 1
     image: cvat/server
-    tag: v2.21.3
+    tag: v2.22.0
     imagePullPolicy: Always
     permissionFix:
       enabled: true
@@ -153,7 +153,7 @@ cvat:
   frontend:
     replicas: 1
     image: cvat/ui
-    tag: v2.21.3
+    tag: v2.22.0
     imagePullPolicy: Always
     labels: {}
     #  test: test
diff --git a/pyproject.toml b/pyproject.toml
index 6d0772451578..528bdc579fcc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,3 +7,14 @@ skip_gitignore = true # align tool behavior with Black
 [tool.black]
 line-length = 100
 target-version = ['py39']
+extend-exclude = """
+# TODO: get rid of these
+^/cvat/apps/(
+    dataset_manager|dataset_repo|engine|events
+    |health|iam|lambda_manager|log_viewer
+    |organizations|webhooks
+)/
+| ^/cvat/settings/
+| ^/serverless/
+| ^/utils/dataset_manifest/
+"""
diff --git a/rqscheduler.py b/rqscheduler.py
index 82b7499baf89..5ae76e64a7f0 100644
--- a/rqscheduler.py
+++ b/rqscheduler.py
@@ -9,5 +9,5 @@
 
 from rq_scheduler.scripts import rqscheduler
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     rqscheduler.main()
diff --git a/site/build_docs.py b/site/build_docs.py
index 25af0b0e8f82..2eca3a941330 100755
--- a/site/build_docs.py
+++ b/site/build_docs.py
@@ -157,9 +157,7 @@ def validate_env():
         try:
             subprocess.run([hugo, "version"], capture_output=True)  # nosec
         except (subprocess.CalledProcessError, FileNotFoundError) as ex:
-            raise Exception(
-                f"Failed to run '{hugo}', please make sure it exists."
-            ) from ex
+            raise Exception(f"Failed to run '{hugo}', please make sure it exists.") from ex
 
 
 if __name__ == "__main__":
diff --git a/site/content/en/docs/administration/advanced/ldap.md b/site/content/en/docs/administration/advanced/ldap.md
index c1b6be282f26..c57824d13fa3 100644
--- a/site/content/en/docs/administration/advanced/ldap.md
+++ b/site/content/en/docs/administration/advanced/ldap.md
@@ -100,9 +100,6 @@ AUTHENTICATION_BACKENDS += ['django_auth_ldap.backend.LDAPBackend']
 AUTH_LDAP_ADMIN_GROUPS = [
     'CN=CVAT Admins,%s' % _BASE_DN,
 ]
-AUTH_LDAP_BUSINESS_GROUPS = [
-    'CN=CVAT Managers,%s' % _BASE_DN,
-]
 AUTH_LDAP_WORKER_GROUPS = [
     'CN=CVAT Workers,%s' % _BASE_DN,
 ]
@@ -112,7 +109,6 @@ AUTH_LDAP_USER_GROUPS = [
 
 DJANGO_AUTH_LDAP_GROUPS = {
     "admin": AUTH_LDAP_ADMIN_GROUPS,
-    "business": AUTH_LDAP_BUSINESS_GROUPS,
     "user": AUTH_LDAP_USER_GROUPS,
     "worker": AUTH_LDAP_WORKER_GROUPS,
 }
@@ -181,9 +177,6 @@ AUTHENTICATION_BACKENDS += ['django_auth_ldap.backend.LDAPBackend']
 AUTH_LDAP_ADMIN_GROUPS = [
     'CN=cvat_admins,CN=Groups,%s' % _BASE_DN,
 ]
-AUTH_LDAP_BUSINESS_GROUPS = [
-    'CN=cvat_managers,CN=Groups,%s' % _BASE_DN,
-]
 AUTH_LDAP_WORKER_GROUPS = [
     'CN=cvat_workers,CN=Groups,%s' % _BASE_DN,
 ]
@@ -193,7 +186,6 @@ AUTH_LDAP_USER_GROUPS = [
 
 DJANGO_AUTH_LDAP_GROUPS = {
     "admin": AUTH_LDAP_ADMIN_GROUPS,
-    "business": AUTH_LDAP_BUSINESS_GROUPS,
     "user": AUTH_LDAP_USER_GROUPS,
     "worker": AUTH_LDAP_WORKER_GROUPS,
 }
diff --git a/site/content/en/docs/administration/basics/admin-account.md b/site/content/en/docs/administration/basics/admin-account.md
index 08182f80a22b..bb72a99af89b 100644
--- a/site/content/en/docs/administration/basics/admin-account.md
+++ b/site/content/en/docs/administration/basics/admin-account.md
@@ -11,7 +11,7 @@ The user you register by default does not have full permissions on the instance,
 so you must create a superuser.
 The superuser can use [Django administration panel](http://localhost:8080/admin)
 to assign groups (roles) to other users.
-<br>Available roles are: user (default), admin, business, worker.
+<br>Available roles are: user (default), admin, worker.
 
 ### Prerequisites
 
diff --git a/site/content/en/docs/manual/advanced/annotation-with-brush-tool.md b/site/content/en/docs/manual/advanced/annotation-with-brush-tool.md
index 8f672c1b892a..bc135b40273d 100644
--- a/site/content/en/docs/manual/advanced/annotation-with-brush-tool.md
+++ b/site/content/en/docs/manual/advanced/annotation-with-brush-tool.md
@@ -42,6 +42,7 @@ It has the following elements:
 | ![Brush size](/images/brushing_tools_brush_size.png)            | **Brush size** in pixels. <br>**Note:** Visible only when **Brush** or **Eraser** are selected.                                                                                                          |
 | ![Brush shape](/images/brushing_tools_brush_shape.png)          | **Brush shape** with two options: circle and square. <br>**Note:** Visible only when **Brush** or **Eraser** are selected.                                                                               |
 | ![Pixel remove](/images/brushing_tools_pixels.png)              | **Remove underlying pixels**. When you are drawing or editing a mask with this tool, <br>pixels on other masks that are located at the same positions as the pixels of the <br>current mask are deleted. |
+| ![Hide mask](/images/brushing_tools_hide.png)              | **Hide mask**. When drawing or editing a mask, you can enable this feature to temporarily hide the mask, allowing you to see the objects underneath more clearly. |
 | ![Label](/images/brushing_tools_label_drop.png)                 | **Label** that will be assigned to the newly created mask                                                                                                                                                |     |
 | ![Move](/images/brushing_tools_brush_move.png)                  | **Move**. Click and hold to move the menu bar to the other place on the screen                                                                                                                           |
 
diff --git a/site/content/en/images/brushing_tool_menu.png b/site/content/en/images/brushing_tool_menu.png
index f5d6726d1d17..418f58f6ba36 100644
Binary files a/site/content/en/images/brushing_tool_menu.png and b/site/content/en/images/brushing_tool_menu.png differ
diff --git a/site/content/en/images/brushing_tools_hide.png b/site/content/en/images/brushing_tools_hide.png
new file mode 100644
index 000000000000..e9d7ba5552bd
Binary files /dev/null and b/site/content/en/images/brushing_tools_hide.png differ
diff --git a/site/process_sdk_docs.py b/site/process_sdk_docs.py
index 3b1941248410..03324aea691b 100755
--- a/site/process_sdk_docs.py
+++ b/site/process_sdk_docs.py
@@ -25,9 +25,7 @@ def __init__(self, *, input_dir: str, site_root: str) -> None:
         self._site_root = site_root
 
         self._content_dir = osp.join(self._site_root, "content")
-        self._sdk_reference_dir = osp.join(
-            self._content_dir, "en/docs/api_sdk/sdk/reference"
-        )
+        self._sdk_reference_dir = osp.join(self._content_dir, "en/docs/api_sdk/sdk/reference")
         self._templates_dir = osp.join(self._site_root, "templates")
 
     @staticmethod
@@ -97,9 +95,7 @@ def _move_api_summary(self):
         apis_index_filename = osp.join(
             osp.relpath(self._sdk_reference_dir, self._content_dir), "apis/_index.md"
         )
-        apis_index_path = osp.join(
-            self._templates_dir, apis_index_filename + ".template"
-        )
+        apis_index_path = osp.join(self._templates_dir, apis_index_filename + ".template")
         with open(apis_index_path) as f:
             contents = f.read()
 
@@ -126,9 +122,7 @@ def _fix_page_links_and_references(self):
             os.rename(src_path, dst_path)
             mapping[src_filename] = dst_filename
 
-        self._reference_files = [
-            osp.join(self._sdk_reference_dir, p) for p in mapping.values()
-        ]
+        self._reference_files = [osp.join(self._sdk_reference_dir, p) for p in mapping.values()]
 
         for p in iglob(self._sdk_reference_dir + "/**/*.md", recursive=True):
             with open(p) as f:
@@ -146,9 +140,7 @@ def _fix_page_links_and_references(self):
             with open(p, "w") as f:
                 f.write(contents)
 
-    def _process_non_code_blocks(
-        self, text: str, handlers: List[Callable[[str], str]]
-    ) -> str:
+    def _process_non_code_blocks(self, text: str, handlers: List[Callable[[str], str]]) -> str:
         """
         Allows to process Markdown documents with passed callbacks. Callbacks are only
         executed outside code blocks.
diff --git a/supervisord/utils.conf b/supervisord/utils.conf
index 1271e6eef536..dc7030023c35 100644
--- a/supervisord/utils.conf
+++ b/supervisord/utils.conf
@@ -26,19 +26,10 @@ environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler"
 numprocs=1
 autorestart=true
 
-[program:rqworker-notifications]
+[program:rqworker]
 command=%(ENV_HOME)s/wait_for_deps.sh
-    python3 %(ENV_HOME)s/manage.py rqworker -v 3 notifications
+    python3 %(ENV_HOME)s/manage.py rqworker -v 3 notifications cleaning
         --worker-class cvat.rqworker.DefaultWorker
-environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler",CVAT_POSTGRES_APPLICATION_NAME="cvat:worker:notifications"
+environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler",CVAT_POSTGRES_APPLICATION_NAME="cvat:worker:notifications+cleaning"
 numprocs=%(ENV_NUMPROCS)s
 autorestart=true
-
-[program:rqworker-cleaning]
-command=%(ENV_HOME)s/wait_for_deps.sh
-    python3 %(ENV_HOME)s/manage.py rqworker -v 3 cleaning
-        --worker-class cvat.rqworker.DefaultWorker
-environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler",CVAT_POSTGRES_APPLICATION_NAME="cvat:worker:cleaning"
-numprocs=%(ENV_NUMPROCS)s
-process_name=%(program_name)s-%(process_num)d
-autorestart=true
diff --git a/utils/__init__.py b/utils/__init__.py
index d0af4b967942..6370694b5ea2 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -1,4 +1,3 @@
 # Copyright (C) 2022 Intel Corporation
 #
 # SPDX-License-Identifier: MIT
-
diff --git a/utils/dicom_converter/script.py b/utils/dicom_converter/script.py
index 23a1e7526e3a..3fe7ef0be6dd 100644
--- a/utils/dicom_converter/script.py
+++ b/utils/dicom_converter/script.py
@@ -16,10 +16,20 @@
 
 
 # Script configuration
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
-parser = argparse.ArgumentParser(description='The script is used to convert some kinds of DICOM (.dcm) files to regular image files (.png)')
-parser.add_argument('input', type=str, help='A root directory with medical data files in DICOM format. The script finds all these files based on their extension')
-parser.add_argument('output', type=str, help='Where to save converted files. The script repeats internal directories structure of the input root directory')
+logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
+parser = argparse.ArgumentParser(
+    description="The script is used to convert some kinds of DICOM (.dcm) files to regular image files (.png)"
+)
+parser.add_argument(
+    "input",
+    type=str,
+    help="A root directory with medical data files in DICOM format. The script finds all these files based on their extension",
+)
+parser.add_argument(
+    "output",
+    type=str,
+    help="Where to save converted files. The script repeats internal directories structure of the input root directory",
+)
 args = parser.parse_args()
 
 
@@ -32,11 +42,11 @@ def __init__(self, filename):
             self._max_value = ds.pixel_array.max()
             self._depth = ds.BitsStored
 
-            logging.debug('File: {}'.format(filename))
-            logging.debug('Photometric interpretation: {}'.format(self._photometric_interpretation))
-            logging.debug('Min value: {}'.format(self._min_value))
-            logging.debug('Max value: {}'.format(self._max_value))
-            logging.debug('Depth: {}'.format(self._depth))
+            logging.debug("File: {}".format(filename))
+            logging.debug("Photometric interpretation: {}".format(self._photometric_interpretation))
+            logging.debug("Min value: {}".format(self._min_value))
+            logging.debug("Max value: {}".format(self._max_value))
+            logging.debug("Depth: {}".format(self._depth))
 
             try:
                 self._length = ds["NumberOfFrames"].value
@@ -53,38 +63,40 @@ def __iter__(self):
         for pixel_array in self._pixel_array:
             # Normalization to an output range 0..255, 0..65535
             pixel_array = pixel_array - self._min_value
-            pixel_array = pixel_array.astype(int) * (2 ** self._depth - 1)
+            pixel_array = pixel_array.astype(int) * (2**self._depth - 1)
             pixel_array = pixel_array // (self._max_value - self._min_value)
 
             # In some cases we need to convert colors additionally
-            if 'YBR' in self._photometric_interpretation:
-                 pixel_array = convert_color_space(pixel_array, self._photometric_interpretation, 'RGB')
+            if "YBR" in self._photometric_interpretation:
+                pixel_array = convert_color_space(
+                    pixel_array, self._photometric_interpretation, "RGB"
+                )
 
             if self._depth == 8:
                 image = Image.fromarray(pixel_array.astype(np.uint8))
             elif self._depth == 16:
                 image = Image.fromarray(pixel_array.astype(np.uint16))
             else:
-                raise Exception('Not supported depth {}'.format(self._depth))
+                raise Exception("Not supported depth {}".format(self._depth))
 
             yield image
 
 
 def main(root_dir, output_root_dir):
-    dicom_files = glob(os.path.join(root_dir, '**', '*.dcm'), recursive = True)
+    dicom_files = glob(os.path.join(root_dir, "**", "*.dcm"), recursive=True)
     if not len(dicom_files):
-        logging.info('DICOM files are not found under the specified path')
+        logging.info("DICOM files are not found under the specified path")
     else:
-        logging.info('Number of found DICOM files: ' + str(len(dicom_files)))
+        logging.info("Number of found DICOM files: " + str(len(dicom_files)))
 
     pbar = tqdm(dicom_files)
     for input_filename in pbar:
-        pbar.set_description('Conversion: ' + input_filename)
+        pbar.set_description("Conversion: " + input_filename)
         input_basename = os.path.basename(input_filename)
 
         output_subpath = os.path.relpath(os.path.dirname(input_filename), root_dir)
         output_path = os.path.join(output_root_dir, output_subpath)
-        output_basename = '{}.png'.format(os.path.splitext(input_basename)[0])
+        output_basename = "{}.png".format(os.path.splitext(input_basename)[0])
         output_filename = os.path.join(output_path, output_basename)
 
         if not os.path.exists(output_path):
@@ -98,16 +110,19 @@ def main(root_dir, output_root_dir):
                     image.save(output_filename)
                 else:
                     filename_index = str(i).zfill(len(str(length)))
-                    list_output_filename = '{}_{}.png'.format(os.path.splitext(output_filename)[0], filename_index)
+                    list_output_filename = "{}_{}.png".format(
+                        os.path.splitext(output_filename)[0], filename_index
+                    )
                     image.save(list_output_filename)
         except Exception as ex:
-            logging.error('Error while processing ' + input_filename)
+            logging.error("Error while processing " + input_filename)
             logging.error(ex)
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     input_root_path = os.path.abspath(args.input.rstrip(os.sep))
     output_root_path = os.path.abspath(args.output.rstrip(os.sep))
 
-    logging.info('From: {}'.format(input_root_path))
-    logging.info('To: {}'.format(output_root_path))
+    logging.info("From: {}".format(input_root_path))
+    logging.info("To: {}".format(output_root_path))
     main(input_root_path, output_root_path)


Test patch:
diff --git a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py
index 3350a4180efe..059a45f6df2d 100644
--- a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py
+++ b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py
@@ -141,7 +141,7 @@ def setUpTestData(cls):
     @classmethod
     def create_db_users(cls):
         (group_admin, _) = Group.objects.get_or_create(name="admin")
-        (group_user, _) = Group.objects.get_or_create(name="business")
+        (group_user, _) = Group.objects.get_or_create(name="user")
 
         user_admin = User.objects.create_superuser(username="admin", email="",
             password="admin")
diff --git a/cvat/apps/engine/rules/tests/generators/annotationguides_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/annotationguides_test.gen.rego.py
index 4cf562741677..1dbfcc1167f5 100644
--- a/cvat/apps/engine/rules/tests/generators/annotationguides_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/annotationguides_test.gen.rego.py
@@ -46,7 +46,7 @@ def read_rules(name):
     "job:assignee",
     "none",
 ]
-GROUPS = ["admin", "business", "user", "worker"]
+GROUPS = ["admin", "user", "worker"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [True, False]
 
diff --git a/cvat/apps/engine/rules/tests/generators/cloudstorages_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/cloudstorages_test.gen.rego.py
index 63460df540b2..4a4941e0fd1c 100644
--- a/cvat/apps/engine/rules/tests/generators/cloudstorages_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/cloudstorages_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["owner", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [False, True]
 
diff --git a/cvat/apps/engine/rules/tests/generators/comments_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/comments_test.gen.rego.py
index f36c8a7dfa0d..a13a1897c66a 100644
--- a/cvat/apps/engine/rules/tests/generators/comments_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/comments_test.gen.rego.py
@@ -51,7 +51,7 @@ def read_rules(name):
     "owner",
     "none",
 ]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [True, False]
 HAS_PROJ = [True, False]
diff --git a/cvat/apps/engine/rules/tests/generators/issues_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/issues_test.gen.rego.py
index 0a35d83880eb..53213eb39d2d 100644
--- a/cvat/apps/engine/rules/tests/generators/issues_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/issues_test.gen.rego.py
@@ -50,7 +50,7 @@ def read_rules(name):
     "assignee",
     "none",
 ]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [True, False]
 HAS_PROJ = [True, False]
diff --git a/cvat/apps/engine/rules/tests/generators/jobs_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/jobs_test.gen.rego.py
index ca799f953cd3..e36f8c8ec7be 100644
--- a/cvat/apps/engine/rules/tests/generators/jobs_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/jobs_test.gen.rego.py
@@ -50,7 +50,7 @@ def read_rules(name):
     "assignee",
     "none",
 ]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [True, False]
 
diff --git a/cvat/apps/engine/rules/tests/generators/projects_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/projects_test.gen.rego.py
index 6657f21d2994..d4a7259893fc 100644
--- a/cvat/apps/engine/rules/tests/generators/projects_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/projects_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["owner", "assignee", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [False, True]
 
diff --git a/cvat/apps/engine/rules/tests/generators/server_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/server_test.gen.rego.py
index 8e9b57a814d8..c2b4195191a4 100644
--- a/cvat/apps/engine/rules/tests/generators/server_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/server_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 
 
diff --git a/cvat/apps/engine/rules/tests/generators/tasks_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/tasks_test.gen.rego.py
index 61da5c8520de..30925fcee18b 100644
--- a/cvat/apps/engine/rules/tests/generators/tasks_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/tasks_test.gen.rego.py
@@ -43,7 +43,7 @@ def read_rules(name):
 SCOPES = list({rule["scope"] for rule in simple_rules})
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["project:owner", "project:assignee", "owner", "assignee", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [True, False]
 
diff --git a/cvat/apps/engine/rules/tests/generators/users_test.gen.rego.py b/cvat/apps/engine/rules/tests/generators/users_test.gen.rego.py
index 595cbaae4ee4..a609492868f6 100644
--- a/cvat/apps/engine/rules/tests/generators/users_test.gen.rego.py
+++ b/cvat/apps/engine/rules/tests/generators/users_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["self", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 
 
diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py
index 07e01461ab73..e6ed6b6c0303 100644
--- a/cvat/apps/engine/tests/test_rest_api.py
+++ b/cvat/apps/engine/tests/test_rest_api.py
@@ -54,7 +54,6 @@
 
 def create_db_users(cls):
     (group_admin, _) = Group.objects.get_or_create(name="admin")
-    (group_business, _) = Group.objects.get_or_create(name="business")
     (group_user, _) = Group.objects.get_or_create(name="user")
     (group_annotator, _) = Group.objects.get_or_create(name="worker")
     (group_somebody, _) = Group.objects.get_or_create(name="somebody")
@@ -63,7 +62,7 @@ def create_db_users(cls):
         password="admin")
     user_admin.groups.add(group_admin)
     user_owner = User.objects.create_user(username="user1", password="user1")
-    user_owner.groups.add(group_business)
+    user_owner.groups.add(group_user)
     user_assignee = User.objects.create_user(username="user2", password="user2")
     user_assignee.groups.add(group_annotator)
     user_annotator = User.objects.create_user(username="user3", password="user3")
diff --git a/cvat/apps/events/rules/tests/generators/events_test.gen.rego.py b/cvat/apps/events/rules/tests/generators/events_test.gen.rego.py
index da9d54d79e22..dee2d4a68963 100644
--- a/cvat/apps/events/rules/tests/generators/events_test.gen.rego.py
+++ b/cvat/apps/events/rules/tests/generators/events_test.gen.rego.py
@@ -42,7 +42,7 @@ def read_rules(name):
 SCOPES = list({rule["scope"] for rule in simple_rules})
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [True, False]
 
diff --git a/cvat/apps/lambda_manager/rules/tests/generators/lambda_test.gen.rego.py b/cvat/apps/lambda_manager/rules/tests/generators/lambda_test.gen.rego.py
index 5a669c5f49fc..94f694988a38 100644
--- a/cvat/apps/lambda_manager/rules/tests/generators/lambda_test.gen.rego.py
+++ b/cvat/apps/lambda_manager/rules/tests/generators/lambda_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = list({rule["scope"] for rule in simple_rules})
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 
 
diff --git a/cvat/apps/lambda_manager/tests/test_lambda.py b/cvat/apps/lambda_manager/tests/test_lambda.py
index 57c74cf2c529..794ef8cefabe 100644
--- a/cvat/apps/lambda_manager/tests/test_lambda.py
+++ b/cvat/apps/lambda_manager/tests/test_lambda.py
@@ -133,7 +133,7 @@ def _invoke_function(self, func, payload):
     @classmethod
     def _create_db_users(cls):
         (group_admin, _) = Group.objects.get_or_create(name="admin")
-        (group_user, _) = Group.objects.get_or_create(name="business")
+        (group_user, _) = Group.objects.get_or_create(name="user")
 
         user_admin = User.objects.create_superuser(username="admin", email="",
             password="admin")
diff --git a/cvat/apps/log_viewer/rules/tests/configs/analytics.csv b/cvat/apps/log_viewer/rules/tests/configs/analytics.csv
index a581e0716e5f..7ff4ea280475 100644
--- a/cvat/apps/log_viewer/rules/tests/configs/analytics.csv
+++ b/cvat/apps/log_viewer/rules/tests/configs/analytics.csv
@@ -1,3 +1,2 @@
 Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership,HasAnalyticsAccess
-view,Analytics,N/A,N/A,resource['visibility']=='public',GET,"/analytics",business,N/A,N/A
 view,Analytics,N/A,N/A,,GET,"/analytics",none,N/A,true
diff --git a/cvat/apps/log_viewer/rules/tests/generators/analytics_test.gen.rego.py b/cvat/apps/log_viewer/rules/tests/generators/analytics_test.gen.rego.py
index 7e40f092607c..95d566e4b93a 100644
--- a/cvat/apps/log_viewer/rules/tests/generators/analytics_test.gen.rego.py
+++ b/cvat/apps/log_viewer/rules/tests/generators/analytics_test.gen.rego.py
@@ -41,18 +41,12 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 HAS_ANALYTICS_ACCESS = [True, False]
 
 
 def RESOURCES(scope):
-    if scope == "view":
-        return [
-            {"visibility": "public"},
-            {"visibility": "private"},
-        ]
-
     return [None]
 
 
diff --git a/cvat/apps/organizations/rules/tests/generators/invitations_test.gen.rego.py b/cvat/apps/organizations/rules/tests/generators/invitations_test.gen.rego.py
index c3ba86abb75f..bf7edec50713 100644
--- a/cvat/apps/organizations/rules/tests/generators/invitations_test.gen.rego.py
+++ b/cvat/apps/organizations/rules/tests/generators/invitations_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["owner", "invitee", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [False, True]
 
diff --git a/cvat/apps/organizations/rules/tests/generators/memberships_test.gen.rego.py b/cvat/apps/organizations/rules/tests/generators/memberships_test.gen.rego.py
index b86548142da7..c74a4a7c992b 100644
--- a/cvat/apps/organizations/rules/tests/generators/memberships_test.gen.rego.py
+++ b/cvat/apps/organizations/rules/tests/generators/memberships_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["self", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [False, True]
 
diff --git a/cvat/apps/organizations/rules/tests/generators/organizations_test.gen.rego.py b/cvat/apps/organizations/rules/tests/generators/organizations_test.gen.rego.py
index a6c111bfef40..d2a8a6fb653b 100644
--- a/cvat/apps/organizations/rules/tests/generators/organizations_test.gen.rego.py
+++ b/cvat/apps/organizations/rules/tests/generators/organizations_test.gen.rego.py
@@ -41,7 +41,7 @@ def read_rules(name):
 SCOPES = {rule["scope"] for rule in simple_rules}
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["owner", "maintainer", "supervisor", "worker", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 
 
diff --git a/cvat/apps/webhooks/rules/tests/generators/webhooks_test.gen.rego.py b/cvat/apps/webhooks/rules/tests/generators/webhooks_test.gen.rego.py
index c367a42cc98b..66417f3d096d 100644
--- a/cvat/apps/webhooks/rules/tests/generators/webhooks_test.gen.rego.py
+++ b/cvat/apps/webhooks/rules/tests/generators/webhooks_test.gen.rego.py
@@ -40,7 +40,7 @@ def read_rules(name):
 SCOPES = list({rule["scope"] for rule in simple_rules})
 CONTEXTS = ["sandbox", "organization"]
 OWNERSHIPS = ["project:owner", "owner", "none"]
-GROUPS = ["admin", "business", "user", "worker", "none"]
+GROUPS = ["admin", "user", "worker", "none"]
 ORG_ROLES = ["owner", "maintainer", "supervisor", "worker", None]
 SAME_ORG = [True, False]
 
diff --git a/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js b/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js
index 6f72aaf6219f..2e80ff356b5c 100644
--- a/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js
+++ b/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js
@@ -40,7 +40,7 @@ context('Save filtered object in AAM.', () => {
     });
 
     describe(`Testing case "${caseId}"`, () => {
-        it(`Set filter label == “${labelName}”.`, () => {
+        it(`Set filter label == "${labelName}".`, () => {
             cy.addFiltersRule(0);
             cy.setFilter({
                 groupIndex: 0,
diff --git a/tests/cypress/e2e/actions_objects2/case_16_z_order_features.js b/tests/cypress/e2e/actions_objects2/case_16_z_order_features.js
index cb39d1bcd6c9..745af4157ba5 100644
--- a/tests/cypress/e2e/actions_objects2/case_16_z_order_features.js
+++ b/tests/cypress/e2e/actions_objects2/case_16_z_order_features.js
@@ -63,7 +63,7 @@ context('Actions on polygon', () => {
             cy.get('.cvat-canvas-container').click();
         });
 
-        it('Second shape is over the first shape', () => {
+        it('Second shape is over the first shape', () => {
             // The larger the index of an element in the array the closer it is to us
             cy.get('.cvat_canvas_shape').then(($canvasShape) => {
                 expect(Number($canvasShape[1].id.match(/\d+$/))).to.be.equal(2);
@@ -76,7 +76,7 @@ context('Actions on polygon', () => {
             cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated');
         });
 
-        it('First shape is over the second shape', () => {
+        it('First shape is over the second shape', () => {
             // The larger the index of an element in the array the closer it is to us
             cy.get('.cvat_canvas_shape').then(($canvasShape) => {
                 expect(Number($canvasShape[1].id.match(/\d+$/))).to.be.equal(1);
diff --git a/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js b/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js
index 855d452bbddc..97fcf850b984 100644
--- a/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js
+++ b/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js
@@ -218,7 +218,7 @@ context('Lock/hide features.', () => {
                 cy.contains('Labels').click();
             });
         });
-        it('Repeat hide/lock for one of the labels. Objects with other labels weren’t affected.', () => {
+        it("Repeat hide/lock for one of the labels. Objects with other labels weren't affected.", () => {
             const objectsSameLabel = ['cvat_canvas_shape_1', 'cvat_canvas_shape_2', 'cvat_canvas_shape_3'];
             cy.get('.cvat-objects-sidebar-labels-list').within(() => {
                 // Hide and lock all object with "Main task" label (#cvat_canvas_shape_1-3).
diff --git a/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js b/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js
index fd641be141c0..48227db05641 100644
--- a/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js
+++ b/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js
@@ -33,7 +33,7 @@ context('Changing a default value for an attribute.', () => {
     });
 
     describe(`Testing case "${caseId}", issue 2968`, () => {
-        it('Add a label, add text (leave it’s value empty by default) & checkbox attributes.', () => {
+        it('Add a label, add text (leave its value empty by default) & checkbox attributes.', () => {
             cy.intercept('PATCH', '/api/tasks/**').as('patchTask');
             cy.addNewLabel({ name: additionalLabel }, additionalAttrsLabel);
             cy.wait('@patchTask').its('response.statusCode').should('equal', 200);
diff --git a/tests/cypress/e2e/features/masks_basics.js b/tests/cypress/e2e/features/masks_basics.js
index 3e119e97f039..ac7a1358e231 100644
--- a/tests/cypress/e2e/features/masks_basics.js
+++ b/tests/cypress/e2e/features/masks_basics.js
@@ -226,6 +226,59 @@ context('Manipulations with masks', { scrollBehavior: false }, () => {
             cy.get('body').type('n');
             cy.get('.cvat-brush-tools-toolbox').should('not.be.visible');
         });
+
+        it('Check hide mask feature', () => {
+            function checkHideFeature() {
+                cy.get('.cvat-brush-tools-hide').click();
+                cy.get('.cvat-brush-tools-hide').should('have.class', 'cvat-brush-tools-active-tool');
+                cy.get('.cvat_masks_canvas_wrapper').should('not.be.visible');
+                cy.get('.cvat-brush-tools-hide').click();
+                cy.get('.cvat_masks_canvas_wrapper').should('be.visible');
+            }
+
+            function checkHideShortcut() {
+                cy.get('body').type('h');
+                cy.get('.cvat-brush-tools-hide').should('have.class', 'cvat-brush-tools-active-tool');
+                cy.get('.cvat_masks_canvas_wrapper').should('not.be.visible');
+            }
+
+            function checkObjectIsHidden() {
+                cy.get('#cvat-objects-sidebar-state-item-1').within(() => {
+                    cy.get('.cvat-object-item-button-hidden-enabled').should('exist');
+                });
+            }
+
+            const mask = [{
+                method: 'brush',
+                coordinates: [[450, 250], [600, 400]],
+            }];
+            const drawPolygon = [{
+                method: 'polygon-plus',
+                coordinates: [[450, 210], [650, 400], [450, 600], [260, 400]],
+            }];
+            cy.startMaskDrawing();
+            cy.drawMask(mask);
+
+            checkHideFeature();
+            checkHideShortcut();
+
+            cy.finishMaskDrawing();
+            cy.get('#cvat_canvas_shape_1').should('be.visible');
+
+            cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit');
+            checkHideFeature();
+
+            cy.drawMask(drawPolygon);
+            checkHideShortcut();
+            cy.get('.cvat_canvas_shape_drawing')
+                .invoke('attr', 'fill-opacity')
+                .then((opacity) => expect(+opacity).to.be.equal(0));
+            checkObjectIsHidden();
+            cy.get('.cvat-brush-tools-brush').click();
+            cy.get('.cvat-brush-tools-brush').should('have.class', 'cvat-brush-tools-active-tool');
+            cy.finishMaskDrawing();
+            checkObjectIsHidden();
+        });
     });
 
     describe('Tests to make sure that empty masks cannot be created', () => {
diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js
index 91322e9c3695..d3988c2e56a0 100644
--- a/tests/cypress/support/commands.js
+++ b/tests/cypress/support/commands.js
@@ -1565,6 +1565,7 @@ Cypress.Commands.add('startMaskDrawing', () => {
 Cypress.Commands.add('finishMaskDrawing', () => {
     cy.get('.cvat-brush-tools-brush').click();
     cy.get('.cvat-brush-tools-finish').click();
+    cy.hideTooltips();
 });
 
 Cypress.Commands.add('sliceShape', (
diff --git a/tests/python/cli/example_function.py b/tests/python/cli/example_function.py
index 4b1b41857825..57d67a5b40a2 100644
--- a/tests/python/cli/example_function.py
+++ b/tests/python/cli/example_function.py
@@ -2,8 +2,6 @@
 #
 # SPDX-License-Identifier: MIT
 
-from typing import List
-
 import cvat_sdk.auto_annotation as cvataa
 import cvat_sdk.models as models
 import PIL.Image
@@ -17,7 +15,7 @@
 
 def detect(
     context: cvataa.DetectionFunctionContext, image: PIL.Image.Image
-) -> List[models.LabeledShapeRequest]:
+) -> list[models.LabeledShapeRequest]:
     return [
         cvataa.rectangle(0, [1, 2, 3, 4]),
     ]
diff --git a/tests/python/cli/example_parameterized_function.py b/tests/python/cli/example_parameterized_function.py
index 29d9038e78b4..e46d40867a4c 100644
--- a/tests/python/cli/example_parameterized_function.py
+++ b/tests/python/cli/example_parameterized_function.py
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: MIT
 
 from types import SimpleNamespace as namespace
-from typing import List
 
 import cvat_sdk.auto_annotation as cvataa
 import cvat_sdk.models as models
@@ -24,7 +23,7 @@ def create(s: str, i: int, f: float, b: bool) -> cvataa.DetectionFunction:
 
     def detect(
         context: cvataa.DetectionFunctionContext, image: PIL.Image.Image
-    ) -> List[models.LabeledShapeRequest]:
+    ) -> list[models.LabeledShapeRequest]:
         return [
             cvataa.rectangle(0, [1, 2, 3, 4]),
         ]
diff --git a/tests/python/cli/util.py b/tests/python/cli/util.py
index ff1173fa4a8d..d399e695dab8 100644
--- a/tests/python/cli/util.py
+++ b/tests/python/cli/util.py
@@ -8,8 +8,9 @@
 import ssl
 import threading
 import unittest
+from collections.abc import Generator
 from pathlib import Path
-from typing import Any, Dict, Generator, List, Union
+from typing import Any, Union
 
 import requests
 
@@ -28,7 +29,7 @@ def run_cli(test: Union[unittest.TestCase, Any], *args: str, expected_code: int
         assert expected_code == main(args)
 
 
-def generate_images(dst_dir: Path, count: int) -> List[Path]:
+def generate_images(dst_dir: Path, count: int) -> list[Path]:
     filenames = []
     dst_dir.mkdir(parents=True, exist_ok=True)
     for i in range(count):
@@ -70,7 +71,7 @@ def do_POST(self):
         response = requests.post(data=self.rfile.read(body_length), **self._shared_request_args())
         self._translate_response(response)
 
-    def _shared_request_args(self) -> Dict[str, Any]:
+    def _shared_request_args(self) -> dict[str, Any]:
         headers = {k.lower(): v for k, v in self.headers.items()}
         del headers["host"]
 
diff --git a/tests/python/rest_api/test_analytics.py b/tests/python/rest_api/test_analytics.py
index f14cdd206f84..68671889a21c 100644
--- a/tests/python/rest_api/test_analytics.py
+++ b/tests/python/rest_api/test_analytics.py
@@ -38,7 +38,6 @@ def _test_cannot_see(self, user):
         "conditions, is_allow",
         [
             (dict(privilege="admin"), True),
-            (dict(privilege="business"), True),
             (dict(privilege="worker", has_analytics_access=False), False),
             (dict(privilege="worker", has_analytics_access=True), True),
             (dict(privilege="user", has_analytics_access=False), False),
diff --git a/tests/python/rest_api/test_analytics_reports.py b/tests/python/rest_api/test_analytics_reports.py
index a50c053fc138..bb48b19e2dd5 100644
--- a/tests/python/rest_api/test_analytics_reports.py
+++ b/tests/python/rest_api/test_analytics_reports.py
@@ -4,7 +4,7 @@
 
 import json
 from http import HTTPStatus
-from typing import Any, Dict, Optional
+from typing import Any, Optional
 
 import pytest
 from cvat_sdk.api_client import models
@@ -67,7 +67,7 @@ def _test_get_report_200(
         job_id: Optional[int] = None,
         task_id: Optional[int] = None,
         project_id: Optional[int] = None,
-        expected_data: Optional[Dict[str, Any]] = None,
+        expected_data: Optional[dict[str, Any]] = None,
         **kwargs,
     ):
         params = self._get_query_params(job_id=job_id, task_id=task_id, project_id=project_id)
diff --git a/tests/python/rest_api/test_cloud_storages.py b/tests/python/rest_api/test_cloud_storages.py
index 9fc1739b9e0f..ce2db93cab56 100644
--- a/tests/python/rest_api/test_cloud_storages.py
+++ b/tests/python/rest_api/test_cloud_storages.py
@@ -58,7 +58,6 @@ def _test_cannot_see(self, user, storage_id):
         "group, is_owner, is_allow",
         [
             ("admin", False, True),
-            ("business", False, False),
             ("user", True, True),
         ],
     )
@@ -302,7 +301,6 @@ def _test_cannot_update(self, user, storage_id, spec):
         "group, is_owner, is_allow",
         [
             ("admin", False, True),
-            ("business", False, False),
             ("worker", True, True),
         ],
     )
@@ -387,7 +385,6 @@ def _test_cannot_see(self, user, storage_id):
         "group, is_owner, is_allow",
         [
             ("admin", False, True),
-            ("business", False, False),
             ("user", True, True),
         ],
     )
diff --git a/tests/python/rest_api/test_issues.py b/tests/python/rest_api/test_issues.py
index c6c043f2e449..f1cbfdafacd2 100644
--- a/tests/python/rest_api/test_issues.py
+++ b/tests/python/rest_api/test_issues.py
@@ -6,7 +6,7 @@
 import json
 from copy import deepcopy
 from http import HTTPStatus
-from typing import Any, Dict, List, Tuple
+from typing import Any
 
 import pytest
 from cvat_sdk import models
@@ -55,8 +55,6 @@ def _test_check_response(self, user, data, is_allow, **kwargs):
         [
             ("admin", True, True),
             ("admin", False, True),
-            ("business", True, True),
-            ("business", False, False),
             ("worker", True, True),
             ("worker", False, False),
             ("user", True, True),
@@ -185,8 +183,6 @@ def get_data(issue_id, *, username: str = None):
         [
             ("admin", True, None, True),
             ("admin", False, None, True),
-            ("business", True, None, True),
-            ("business", False, None, False),
             ("user", True, None, True),
             ("user", False, None, False),
             ("worker", False, True, True),
@@ -275,8 +271,6 @@ def _test_check_response(self, user, issue_id, expect_success, **kwargs):
         [
             ("admin", True, None, True),
             ("admin", False, None, True),
-            ("business", True, None, True),
-            ("business", False, None, False),
             ("user", True, None, True),
             ("user", False, None, False),
             ("worker", False, True, True),
@@ -373,7 +367,7 @@ def setup(self, restore_db_per_class, admin_user, comments, issues):
     def _get_endpoint(self, api_client: ApiClient) -> Endpoint:
         return api_client.comments_api.list_endpoint
 
-    def _get_field_samples(self, field: str) -> Tuple[Any, List[Dict[str, Any]]]:
+    def _get_field_samples(self, field: str) -> tuple[Any, list[dict[str, Any]]]:
         if field == "job_id":
             issue_id, issue_comments = super()._get_field_samples("issue_id")
             issue = next((s for s in self.sample_issues if s["id"] == issue_id))
diff --git a/tests/python/rest_api/test_jobs.py b/tests/python/rest_api/test_jobs.py
index 6d5626fcda99..5057f652030c 100644
--- a/tests/python/rest_api/test_jobs.py
+++ b/tests/python/rest_api/test_jobs.py
@@ -15,7 +15,7 @@
 from http import HTTPStatus
 from io import BytesIO
 from itertools import groupby, product
-from typing import Any, Dict, List, Optional, Set, Tuple, Union
+from typing import Any, Optional, Union
 
 import numpy as np
 import pytest
@@ -71,7 +71,7 @@ def filter_jobs(jobs, tasks, org):
 
 @pytest.mark.usefixtures("restore_db_per_function")
 class TestPostJobs:
-    def _test_create_job_ok(self, user: str, data: Dict[str, Any], **kwargs):
+    def _test_create_job_ok(self, user: str, data: dict[str, Any], **kwargs):
         with make_api_client(user) as api_client:
             (_, response) = api_client.jobs_api.create(
                 models.JobWriteRequest(**deepcopy(data)), **kwargs
@@ -80,7 +80,7 @@ def _test_create_job_ok(self, user: str, data: Dict[str, Any], **kwargs):
         return response
 
     def _test_create_job_fails(
-        self, user: str, data: Dict[str, Any], *, expected_status: int, **kwargs
+        self, user: str, data: dict[str, Any], *, expected_status: int, **kwargs
     ):
         with make_api_client(user) as api_client:
             (_, response) = api_client.jobs_api.create(
@@ -110,7 +110,7 @@ def test_can_create_gt_job_in_a_task(
         tasks,
         task_mode: str,
         frame_selection_method: str,
-        method_params: Set[str],
+        method_params: set[str],
     ):
         required_task_size = 15
 
@@ -544,7 +544,7 @@ def test_destroy_gt_job_in_org_task(
 @pytest.mark.usefixtures("restore_db_per_class")
 class TestGetJobs:
     def _test_get_job_200(
-        self, user, jid, *, expected_data: Optional[Dict[str, Any]] = None, **kwargs
+        self, user, jid, *, expected_data: Optional[dict[str, Any]] = None, **kwargs
     ):
         with make_api_client(user) as client:
             (_, response) = client.jobs_api.retrieve(jid, **kwargs)
@@ -568,7 +568,7 @@ def test_admin_can_get_org_job(self, admin_user, jobs, tasks):
         job = next(job for job in jobs if tasks[job["task_id"]]["organization"] is not None)
         self._test_get_job_200(admin_user, job["id"], expected_data=job)
 
-    @pytest.mark.parametrize("groups", [["business"], ["user"]])
+    @pytest.mark.parametrize("groups", [["user"]])
     def test_non_admin_org_staff_can_get_job(
         self, groups, users, organizations, org_staff, jobs_by_org
     ):
@@ -581,7 +581,7 @@ def test_non_admin_org_staff_can_get_job(
         job = jobs_by_org[org_id][0]
         self._test_get_job_200(user["username"], job["id"], expected_data=job)
 
-    @pytest.mark.parametrize("groups", [["business"], ["user"], ["worker"]])
+    @pytest.mark.parametrize("groups", [["user"], ["worker"]])
     def test_non_admin_job_staff_can_get_job(self, groups, users, jobs, is_job_staff):
         user, job = next(
             (user, job)
@@ -591,7 +591,7 @@ def test_non_admin_job_staff_can_get_job(self, groups, users, jobs, is_job_staff
         )
         self._test_get_job_200(user["username"], job["id"], expected_data=job)
 
-    @pytest.mark.parametrize("groups", [["business"], ["user"], ["worker"]])
+    @pytest.mark.parametrize("groups", [["user"], ["worker"]])
     def test_non_admin_non_job_staff_non_org_staff_cannot_get_job(
         self, groups, users, organizations, org_staff, jobs, is_job_staff
     ):
@@ -955,7 +955,7 @@ def test_admin_list_jobs(self, jobs, tasks, org):
         self._test_list_jobs_200("admin1", jobs, **kwargs)
 
     @pytest.mark.parametrize("org_id", ["", None, 1, 2])
-    @pytest.mark.parametrize("groups", [["business"], ["user"], ["worker"], []])
+    @pytest.mark.parametrize("groups", [["user"], ["worker"], []])
     def test_non_admin_list_jobs(
         self, org_id, groups, users, jobs, tasks, projects, org_staff, is_org_member
     ):
@@ -1024,8 +1024,6 @@ def _test_get_job_annotations_403(self, user, jid):
         [
             (["admin"], True, True),
             (["admin"], False, True),
-            (["business"], True, True),
-            (["business"], False, False),
             (["worker"], True, True),
             (["worker"], False, False),
             (["user"], True, True),
@@ -1093,7 +1091,7 @@ def test_member_get_job_annotations(
     @pytest.mark.parametrize("org", [1])
     @pytest.mark.parametrize(
         "privilege, expect_success",
-        [("admin", True), ("business", False), ("worker", False), ("user", False)],
+        [("admin", True), ("worker", False), ("user", False)],
     )
     def test_non_member_get_job_annotations(
         self,
@@ -1191,7 +1189,7 @@ def test_member_update_job_annotations(
     @pytest.mark.parametrize("org", [2])
     @pytest.mark.parametrize(
         "privilege, expect_success",
-        [("admin", True), ("business", False), ("worker", False), ("user", False)],
+        [("admin", True), ("worker", False), ("user", False)],
     )
     def test_non_member_update_job_annotations(
         self,
@@ -1218,8 +1216,6 @@ def test_non_member_update_job_annotations(
         [
             ("admin", True, True),
             ("admin", False, True),
-            ("business", True, True),
-            ("business", False, False),
             ("worker", True, True),
             ("worker", False, False),
             ("user", True, True),
@@ -1446,7 +1442,7 @@ def _test_export_dataset(
         username: str,
         jid: int,
         *,
-        api_version: Union[int, Tuple[int]],
+        api_version: Union[int, tuple[int]],
         local_download: bool = True,
         **kwargs,
     ) -> Optional[bytes]:
@@ -1477,9 +1473,9 @@ def _test_export_annotations(
     def test_can_export_dataset_locally_and_to_cloud_with_both_api_versions(
         self,
         admin_user: str,
-        jobs_with_shapes: List,
+        jobs_with_shapes: list,
         filter_tasks,
-        api_version: Tuple[int],
+        api_version: tuple[int],
         local_download: bool,
     ):
         filter_ = "target_storage__location"
@@ -1651,15 +1647,6 @@ def test_admin_get_org_job_preview(self, jobs, tasks):
         job_id = next(job["id"] for job in jobs if tasks[job["task_id"]]["organization"])
         self._test_get_job_preview_200("admin2", job_id)
 
-    def test_business_can_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff):
-        username, job_id = next(
-            (user["username"], job["id"])
-            for user in find_users(privilege="business")
-            for job in jobs
-            if is_job_staff(user["id"], job["id"])
-        )
-        self._test_get_job_preview_200(username, job_id)
-
     def test_user_can_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff):
         username, job_id = next(
             (user["username"], job["id"])
@@ -1669,15 +1656,6 @@ def test_user_can_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staf
         )
         self._test_get_job_preview_200(username, job_id)
 
-    def test_business_cannot_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff):
-        username, job_id = next(
-            (user["username"], job["id"])
-            for user in find_users(privilege="business")
-            for job in jobs
-            if not is_job_staff(user["id"], job["id"])
-        )
-        self._test_get_job_preview_403(username, job_id)
-
     def test_user_cannot_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff):
         username, job_id = next(
             (user["username"], job["id"])
diff --git a/tests/python/rest_api/test_labels.py b/tests/python/rest_api/test_labels.py
index d64133e7dd36..00cfd6225647 100644
--- a/tests/python/rest_api/test_labels.py
+++ b/tests/python/rest_api/test_labels.py
@@ -7,7 +7,7 @@
 from copy import deepcopy
 from http import HTTPStatus
 from types import SimpleNamespace
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Optional
 
 import pytest
 from cvat_sdk import exceptions, models
@@ -60,7 +60,7 @@ def setup(self, _base_setup):
         """
 
     @staticmethod
-    def _labels_by_source(labels: List[Dict], *, source_key: str) -> Dict[int, List[Dict]]:
+    def _labels_by_source(labels: list[dict], *, source_key: str) -> dict[int, list[dict]]:
         labels_by_source = {}
         for label in labels:
             label_source = label.get(source_key)
@@ -216,7 +216,7 @@ def setup(self, restore_db_per_class, admin_user, labels, jobs_wlc, tasks_wlc, p
     def _get_endpoint(self, api_client: ApiClient) -> Endpoint:
         return api_client.labels_api.list_endpoint
 
-    def _get_field_samples(self, field: str) -> Tuple[Any, List[Dict[str, Any]]]:
+    def _get_field_samples(self, field: str) -> tuple[Any, list[dict[str, Any]]]:
         if field == "parent":
             parent_id, gt_objects = self._get_field_samples("parent_id")
             parent_name = self._get_field(
@@ -584,8 +584,8 @@ def _test_update_denied(self, user, lid, data, expected_status=HTTPStatus.FORBID
         return response
 
     def _get_patch_data(
-        self, original_data: Dict[str, Any], **overrides
-    ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+        self, original_data: dict[str, Any], **overrides
+    ) -> tuple[dict[str, Any], dict[str, Any]]:
         result = deepcopy(original_data)
         result.update(overrides)
 
diff --git a/tests/python/rest_api/test_memberships.py b/tests/python/rest_api/test_memberships.py
index e03cac2e2779..a25074af4890 100644
--- a/tests/python/rest_api/test_memberships.py
+++ b/tests/python/rest_api/test_memberships.py
@@ -4,7 +4,7 @@
 # SPDX-License-Identifier: MIT
 
 from http import HTTPStatus
-from typing import ClassVar, List
+from typing import ClassVar
 
 import pytest
 from cvat_sdk.api_client.api_client import ApiClient, Endpoint
@@ -40,7 +40,7 @@ def test_can_filter_by_org_id(self, field_value, query_value, memberships):
         )
 
     def test_non_admin_can_see_only_self_memberships(self, memberships):
-        non_admins = ["business1", "user1", "dummy1", "worker2"]
+        non_admins = ["user1", "dummy1", "worker2"]
         for username in non_admins:
             data = [obj for obj in memberships if obj["user"]["username"] == username]
             self._test_can_see_memberships(username, data)
@@ -80,7 +80,7 @@ def test_can_use_simple_filter_for_object_list(self, field):
 @pytest.mark.usefixtures("restore_db_per_function")
 class TestPatchMemberships:
     _ORG: ClassVar[int] = 1
-    ROLES: ClassVar[List[str]] = ["worker", "supervisor", "maintainer", "owner"]
+    ROLES: ClassVar[list[str]] = ["worker", "supervisor", "maintainer", "owner"]
 
     def _test_can_change_membership(self, user, membership_id, new_role):
         response = patch_method(
diff --git a/tests/python/rest_api/test_organizations.py b/tests/python/rest_api/test_organizations.py
index 5daee9e53537..50834b1fff83 100644
--- a/tests/python/rest_api/test_organizations.py
+++ b/tests/python/rest_api/test_organizations.py
@@ -31,7 +31,6 @@ class TestMetadataOrganizations:
         [
             ("admin", None, None),
             ("user", None, False),
-            ("business", None, False),
             ("worker", None, False),
             (None, "owner", True),
             (None, "maintainer", True),
@@ -79,7 +78,6 @@ class TestGetOrganizations:
         [
             ("admin", None, None, True),
             ("user", None, False, False),
-            ("business", None, False, False),
             ("worker", None, False, False),
             (None, "owner", True, True),
             (None, "maintainer", True, True),
@@ -182,7 +180,6 @@ def expected_data(self, organizations, request_data):
         [
             ("admin", None, None, True),
             ("user", None, False, False),
-            ("business", None, False, False),
             ("worker", None, False, False),
             (None, "owner", True, True),
             (None, "maintainer", True, True),
@@ -239,7 +236,6 @@ class TestDeleteOrganizations:
             (None, "worker", True, False),
             (None, "supervisor", True, False),
             ("user", None, False, False),
-            ("business", None, False, False),
             ("worker", None, False, False),
         ],
     )
diff --git a/tests/python/rest_api/test_projects.py b/tests/python/rest_api/test_projects.py
index b0c8a3b247c4..abfccd5f6b03 100644
--- a/tests/python/rest_api/test_projects.py
+++ b/tests/python/rest_api/test_projects.py
@@ -16,7 +16,7 @@
 from itertools import product
 from operator import itemgetter
 from time import sleep
-from typing import Dict, List, Optional, Tuple, Union
+from typing import Optional, Union
 
 import pytest
 from cvat_sdk.api_client import ApiClient, Configuration, models
@@ -34,8 +34,14 @@
     patch_method,
     post_method,
 )
+from shared.utils.helpers import generate_image_files
 
-from .utils import CollectionSimpleFilterTestBase, export_project_backup, export_project_dataset
+from .utils import (
+    CollectionSimpleFilterTestBase,
+    create_task,
+    export_project_backup,
+    export_project_dataset,
+)
 
 
 @pytest.mark.usefixtures("restore_db_per_class")
@@ -447,7 +453,7 @@ def test_if_worker_cannot_create_project(self, find_users):
         spec = {"name": f"test {username} tries to create a project"}
         self._test_create_project_403(username, spec)
 
-    @pytest.mark.parametrize("privilege", ("admin", "business", "user"))
+    @pytest.mark.parametrize("privilege", ("admin", "user"))
     def test_if_user_can_create_project(self, find_users, privilege):
         privileged_users = find_users(privilege=privilege)
         assert len(privileged_users)
@@ -498,7 +504,7 @@ def _create_user(cls, api_client: ApiClient, email: str) -> str:
         return json.loads(response.data)
 
     @classmethod
-    def _create_org(cls, api_client: ApiClient, members: Optional[Dict[str, str]] = None) -> str:
+    def _create_org(cls, api_client: ApiClient, members: Optional[dict[str, str]] = None) -> str:
         with api_client:
             (_, response) = api_client.organizations_api.create(
                 models.OrganizationWriteRequest(slug="test_org_roles"), _parse_response=False
@@ -611,6 +617,7 @@ def _check_cvat_for_video_project_annotations_meta(content, values_to_be_checked
 
 @pytest.mark.usefixtures("restore_db_per_function")
 @pytest.mark.usefixtures("restore_redis_inmem_per_function")
+@pytest.mark.usefixtures("restore_redis_ondisk_per_function")
 class TestImportExportDatasetProject:
 
     @pytest.fixture(autouse=True)
@@ -622,7 +629,7 @@ def _test_export_dataset(
         username: str,
         pid: int,
         *,
-        api_version: Union[int, Tuple[int]],
+        api_version: Union[int, tuple[int]],
         local_download: bool = True,
         **kwargs,
     ) -> Optional[bytes]:
@@ -771,7 +778,7 @@ def test_can_import_export_dataset_with_some_format(self, format_name: str, api_
         "local_download", (True, pytest.param(False, marks=pytest.mark.with_external_services))
     )
     def test_can_export_dataset_locally_and_to_cloud_with_both_api_versions(
-        self, admin_user: str, filter_projects, api_version: Tuple[int], local_download: bool
+        self, admin_user: str, filter_projects, api_version: tuple[int], local_download: bool
     ):
         filter_ = "target_storage__location"
         if local_download:
@@ -1038,10 +1045,65 @@ def test_creates_subfolders_for_subsets_on_export(
                     len([f for f in zip_file.namelist() if f.startswith(folder_prefix)]) > 0
                 ), f"No {folder_prefix} in {zip_file.namelist()}"
 
+    def test_export_project_with_honeypots(
+        self,
+        admin_user: str,
+    ):
+        project_spec = {
+            "name": "Project with honeypots",
+            "labels": [{"name": "cat"}],
+        }
+
+        with make_api_client(admin_user) as api_client:
+            project, _ = api_client.projects_api.create(project_spec)
+
+        image_files = generate_image_files(3)
+        image_names = [i.name for i in image_files]
+
+        task_params = {
+            "name": "Task with honeypots",
+            "segment_size": 1,
+            "project_id": project.id,
+        }
+
+        data_params = {
+            "image_quality": 70,
+            "client_files": image_files,
+            "sorting_method": "random",
+            "validation_params": {
+                "mode": "gt_pool",
+                "frame_selection_method": "manual",
+                "frames_per_job_count": 1,
+                "frames": [image_files[-1].name],
+            },
+        }
+
+        create_task(admin_user, spec=task_params, data=data_params)
+
+        dataset = export_project_dataset(
+            admin_user, api_version=2, save_images=True, id=project.id, format="COCO 1.0"
+        )
+
+        with zipfile.ZipFile(io.BytesIO(dataset)) as zip_file:
+            subset_path = "images/default"
+            assert (
+                sorted(
+                    [
+                        f[len(subset_path) + 1 :]
+                        for f in zip_file.namelist()
+                        if f.startswith(subset_path)
+                    ]
+                )
+                == image_names
+            )
+            with zip_file.open("annotations/instances_default.json") as anno_file:
+                annotations = json.load(anno_file)
+                assert sorted([a["file_name"] for a in annotations["images"]]) == image_names
+
 
 @pytest.mark.usefixtures("restore_db_per_function")
 class TestPatchProjectLabel:
-    def _get_project_labels(self, pid, user, **kwargs) -> List[models.Label]:
+    def _get_project_labels(self, pid, user, **kwargs) -> list[models.Label]:
         kwargs.setdefault("return_json", True)
         with make_api_client(user) as api_client:
             return get_paginated_collection(
diff --git a/tests/python/rest_api/test_quality_control.py b/tests/python/rest_api/test_quality_control.py
index 1886a0a62ac1..d03675c9156e 100644
--- a/tests/python/rest_api/test_quality_control.py
+++ b/tests/python/rest_api/test_quality_control.py
@@ -3,11 +3,12 @@
 # SPDX-License-Identifier: MIT
 
 import json
+from collections.abc import Iterable
 from copy import deepcopy
 from functools import partial
 from http import HTTPStatus
 from itertools import groupby
-from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
+from typing import Any, Callable, Optional
 
 import pytest
 from cvat_sdk.api_client import exceptions, models
@@ -84,7 +85,7 @@ def create_gt_job(self, user, task_id):
     def find_sandbox_task(self, tasks, jobs, users, is_task_staff):
         def _find(
             is_staff: bool, *, has_gt_jobs: Optional[bool] = None
-        ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+        ) -> tuple[dict[str, Any], dict[str, Any]]:
             task = next(
                 t
                 for t in tasks
@@ -116,7 +117,7 @@ def find_sandbox_task_without_gt(self, find_sandbox_task):
     def find_org_task(self, tasks, jobs, users, is_org_member, is_task_staff):
         def _find(
             is_staff: bool, user_org_role: str, *, has_gt_jobs: Optional[bool] = None
-        ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+        ) -> tuple[dict[str, Any], dict[str, Any]]:
             for user in users:
                 if user["is_superuser"]:
                     continue
@@ -249,7 +250,7 @@ def test_user_list_reports_in_org_task(
 @pytest.mark.usefixtures("restore_db_per_class")
 class TestGetQualityReports(_PermissionTestBase):
     def _test_get_report_200(
-        self, user: str, obj_id: int, *, expected_data: Optional[Dict[str, Any]] = None, **kwargs
+        self, user: str, obj_id: int, *, expected_data: Optional[dict[str, Any]] = None, **kwargs
     ):
         with make_api_client(user) as api_client:
             (_, response) = api_client.quality_api.retrieve_report(obj_id, **kwargs)
@@ -308,7 +309,7 @@ def test_user_get_report_in_org_task(
 @pytest.mark.usefixtures("restore_db_per_class")
 class TestGetQualityReportData(_PermissionTestBase):
     def _test_get_report_data_200(
-        self, user: str, obj_id: int, *, expected_data: Optional[Dict[str, Any]] = None, **kwargs
+        self, user: str, obj_id: int, *, expected_data: Optional[dict[str, Any]] = None, **kwargs
     ):
         with make_api_client(user) as api_client:
             (_, response) = api_client.quality_api.retrieve_report_data(obj_id, **kwargs)
@@ -603,7 +604,7 @@ def _test_check_status_of_report_creation_by_non_rq_job_owner(
 
     def test_non_rq_job_owner_cannot_check_status_of_report_creation_in_sandbox(
         self,
-        find_sandbox_task_without_gt: Callable[[bool], Tuple[Dict[str, Any], Dict[str, Any]]],
+        find_sandbox_task_without_gt: Callable[[bool], tuple[dict[str, Any], dict[str, Any]]],
         admin_user: str,
         users: Iterable,
     ):
@@ -630,8 +631,8 @@ def test_non_rq_job_owner_cannot_check_status_of_report_creation_in_org(
         self,
         role: str,
         admin_user: str,
-        find_org_task_without_gt: Callable[[bool, str], Tuple[Dict[str, Any], Dict[str, Any]]],
-        find_users: Callable[..., List[Dict[str, Any]]],
+        find_org_task_without_gt: Callable[[bool, str], tuple[dict[str, Any], dict[str, Any]]],
+        find_users: Callable[..., list[dict[str, Any]]],
     ):
         task, task_staff = find_org_task_without_gt(is_staff=True, user_org_role="supervisor")
 
@@ -657,8 +658,8 @@ def test_admin_can_check_status_of_report_creation(
         is_sandbox: bool,
         users: Iterable,
         admin_user: str,
-        find_org_task_without_gt: Callable[[bool, str], Tuple[Dict[str, Any], Dict[str, Any]]],
-        find_sandbox_task_without_gt: Callable[[bool], Tuple[Dict[str, Any], Dict[str, Any]]],
+        find_org_task_without_gt: Callable[[bool, str], tuple[dict[str, Any], dict[str, Any]]],
+        find_sandbox_task_without_gt: Callable[[bool], tuple[dict[str, Any], dict[str, Any]]],
     ):
         if is_sandbox:
             task, task_staff = find_sandbox_task_without_gt(is_staff=True)
@@ -696,7 +697,7 @@ def setup(self, restore_db_per_class, admin_user, quality_reports, jobs, tasks):
     def _get_endpoint(self, api_client: ApiClient) -> Endpoint:
         return api_client.quality_api.list_reports_endpoint
 
-    def _get_field_samples(self, field: str) -> Tuple[Any, List[Dict[str, Any]]]:
+    def _get_field_samples(self, field: str) -> tuple[Any, list[dict[str, Any]]]:
         if field == "task_id":
             # This filter includes both the task and nested job reports
             task_id, task_reports = super()._get_field_samples(field)
@@ -819,7 +820,7 @@ def setup(
     def _get_endpoint(self, api_client: ApiClient) -> Endpoint:
         return api_client.quality_api.list_conflicts_endpoint
 
-    def _get_field_samples(self, field: str) -> Tuple[Any, List[Dict[str, Any]]]:
+    def _get_field_samples(self, field: str) -> tuple[Any, list[dict[str, Any]]]:
         if field == "job_id":
             # This field is not included in the response
             job_id = self._find_valid_field_value(self.report_samples, field_path=["job_id"])
@@ -889,7 +890,7 @@ def test_can_use_simple_filter_for_object_list(self, field):
 @pytest.mark.usefixtures("restore_db_per_class")
 class TestListSettings(_PermissionTestBase):
     def _test_list_settings_200(
-        self, user: str, task_id: int, *, expected_data: Optional[Dict[str, Any]] = None, **kwargs
+        self, user: str, task_id: int, *, expected_data: Optional[dict[str, Any]] = None, **kwargs
     ):
         with make_api_client(user) as api_client:
             actual = get_paginated_collection(
@@ -951,7 +952,7 @@ def test_user_list_settings_in_org_task(
 @pytest.mark.usefixtures("restore_db_per_class")
 class TestGetSettings(_PermissionTestBase):
     def _test_get_settings_200(
-        self, user: str, obj_id: int, *, expected_data: Optional[Dict[str, Any]] = None, **kwargs
+        self, user: str, obj_id: int, *, expected_data: Optional[dict[str, Any]] = None, **kwargs
     ):
         with make_api_client(user) as api_client:
             (_, response) = api_client.quality_api.retrieve_settings(obj_id, **kwargs)
@@ -1016,9 +1017,9 @@ def _test_patch_settings_200(
         self,
         user: str,
         obj_id: int,
-        data: Dict[str, Any],
+        data: dict[str, Any],
         *,
-        expected_data: Optional[Dict[str, Any]] = None,
+        expected_data: Optional[dict[str, Any]] = None,
         **kwargs,
     ):
         with make_api_client(user) as api_client:
@@ -1032,7 +1033,7 @@ def _test_patch_settings_200(
 
         return response
 
-    def _test_patch_settings_403(self, user: str, obj_id: int, data: Dict[str, Any], **kwargs):
+    def _test_patch_settings_403(self, user: str, obj_id: int, data: dict[str, Any], **kwargs):
         with make_api_client(user) as api_client:
             (_, response) = api_client.quality_api.partial_update_settings(
                 obj_id,
@@ -1045,7 +1046,7 @@ def _test_patch_settings_403(self, user: str, obj_id: int, data: Dict[str, Any],
 
         return response
 
-    def _get_request_data(self, data: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+    def _get_request_data(self, data: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
         patched_data = deepcopy(data)
 
         for field, value in data.items():
@@ -1211,6 +1212,8 @@ def test_modified_task_produces_different_metrics(
             "oks_sigma",
             "compare_line_orientation",
             "panoptic_comparison",
+            "point_size_base",
+            "match_empty_frames",
         ],
     )
     def test_settings_affect_metrics(
@@ -1228,6 +1231,12 @@ def test_settings_affect_metrics(
             settings[parameter] = 1 - settings[parameter]
             if parameter == "group_match_threshold":
                 settings[parameter] = 0.9
+        elif parameter == "point_size_base":
+            settings[parameter] = next(
+                v
+                for v in models.PointSizeBaseEnum.allowed_values[("value",)].values()
+                if v != settings[parameter]
+            )
         else:
             assert False
 
@@ -1237,7 +1246,12 @@ def test_settings_affect_metrics(
             )
 
         new_report = self.create_quality_report(admin_user, task_id)
-        assert new_report["summary"]["conflict_count"] != old_report["summary"]["conflict_count"]
+        if parameter == "match_empty_frames":
+            assert new_report["summary"]["valid_count"] != old_report["summary"]["valid_count"]
+        else:
+            assert (
+                new_report["summary"]["conflict_count"] != old_report["summary"]["conflict_count"]
+            )
 
     def test_old_report_can_be_loaded(self, admin_user, quality_reports):
         report = min((r for r in quality_reports if r["task_id"]), key=lambda r: r["id"])
diff --git a/tests/python/rest_api/test_remote_url.py b/tests/python/rest_api/test_remote_url.py
index cc50a3284d58..c230aa3a9584 100644
--- a/tests/python/rest_api/test_remote_url.py
+++ b/tests/python/rest_api/test_remote_url.py
@@ -5,7 +5,7 @@
 
 from http import HTTPStatus
 from time import sleep
-from typing import Any, Dict
+from typing import Any
 
 import pytest
 
@@ -21,7 +21,7 @@ def _post_task_remote_data(username, task_id, resources):
     return post_method(username, f"tasks/{task_id}/data", data)
 
 
-def _wait_until_task_is_created(username: str, rq_id: str) -> Dict[str, Any]:
+def _wait_until_task_is_created(username: str, rq_id: str) -> dict[str, Any]:
     url = f"requests/{rq_id}"
 
     for _ in range(100):
diff --git a/tests/python/rest_api/test_requests.py b/tests/python/rest_api/test_requests.py
index f06e97ae7fba..a6f808f73056 100644
--- a/tests/python/rest_api/test_requests.py
+++ b/tests/python/rest_api/test_requests.py
@@ -4,7 +4,6 @@
 
 import io
 from http import HTTPStatus
-from typing import List
 from urllib.parse import urlparse
 
 import pytest
@@ -29,6 +28,7 @@
 
 @pytest.mark.usefixtures("restore_db_per_class")
 @pytest.mark.usefixtures("restore_redis_inmem_per_function")
+@pytest.mark.usefixtures("restore_redis_ondisk_per_function")
 @pytest.mark.timeout(30)
 class TestRequestsListFilters(CollectionSimpleFilterTestBase):
 
@@ -87,7 +87,7 @@ def fxt_make_requests(
         fxt_make_export_job_requests,
         fxt_download_file,
     ):
-        def _make_requests(project_ids: List[int], task_ids: List[int], job_ids: List[int]):
+        def _make_requests(project_ids: list[int], task_ids: list[int], job_ids: list[int]):
             # make requests to export projects|tasks|jobs annotations|datasets|backups
             fxt_make_export_project_requests(project_ids[1:])
             fxt_make_export_task_requests(task_ids[1:])
@@ -161,7 +161,7 @@ def download_file(resource: str, rid: int, subresource: str):
 
     @pytest.fixture
     def fxt_make_export_project_requests(self):
-        def make_requests(project_ids: List[int]):
+        def make_requests(project_ids: list[int]):
             for project_id in project_ids:
                 export_project_backup(
                     self.user, api_version=2, id=project_id, download_result=False
@@ -181,7 +181,7 @@ def make_requests(project_ids: List[int]):
 
     @pytest.fixture
     def fxt_make_export_task_requests(self):
-        def make_requests(task_ids: List[int]):
+        def make_requests(task_ids: list[int]):
             for task_id in task_ids:
                 export_task_backup(self.user, api_version=2, id=task_id, download_result=False)
                 export_task_dataset(
@@ -195,7 +195,7 @@ def make_requests(task_ids: List[int]):
 
     @pytest.fixture
     def fxt_make_export_job_requests(self):
-        def make_requests(job_ids: List[int]):
+        def make_requests(job_ids: list[int]):
             for job_id in job_ids:
                 export_job_dataset(
                     self.user,
@@ -230,7 +230,7 @@ def make_requests(job_ids: List[int]):
         ],
     )
     def test_can_use_simple_filter_for_object_list(
-        self, simple_filter: str, values: List, fxt_resources_ids, fxt_make_requests
+        self, simple_filter: str, values: list, fxt_resources_ids, fxt_make_requests
     ):
         project_ids, task_ids, job_ids = fxt_resources_ids
         fxt_make_requests(project_ids, task_ids, job_ids)
diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py
index c57dec13f639..a61683d981e2 100644
--- a/tests/python/rest_api/test_tasks.py
+++ b/tests/python/rest_api/test_tasks.py
@@ -14,6 +14,7 @@
 import zipfile
 from abc import ABCMeta, abstractmethod
 from collections import Counter
+from collections.abc import Generator, Iterable, Sequence
 from contextlib import closing
 from copy import deepcopy
 from datetime import datetime
@@ -26,20 +27,7 @@
 from pathlib import Path
 from tempfile import NamedTemporaryFile, TemporaryDirectory
 from time import sleep, time
-from typing import (
-    Any,
-    Callable,
-    ClassVar,
-    Dict,
-    Generator,
-    Iterable,
-    List,
-    Optional,
-    Sequence,
-    Set,
-    Tuple,
-    Union,
-)
+from typing import Any, Callable, ClassVar, Optional, Union
 
 import attrs
 import numpy as np
@@ -141,7 +129,6 @@ def _test_assigned_users_to_see_task_data(self, tasks, users, is_task_staff, **k
         "groups, is_staff, is_allow",
         [
             ("admin", False, True),
-            ("business", False, False),
         ],
     )
     def test_project_tasks_visibility(
@@ -350,7 +337,6 @@ def _test_users_to_create_task_in_project(
         "groups, is_staff, is_allow",
         [
             ("admin", False, True),
-            ("business", False, False),
             ("user", True, True),
         ],
     )
@@ -511,8 +497,6 @@ def get_data(tid):
         [
             ("admin", True, True),
             ("admin", False, True),
-            ("business", True, True),
-            ("business", False, False),
             ("worker", True, True),
             ("worker", False, False),
             ("user", True, True),
@@ -746,7 +730,7 @@ def _test_can_export_dataset(
         username: str,
         task_id: int,
         *,
-        api_version: Union[int, Tuple[int]],
+        api_version: Union[int, tuple[int]],
         local_download: bool = True,
         **kwargs,
     ) -> Optional[bytes]:
@@ -768,7 +752,7 @@ def test_can_export_task_dataset_locally_and_to_cloud_with_both_api_versions(
         admin_user,
         tasks_with_shapes,
         filter_tasks,
-        api_version: Tuple[int],
+        api_version: tuple[int],
         local_download: bool,
     ):
         filter_ = "target_storage__location"
@@ -1527,17 +1511,17 @@ def _create_task_with_cloud_data(
         request,
         cloud_storage: Any,
         use_manifest: bool,
-        server_files: List[str],
+        server_files: list[str],
         use_cache: bool = True,
         sorting_method: str = "lexicographical",
         data_type: str = "image",
         video_frame_count: int = 10,
-        server_files_exclude: Optional[List[str]] = None,
+        server_files_exclude: Optional[list[str]] = None,
         org: str = "",
-        filenames: Optional[List[str]] = None,
-        task_spec_kwargs: Optional[Dict[str, Any]] = None,
-        data_spec_kwargs: Optional[Dict[str, Any]] = None,
-    ) -> Tuple[int, Any]:
+        filenames: Optional[list[str]] = None,
+        task_spec_kwargs: Optional[dict[str, Any]] = None,
+        data_spec_kwargs: Optional[dict[str, Any]] = None,
+    ) -> tuple[int, Any]:
         s3_client = s3.make_client(bucket=cloud_storage["resource"])
         if data_type == "video":
             video = generate_video_file(video_frame_count)
@@ -1643,8 +1627,8 @@ def test_create_task_with_cloud_storage_directories_and_excluded_files(
         cloud_storage_id: int,
         use_cache: bool,
         use_manifest: bool,
-        server_files: List[str],
-        server_files_exclude: Optional[List[str]],
+        server_files: list[str],
+        server_files_exclude: Optional[list[str]],
         task_size: int,
         org: str,
         cloud_storages,
@@ -1690,8 +1674,8 @@ def test_create_task_with_cloud_storage_directories_and_predefined_sorting(
         self,
         cloud_storage_id: int,
         use_manifest: bool,
-        server_files: List[str],
-        expected_result: List[str],
+        server_files: list[str],
+        expected_result: list[str],
         org: str,
         cloud_storages,
         request,
@@ -1933,7 +1917,7 @@ def test_create_task_with_cloud_storage_and_retrieve_data(
     )
     def test_create_task_with_cloud_storage_and_check_data_sorting(
         self,
-        filenames: List[str],
+        filenames: list[str],
         sorting_method: str,
         cloud_storage_id: int,
         org: str,
@@ -2024,7 +2008,7 @@ def test_can_specify_file_job_mapping(self):
         )
 
         with make_api_client(self._USERNAME) as api_client:
-            jobs: List[models.JobRead] = get_paginated_collection(
+            jobs: list[models.JobRead] = get_paginated_collection(
                 api_client.jobs_api.list_endpoint, task_id=task_id, sort="id"
             )
             (task_meta, _) = api_client.tasks_api.retrieve_data_meta(id=task_id)
@@ -2088,7 +2072,7 @@ def test_create_task_with_cloud_storage_directories_and_default_bucket_prefix(
         self,
         cloud_storage_id: int,
         use_manifest: bool,
-        server_files: List[str],
+        server_files: list[str],
         default_prefix: str,
         expected_task_size: int,
         org: str,
@@ -2132,7 +2116,7 @@ def test_can_create_task_with_honeypots(
         self,
         fxt_test_name,
         frame_selection_method: str,
-        method_params: Set[str],
+        method_params: set[str],
         per_job_count_param: str,
     ):
         base_segment_size = 4
@@ -2316,7 +2300,7 @@ def test_can_create_task_with_gt_job_from_images(
         self,
         request: pytest.FixtureRequest,
         frame_selection_method: str,
-        method_params: Set[str],
+        method_params: set[str],
     ):
         segment_size = 4
         total_frame_count = 15
@@ -2449,7 +2433,7 @@ def test_can_create_task_with_gt_job_from_video(
         self,
         request: pytest.FixtureRequest,
         frame_selection_method: str,
-        method_params: Set[str],
+        method_params: set[str],
     ):
         segment_size = 4
         total_frame_count = 15
@@ -2650,8 +2634,8 @@ def read_frame(self, i: int) -> Image.Image: ...
 
 @attrs.define
 class _TaskSpecBase(_TaskSpec):
-    _params: Union[Dict, models.TaskWriteRequest]
-    _data_params: Union[Dict, models.DataRequest]
+    _params: Union[dict, models.TaskWriteRequest]
+    _data_params: Union[dict, models.DataRequest]
     size: int = attrs.field(kw_only=True)
 
     @property
@@ -2715,7 +2699,7 @@ def _uploaded_images_task_fxt_base(
         step: Optional[int] = None,
         segment_size: Optional[int] = None,
         **data_kwargs,
-    ) -> Generator[Tuple[_ImagesTaskSpec, int], None, None]:
+    ) -> Generator[tuple[_ImagesTaskSpec, int], None, None]:
         task_params = {
             "name": f"{request.node.name}[{request.fixturename}]",
             "labels": [{"name": "a"}],
@@ -2768,13 +2752,13 @@ def get_frame(i: int) -> bytes:
     @pytest.fixture(scope="class")
     def fxt_uploaded_images_task(
         self, request: pytest.FixtureRequest
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_images_task_fxt_base(request=request)
 
     @pytest.fixture(scope="class")
     def fxt_uploaded_images_task_with_segments(
         self, request: pytest.FixtureRequest
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_images_task_fxt_base(request=request, segment_size=4)
 
     @fixture(scope="class")
@@ -2783,7 +2767,7 @@ def fxt_uploaded_images_task_with_segments(
     @parametrize("start_frame", [3, 7])
     def fxt_uploaded_images_task_with_segments_start_stop_step(
         self, request: pytest.FixtureRequest, start_frame: int, stop_frame: Optional[int], step: int
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_images_task_fxt_base(
             request=request,
             frame_count=30,
@@ -2800,7 +2784,7 @@ def _uploaded_images_task_with_honeypots_and_segments_base(
         start_frame: Optional[int] = None,
         step: Optional[int] = None,
         random_seed: int = 42,
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         validation_params = models.DataRequestValidationParams._from_openapi_data(
             mode="gt_pool",
             frame_selection_method="random_uniform",
@@ -2862,14 +2846,14 @@ def _uploaded_images_task_with_honeypots_and_segments_base(
     @fixture(scope="class")
     def fxt_uploaded_images_task_with_honeypots_and_segments(
         self, request: pytest.FixtureRequest
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_images_task_with_honeypots_and_segments_base(request)
 
     @fixture(scope="class")
     @parametrize("start_frame, step", [(2, 3)])
     def fxt_uploaded_images_task_with_honeypots_and_segments_start_step(
         self, request: pytest.FixtureRequest, start_frame: Optional[int], step: Optional[int]
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_images_task_with_honeypots_and_segments_base(
             request, start_frame=start_frame, step=step
         )
@@ -2878,7 +2862,7 @@ def fxt_uploaded_images_task_with_honeypots_and_segments_start_step(
     @parametrize("random_seed", [1, 2, 5])
     def fxt_uploaded_images_task_with_honeypots_and_changed_real_frames(
         self, request: pytest.FixtureRequest, random_seed: int
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         with closing(
             self._uploaded_images_task_with_honeypots_and_segments_base(
                 request, start_frame=2, step=3, random_seed=random_seed
@@ -2919,7 +2903,7 @@ def _uploaded_images_task_with_gt_and_segments_base(
         start_frame: Optional[int] = None,
         step: Optional[int] = None,
         frame_selection_method: str = "random_uniform",
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         used_frames_count = 16
         total_frame_count = (start_frame or 0) + used_frames_count * (step or 1)
         segment_size = 5
@@ -2979,7 +2963,7 @@ def fxt_uploaded_images_task_with_gt_and_segments_start_step(
         start_frame: Optional[int],
         step: Optional[int],
         frame_selection_method: str,
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_images_task_with_gt_and_segments_base(
             request,
             start_frame=start_frame,
@@ -2996,7 +2980,7 @@ def _uploaded_video_task_fxt_base(
         start_frame: Optional[int] = None,
         stop_frame: Optional[int] = None,
         step: Optional[int] = None,
-    ) -> Generator[Tuple[_VideoTaskSpec, int], None, None]:
+    ) -> Generator[tuple[_VideoTaskSpec, int], None, None]:
         task_params = {
             "name": f"{request.node.name}[{request.fixturename}]",
             "labels": [{"name": "a"}],
@@ -3040,13 +3024,13 @@ def get_video_file() -> io.BytesIO:
     def fxt_uploaded_video_task(
         self,
         request: pytest.FixtureRequest,
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_video_task_fxt_base(request=request)
 
     @pytest.fixture(scope="class")
     def fxt_uploaded_video_task_with_segments(
         self, request: pytest.FixtureRequest
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_video_task_fxt_base(request=request, segment_size=4)
 
     @fixture(scope="class")
@@ -3055,7 +3039,7 @@ def fxt_uploaded_video_task_with_segments(
     @parametrize("start_frame", [3, 7])
     def fxt_uploaded_video_task_with_segments_start_stop_step(
         self, request: pytest.FixtureRequest, start_frame: int, stop_frame: Optional[int], step: int
-    ) -> Generator[Tuple[_TaskSpec, int], None, None]:
+    ) -> Generator[tuple[_TaskSpec, int], None, None]:
         yield from self._uploaded_video_task_fxt_base(
             request=request,
             frame_count=30,
@@ -3065,7 +3049,7 @@ def fxt_uploaded_video_task_with_segments_start_stop_step(
             step=step,
         )
 
-    def _compute_annotation_segment_params(self, task_spec: _TaskSpec) -> List[Tuple[int, int]]:
+    def _compute_annotation_segment_params(self, task_spec: _TaskSpec) -> list[tuple[int, int]]:
         segment_params = []
         frame_step = task_spec.frame_step
         segment_size = getattr(task_spec, "segment_size", 0) or task_spec.size * frame_step
@@ -3589,7 +3573,7 @@ def get_expected_chunk_abs_frame_ids(chunk_id: int):
 
 @pytest.mark.usefixtures("restore_db_per_function")
 class TestPatchTaskLabel:
-    def _get_task_labels(self, pid, user, **kwargs) -> List[models.Label]:
+    def _get_task_labels(self, pid, user, **kwargs) -> list[models.Label]:
         kwargs.setdefault("return_json", True)
         with make_api_client(user) as api_client:
             return get_paginated_collection(
@@ -3863,7 +3847,7 @@ def setup(
         "local_download", (True, pytest.param(False, marks=pytest.mark.with_external_services))
     )
     def test_can_export_backup_with_both_api_versions(
-        self, filter_tasks, api_version: Tuple[int], local_download: bool
+        self, filter_tasks, api_version: tuple[int], local_download: bool
     ):
         task = filter_tasks(
             **{("exclude_" if local_download else "") + "target_storage__location": "cloud_storage"}
@@ -4046,7 +4030,7 @@ class TestWorkWithSimpleGtJobTasks:
     @fixture
     def fxt_task_with_gt_job(
         self, tasks, jobs, job_has_annotations
-    ) -> Generator[Dict[str, Any], None, None]:
+    ) -> Generator[dict[str, Any], None, None]:
         gt_job = next(
             j
             for j in jobs
@@ -4168,7 +4152,7 @@ class TestWorkWithHoneypotTasks:
     @fixture
     def fxt_task_with_honeypots(
         self, tasks, jobs, job_has_annotations
-    ) -> Generator[Dict[str, Any], None, None]:
+    ) -> Generator[dict[str, Any], None, None]:
         gt_job = next(
             j
             for j in jobs
@@ -4535,6 +4519,8 @@ def test_can_change_honeypot_frames_in_task_can_only_select_from_active_validati
     def test_can_change_honeypot_frames_in_annotation_jobs(
         self, admin_user, task, gt_job, annotation_jobs, frame_selection_method: str
     ):
+        _MAX_RANDOM_ATTEMPTS = 20  # This test can have random outcomes, it's expected
+
         assert gt_job["stop_frame"] - gt_job["start_frame"] + 1 >= 2
 
         with make_api_client(admin_user) as api_client:
@@ -4556,16 +4542,34 @@ def test_can_change_honeypot_frames_in_annotation_jobs(
 
                     params["honeypot_real_frames"] = requested_honeypot_real_frames
 
-                new_validation_layout = json.loads(
-                    api_client.jobs_api.partial_update_validation_layout(
-                        annotation_job["id"],
-                        patched_job_validation_layout_write_request=(
-                            models.PatchedJobValidationLayoutWriteRequest(**params)
-                        ),
-                    )[1].data
-                )
+                attempt = 0
+                while attempt < _MAX_RANDOM_ATTEMPTS:
+                    new_validation_layout = json.loads(
+                        api_client.jobs_api.partial_update_validation_layout(
+                            annotation_job["id"],
+                            patched_job_validation_layout_write_request=(
+                                models.PatchedJobValidationLayoutWriteRequest(**params)
+                            ),
+                        )[1].data
+                    )
+
+                    new_honeypot_real_frames = new_validation_layout["honeypot_real_frames"]
+
+                    if (
+                        frame_selection_method == "random_uniform"
+                        and new_honeypot_real_frames
+                        == old_validation_layout["honeypot_real_frames"]
+                    ):
+                        attempt += 1
+                        # The test is fully random, it's possible to get no changes in the updated
+                        # honeypots. Passing a random seed has little sense in this endpoint,
+                        # so we retry several times in such a case instead.
+                    else:
+                        break
 
-                new_honeypot_real_frames = new_validation_layout["honeypot_real_frames"]
+                if attempt >= _MAX_RANDOM_ATTEMPTS and frame_selection_method == "random_uniform":
+                    # The situation is unlikely if everything works, so we consider it a fail
+                    pytest.fail(f"too many attempts ({attempt}) with random honeypot updating")
 
                 assert old_validation_layout["honeypot_count"] == len(new_honeypot_real_frames)
                 assert all(f in gt_frame_set for f in new_honeypot_real_frames)
@@ -5251,7 +5255,7 @@ def setup_class(
         cls._init_tasks()
 
     @classmethod
-    def _create_task_with_annotations(cls, filenames: List[str]):
+    def _create_task_with_annotations(cls, filenames: list[str]):
         images = generate_image_files(len(filenames), filenames=filenames)
 
         source_archive_path = cls.tmp_dir / "source_data.zip"
diff --git a/tests/python/rest_api/test_webhooks.py b/tests/python/rest_api/test_webhooks.py
index 3c528bc78c15..778eda8430ed 100644
--- a/tests/python/rest_api/test_webhooks.py
+++ b/tests/python/rest_api/test_webhooks.py
@@ -96,7 +96,7 @@ def test_admin_can_create_webhook_for_project_in_org(
         assert response.status_code == HTTPStatus.CREATED
         assert "secret" not in response.json()
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_sandbox_project_owner_can_create_webhook_for_project(self, privilege, projects, users):
         users = [user for user in users if privilege in user["groups"]]
         username, project_id = next(
@@ -116,7 +116,7 @@ def test_sandbox_project_owner_can_create_webhook_for_project(self, privilege, p
         assert response.status_code == HTTPStatus.CREATED
         assert "secret" not in response.json()
 
-    @pytest.mark.parametrize("privilege", ["worker", "user", "business"])
+    @pytest.mark.parametrize("privilege", ["worker", "user"])
     def test_sandbox_project_assignee_cannot_create_webhook_for_project(
         self, privilege, projects, users
     ):
@@ -410,7 +410,7 @@ def test_admin_can_get_webhook(self, webhooks, users, projects):
         assert "secret" not in response.json()
         assert DeepDiff(webhooks[wid], response.json(), ignore_order=True) == {}
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_project_owner_can_get_webhook(self, privilege, webhooks, projects, users):
         proj_webhooks = [w for w in webhooks if w["type"] == "project"]
         username, wid = next(
@@ -418,7 +418,7 @@ def test_project_owner_can_get_webhook(self, privilege, webhooks, projects, user
                 (user["username"], webhook["id"])
                 for user in users
                 for webhook in proj_webhooks
-                if privilege not in user["groups"]
+                if privilege in user["groups"]
                 and projects[webhook["project_id"]]["owner"]["id"] == user["id"]
             )
         )
@@ -429,7 +429,7 @@ def test_project_owner_can_get_webhook(self, privilege, webhooks, projects, user
         assert "secret" not in response.json()
         assert DeepDiff(webhooks[wid], response.json(), ignore_order=True) == {}
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_webhook_owner_can_get_webhook(self, privilege, webhooks, projects, users):
         proj_webhooks = [w for w in webhooks if w["type"] == "project"]
         username, wid = next(
@@ -447,7 +447,7 @@ def test_webhook_owner_can_get_webhook(self, privilege, webhooks, projects, user
         assert "secret" not in response.json()
         assert DeepDiff(webhooks[wid], response.json(), ignore_order=True) == {}
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_not_project_staff_cannot_get_webhook(self, privilege, webhooks, projects, users):
         proj_webhooks = [w for w in webhooks if w["type"] == "project"]
         username, wid = next(
@@ -631,7 +631,7 @@ def test_admin_can_get_webhooks_for_project_in_org(self, webhooks):
         assert response.status_code == HTTPStatus.OK
         assert DeepDiff(expected_response, response.json()["results"], ignore_order=True) == {}
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_user_cannot_get_webhook_list_for_project(
         self, privilege, find_users, webhooks, projects
     ):
@@ -654,7 +654,7 @@ def test_user_cannot_get_webhook_list_for_project(
         assert response.status_code == HTTPStatus.OK
         assert DeepDiff([], response.json()["results"], ignore_order=True) == {}
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_user_can_get_webhook_list_for_project(self, privilege, find_users, webhooks, projects):
         username, pid = next(
             (
@@ -824,7 +824,7 @@ def test_cannot_update_with_nonexistent_contenttype(self):
         response = patch_method("admin2", f"webhooks/{self.WID}", patch_data)
         assert response.status_code == HTTPStatus.BAD_REQUEST
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_sandbox_user_can_update_webhook(self, privilege, find_users, webhooks):
         username, webhook = next(
             (
@@ -852,7 +852,7 @@ def test_sandbox_user_can_update_webhook(self, privilege, find_users, webhooks):
             == {}
         )
 
-    @pytest.mark.parametrize("privilege", ["worker", "user", "business"])
+    @pytest.mark.parametrize("privilege", ["worker", "user"])
     def test_sandbox_user_cannot_update_webhook(self, privilege, find_users, webhooks):
         username, webhook = next(
             (
@@ -1029,9 +1029,7 @@ def test_member_can_update_project_webhook_in_org(
 
 @pytest.mark.usefixtures("restore_db_per_function")
 class TestDeleteWebhooks:
-    @pytest.mark.parametrize(
-        "privilege, allow", [("user", False), ("business", False), ("admin", True)]
-    )
+    @pytest.mark.parametrize("privilege, allow", [("user", False), ("admin", True)])
     def test_user_can_delete_project_webhook(
         self, privilege, allow, find_users, webhooks, projects
     ):
@@ -1101,7 +1099,7 @@ def test_admin_can_delete_org_webhook(self, find_users, webhooks, is_org_member)
         response = get_method(username, f"webhooks/{webhook_id}")
         assert response.status_code == HTTPStatus.NOT_FOUND
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_project_owner_can_delete_project_webhook(
         self, privilege, find_users, webhooks, projects
     ):
@@ -1123,7 +1121,7 @@ def test_project_owner_can_delete_project_webhook(
         response = get_method(username, f"webhooks/{webhook_id}")
         assert response.status_code == HTTPStatus.NOT_FOUND
 
-    @pytest.mark.parametrize("privilege", ["user", "business"])
+    @pytest.mark.parametrize("privilege", ["user"])
     def test_webhook_owner_can_delete_project_webhook(
         self, privilege, find_users, webhooks, projects
     ):
diff --git a/tests/python/rest_api/utils.py b/tests/python/rest_api/utils.py
index 0552efc737f4..434c3705ddc3 100644
--- a/tests/python/rest_api/utils.py
+++ b/tests/python/rest_api/utils.py
@@ -4,10 +4,11 @@
 
 import json
 from abc import ABCMeta, abstractmethod
+from collections.abc import Iterator, Sequence
 from copy import deepcopy
 from http import HTTPStatus
 from time import sleep
-from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple, Union
+from typing import Any, Callable, Optional, Union
 
 import requests
 from cvat_sdk.api_client import apis, models
@@ -191,7 +192,7 @@ def export_v2(
 def export_dataset(
     api: Union[ProjectsApi, TasksApi, JobsApi],
     api_version: Union[
-        int, Tuple[int]
+        int, tuple[int]
     ],  # make this parameter required to be sure that all tests was updated and both API versions are used
     *,
     save_images: bool,
@@ -251,21 +252,21 @@ def _get_endpoint_and_kwargs(version: int) -> Endpoint:
 
 # FUTURE-TODO: support username: optional, api_client: optional
 def export_project_dataset(
-    username: str, api_version: Union[int, Tuple[int]], *args, **kwargs
+    username: str, api_version: Union[int, tuple[int]], *args, **kwargs
 ) -> Optional[bytes]:
     with make_api_client(username) as api_client:
         return export_dataset(api_client.projects_api, api_version, *args, **kwargs)
 
 
 def export_task_dataset(
-    username: str, api_version: Union[int, Tuple[int]], *args, **kwargs
+    username: str, api_version: Union[int, tuple[int]], *args, **kwargs
 ) -> Optional[bytes]:
     with make_api_client(username) as api_client:
         return export_dataset(api_client.tasks_api, api_version, *args, **kwargs)
 
 
 def export_job_dataset(
-    username: str, api_version: Union[int, Tuple[int]], *args, **kwargs
+    username: str, api_version: Union[int, tuple[int]], *args, **kwargs
 ) -> Optional[bytes]:
     with make_api_client(username) as api_client:
         return export_dataset(api_client.jobs_api, api_version, *args, **kwargs)
@@ -274,7 +275,7 @@ def export_job_dataset(
 def export_backup(
     api: Union[ProjectsApi, TasksApi],
     api_version: Union[
-        int, Tuple[int]
+        int, tuple[int]
     ],  # make this parameter required to be sure that all tests was updated and both API versions are used
     *,
     max_retries: int = 30,
@@ -309,14 +310,14 @@ def export_backup(
 
 
 def export_project_backup(
-    username: str, api_version: Union[int, Tuple[int]], *args, **kwargs
+    username: str, api_version: Union[int, tuple[int]], *args, **kwargs
 ) -> Optional[bytes]:
     with make_api_client(username) as api_client:
         return export_backup(api_client.projects_api, api_version, *args, **kwargs)
 
 
 def export_task_backup(
-    username: str, api_version: Union[int, Tuple[int]], *args, **kwargs
+    username: str, api_version: Union[int, tuple[int]], *args, **kwargs
 ) -> Optional[bytes]:
     with make_api_client(username) as api_client:
         return export_backup(api_client.tasks_api, api_version, *args, **kwargs)
@@ -379,12 +380,12 @@ def import_backup(
     return import_resource(endpoint, max_retries=max_retries, interval=interval, **kwargs)
 
 
-def import_project_backup(username: str, data: Dict, **kwargs) -> None:
+def import_project_backup(username: str, data: dict, **kwargs) -> None:
     with make_api_client(username) as api_client:
         return import_backup(api_client.projects_api, project_file_request=deepcopy(data), **kwargs)
 
 
-def import_task_backup(username: str, data: Dict, **kwargs) -> None:
+def import_task_backup(username: str, data: dict, **kwargs) -> None:
     with make_api_client(username) as api_client:
         return import_backup(api_client.tasks_api, task_file_request=deepcopy(data), **kwargs)
 
@@ -395,20 +396,20 @@ def import_task_backup(username: str, data: Dict, **kwargs) -> None:
 class CollectionSimpleFilterTestBase(metaclass=ABCMeta):
     # These fields need to be defined in the subclass
     user: str
-    samples: List[Dict[str, Any]]
-    field_lookups: Dict[str, FieldPath] = None
-    cmp_ignore_keys: List[str] = ["updated_date"]
+    samples: list[dict[str, Any]]
+    field_lookups: dict[str, FieldPath] = None
+    cmp_ignore_keys: list[str] = ["updated_date"]
 
     @abstractmethod
     def _get_endpoint(self, api_client: ApiClient) -> Endpoint: ...
 
-    def _retrieve_collection(self, **kwargs) -> List:
+    def _retrieve_collection(self, **kwargs) -> list:
         kwargs["return_json"] = True
         with make_api_client(self.user) as api_client:
             return get_paginated_collection(self._get_endpoint(api_client), **kwargs)
 
     @classmethod
-    def _get_field(cls, d: Dict[str, Any], path: Union[str, FieldPath]) -> Optional[Any]:
+    def _get_field(cls, d: dict[str, Any], path: Union[str, FieldPath]) -> Optional[Any]:
         assert path
         for key in path:
             if isinstance(d, dict):
@@ -428,7 +429,7 @@ def _map_field(self, name: str) -> FieldPath:
 
     @classmethod
     def _find_valid_field_value(
-        cls, samples: Iterator[Dict[str, Any]], field_path: FieldPath
+        cls, samples: Iterator[dict[str, Any]], field_path: FieldPath
     ) -> Any:
         value = None
         for sample in samples:
@@ -439,7 +440,7 @@ def _find_valid_field_value(
         assert value, f"Failed to find a sample for the '{'.'.join(field_path)}' field"
         return value
 
-    def _get_field_samples(self, field: str) -> Tuple[Any, List[Dict[str, Any]]]:
+    def _get_field_samples(self, field: str) -> tuple[Any, list[dict[str, Any]]]:
         field_path = self._map_field(field)
         field_value = self._find_valid_field_value(self.samples, field_path)
 
@@ -463,7 +464,7 @@ def _compare_results(self, gt_objects, received_objects):
         assert diff == {}, diff
 
     def _test_can_use_simple_filter_for_object_list(
-        self, field: str, field_values: Optional[List[Any]] = None
+        self, field: str, field_values: Optional[list[Any]] = None
     ):
         gt_objects = []
         field_path = self._map_field(field)
@@ -485,12 +486,12 @@ def _test_can_use_simple_filter_for_object_list(
             self._compare_results(gt_objects, received_items)
 
 
-def get_attrs(obj: Any, attributes: Sequence[str]) -> Tuple[Any, ...]:
+def get_attrs(obj: Any, attributes: Sequence[str]) -> tuple[Any, ...]:
     """Returns 1 or more object attributes as a tuple"""
     return (getattr(obj, attr) for attr in attributes)
 
 
-def build_exclude_paths_expr(ignore_fields: Iterator[str]) -> List[str]:
+def build_exclude_paths_expr(ignore_fields: Iterator[str]) -> list[str]:
     exclude_expr_parts = []
     for key in ignore_fields:
         if "." in key:
diff --git a/tests/python/sdk/fixtures.py b/tests/python/sdk/fixtures.py
index fa1051141a33..f495c4ce8ef4 100644
--- a/tests/python/sdk/fixtures.py
+++ b/tests/python/sdk/fixtures.py
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: MIT
 
 from pathlib import Path
-from typing import Tuple
 from zipfile import ZipFile
 
 import pytest
@@ -72,7 +71,7 @@ def fxt_coco_dataset(tmp_path: Path, fxt_image_file: Path, fxt_coco_file: Path):
 
 
 @pytest.fixture
-def fxt_new_task(fxt_image_file: Path, fxt_login: Tuple[Client, str]):
+def fxt_new_task(fxt_image_file: Path, fxt_login: tuple[Client, str]):
     client, _ = fxt_login
     task = client.tasks.create_from_data(
         spec={
@@ -87,7 +86,7 @@ def fxt_new_task(fxt_image_file: Path, fxt_login: Tuple[Client, str]):
 
 
 @pytest.fixture
-def fxt_new_task_with_target_storage(fxt_image_file: Path, fxt_login: Tuple[Client, str]):
+def fxt_new_task_with_target_storage(fxt_image_file: Path, fxt_login: tuple[Client, str]):
     client, _ = fxt_login
     task = client.tasks.create_from_data(
         spec={
diff --git a/tests/python/sdk/test_auto_annotation.py b/tests/python/sdk/test_auto_annotation.py
index e7ac8418b69a..ae4a0d711774 100644
--- a/tests/python/sdk/test_auto_annotation.py
+++ b/tests/python/sdk/test_auto_annotation.py
@@ -6,7 +6,6 @@
 from logging import Logger
 from pathlib import Path
 from types import SimpleNamespace as namespace
-from typing import List, Tuple
 
 import cvat_sdk.auto_annotation as cvataa
 import PIL.Image
@@ -27,8 +26,8 @@
 @pytest.fixture(autouse=True)
 def _common_setup(
     tmp_path: Path,
-    fxt_login: Tuple[Client, str],
-    fxt_logger: Tuple[Logger, io.StringIO],
+    fxt_login: tuple[Client, str],
+    fxt_logger: tuple[Logger, io.StringIO],
     restore_redis_ondisk_per_function,
 ):
     logger = fxt_logger[0]
@@ -46,7 +45,7 @@ class TestTaskAutoAnnotation:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
+        fxt_login: tuple[Client, str],
     ):
         self.client = fxt_login[0]
         self.images = [
@@ -114,7 +113,7 @@ def test_detection_rectangle(self):
 
         def detect(
             context: cvataa.DetectionFunctionContext, image: PIL.Image.Image
-        ) -> List[models.LabeledShapeRequest]:
+        ) -> list[models.LabeledShapeRequest]:
             assert context.frame_name in {"1.png", "2.png"}
             assert image.width == image.height == 333
             return [
@@ -168,7 +167,7 @@ def test_detection_skeleton(self):
             ],
         )
 
-        def detect(context, image: PIL.Image.Image) -> List[models.LabeledShapeRequest]:
+        def detect(context, image: PIL.Image.Image) -> list[models.LabeledShapeRequest]:
             assert image.width == image.height == 333
             return [
                 cvataa.skeleton(
@@ -241,7 +240,7 @@ def test_detection_without_clearing(self):
             ],
         )
 
-        def detect(context, image: PIL.Image.Image) -> List[models.LabeledShapeRequest]:
+        def detect(context, image: PIL.Image.Image) -> list[models.LabeledShapeRequest]:
             return [
                 cvataa.rectangle(
                     123,  # car
@@ -570,7 +569,7 @@ def __init__(self, label_id: int) -> None:
             super().__init__()
             self._label_id = label_id
 
-        def forward(self, images: List[torch.Tensor]) -> List[dict]:
+        def forward(self, images: list[torch.Tensor]) -> list[dict]:
             assert isinstance(images, list)
             assert all(isinstance(t, torch.Tensor) for t in images)
 
@@ -589,12 +588,12 @@ def fake_get_detection_model(name: str, weights, test_param):
         return FakeTorchvisionDetector(label_id=car_label_id)
 
     class FakeTorchvisionKeypointDetector(nn.Module):
-        def __init__(self, label_id: int, keypoint_names: List[str]) -> None:
+        def __init__(self, label_id: int, keypoint_names: list[str]) -> None:
             super().__init__()
             self._label_id = label_id
             self._keypoint_names = keypoint_names
 
-        def forward(self, images: List[torch.Tensor]) -> List[dict]:
+        def forward(self, images: list[torch.Tensor]) -> list[dict]:
             assert isinstance(images, list)
             assert all(isinstance(t, torch.Tensor) for t in images)
 
@@ -628,7 +627,7 @@ class TestAutoAnnotationFunctions:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
+        fxt_login: tuple[Client, str],
     ):
         self.client = fxt_login[0]
         self.image = generate_image_file("1.png", size=(100, 100))
diff --git a/tests/python/sdk/test_client.py b/tests/python/sdk/test_client.py
index 7554e8f5f2d8..38609176222c 100644
--- a/tests/python/sdk/test_client.py
+++ b/tests/python/sdk/test_client.py
@@ -5,7 +5,6 @@
 import io
 from contextlib import ExitStack
 from logging import Logger
-from typing import List, Tuple
 
 import packaging.version as pv
 import pytest
@@ -22,7 +21,7 @@ class TestClientUsecases:
     def setup(
         self,
         restore_db_per_function,  # force fixture call order to allow DB setup
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_client: Client,
         fxt_stdout: io.StringIO,
         admin_user: str,
@@ -95,7 +94,7 @@ def test_can_reject_invalid_server_schema():
 
 @pytest.mark.parametrize("raise_exception", (True, False))
 def test_can_warn_on_mismatching_server_version(
-    fxt_logger: Tuple[Logger, io.StringIO], monkeypatch, raise_exception: bool
+    fxt_logger: tuple[Logger, io.StringIO], monkeypatch, raise_exception: bool
 ):
     logger, logger_stream = fxt_logger
 
@@ -118,7 +117,7 @@ def mocked_version(_):
 
 @pytest.mark.parametrize("do_check", (True, False))
 def test_can_check_server_version_in_ctor(
-    fxt_logger: Tuple[Logger, io.StringIO], monkeypatch, do_check: bool
+    fxt_logger: tuple[Logger, io.StringIO], monkeypatch, do_check: bool
 ):
     logger, logger_stream = fxt_logger
 
@@ -141,7 +140,7 @@ def mocked_version(_):
     ) == do_check
 
 
-def test_can_check_server_version_in_method(fxt_logger: Tuple[Logger, io.StringIO], monkeypatch):
+def test_can_check_server_version_in_method(fxt_logger: tuple[Logger, io.StringIO], monkeypatch):
     logger, logger_stream = fxt_logger
 
     def mocked_version(_):
@@ -183,10 +182,10 @@ def mocked_version(_):
     ],
 )
 def test_can_check_server_version_compatibility(
-    fxt_logger: Tuple[Logger, io.StringIO],
+    fxt_logger: tuple[Logger, io.StringIO],
     monkeypatch: pytest.MonkeyPatch,
     server_version: str,
-    supported_versions: List[str],
+    supported_versions: list[str],
     expect_supported: bool,
 ):
     logger, _ = fxt_logger
diff --git a/tests/python/sdk/test_datasets.py b/tests/python/sdk/test_datasets.py
index 542ad9a1e80c..525082d0eae3 100644
--- a/tests/python/sdk/test_datasets.py
+++ b/tests/python/sdk/test_datasets.py
@@ -5,7 +5,6 @@
 import io
 from logging import Logger
 from pathlib import Path
-from typing import Tuple
 
 import cvat_sdk.datasets as cvatds
 import PIL.Image
@@ -21,8 +20,8 @@
 @pytest.fixture(autouse=True)
 def _common_setup(
     tmp_path: Path,
-    fxt_login: Tuple[Client, str],
-    fxt_logger: Tuple[Logger, io.StringIO],
+    fxt_login: tuple[Client, str],
+    fxt_logger: tuple[Logger, io.StringIO],
     restore_redis_ondisk_per_function,
 ):
     logger = fxt_logger[0]
@@ -40,7 +39,7 @@ class TestTaskDataset:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
+        fxt_login: tuple[Client, str],
     ):
         self.client = fxt_login[0]
         self.images = generate_image_files(10)
diff --git a/tests/python/sdk/test_issues_comments.py b/tests/python/sdk/test_issues_comments.py
index 12047c75a1f0..f90b663fbefe 100644
--- a/tests/python/sdk/test_issues_comments.py
+++ b/tests/python/sdk/test_issues_comments.py
@@ -5,7 +5,6 @@
 import io
 from logging import Logger
 from pathlib import Path
-from typing import Tuple
 
 import pytest
 from cvat_sdk import Client
@@ -18,8 +17,8 @@ class TestIssuesUsecases:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_login: tuple[Client, str],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_stdout: io.StringIO,
     ):
         self.tmp_path = tmp_path
@@ -139,8 +138,8 @@ class TestCommentsUsecases:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_login: tuple[Client, str],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_stdout: io.StringIO,
     ):
         self.tmp_path = tmp_path
diff --git a/tests/python/sdk/test_jobs.py b/tests/python/sdk/test_jobs.py
index 3202e2957ff0..3d49978d5da5 100644
--- a/tests/python/sdk/test_jobs.py
+++ b/tests/python/sdk/test_jobs.py
@@ -5,7 +5,7 @@
 import io
 from logging import Logger
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
 import pytest
 from cvat_sdk import Client
@@ -26,8 +26,8 @@ class TestJobUsecases(TestDatasetExport):
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_login: tuple[Client, str],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_stdout: io.StringIO,
         restore_redis_ondisk_per_function,
     ):
diff --git a/tests/python/sdk/test_organizations.py b/tests/python/sdk/test_organizations.py
index 84198c73b24f..54f9798cc849 100644
--- a/tests/python/sdk/test_organizations.py
+++ b/tests/python/sdk/test_organizations.py
@@ -4,7 +4,6 @@
 
 import io
 from logging import Logger
-from typing import Tuple
 
 import pytest
 from cvat_sdk import Client, models
@@ -16,8 +15,8 @@ class TestOrganizationUsecases:
     @pytest.fixture(autouse=True)
     def setup(
         self,
-        fxt_login: Tuple[Client, str],
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_login: tuple[Client, str],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_stdout: io.StringIO,
     ):
         logger, self.logger_stream = fxt_logger
diff --git a/tests/python/sdk/test_projects.py b/tests/python/sdk/test_projects.py
index b03df660d87a..db0b5f265586 100644
--- a/tests/python/sdk/test_projects.py
+++ b/tests/python/sdk/test_projects.py
@@ -5,7 +5,7 @@
 import io
 from logging import Logger
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
 import pytest
 from cvat_sdk import Client, models
@@ -29,8 +29,8 @@ class TestProjectUsecases(TestDatasetExport):
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_login: tuple[Client, str],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_stdout: io.StringIO,
         restore_redis_ondisk_per_function,
     ):
diff --git a/tests/python/sdk/test_pytorch.py b/tests/python/sdk/test_pytorch.py
index 2bcbd122abff..8e6918abf301 100644
--- a/tests/python/sdk/test_pytorch.py
+++ b/tests/python/sdk/test_pytorch.py
@@ -7,7 +7,6 @@
 import os
 from logging import Logger
 from pathlib import Path
-from typing import Tuple
 
 import pytest
 from cvat_sdk import Client, models
@@ -34,8 +33,8 @@
 @pytest.fixture(autouse=True)
 def _common_setup(
     tmp_path: Path,
-    fxt_login: Tuple[Client, str],
-    fxt_logger: Tuple[Logger, io.StringIO],
+    fxt_login: tuple[Client, str],
+    fxt_logger: tuple[Logger, io.StringIO],
     restore_redis_ondisk_per_function,
 ):
     logger = fxt_logger[0]
@@ -54,7 +53,7 @@ class TestTaskVisionDataset:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
+        fxt_login: tuple[Client, str],
     ):
         self.client = fxt_login[0]
         self.images = generate_image_files(10)
@@ -298,7 +297,7 @@ class TestProjectVisionDataset:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
+        fxt_login: tuple[Client, str],
     ):
         self.client = fxt_login[0]
 
diff --git a/tests/python/sdk/test_tasks.py b/tests/python/sdk/test_tasks.py
index 54e0823d3311..0181d5c74d3b 100644
--- a/tests/python/sdk/test_tasks.py
+++ b/tests/python/sdk/test_tasks.py
@@ -7,7 +7,7 @@
 import zipfile
 from logging import Logger
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Optional
 
 import pytest
 from cvat_sdk import Client, models
@@ -30,8 +30,8 @@ class TestTaskUsecases(TestDatasetExport):
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_login: tuple[Client, str],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_stdout: io.StringIO,
         restore_redis_ondisk_per_function,
     ):
diff --git a/tests/python/sdk/test_users.py b/tests/python/sdk/test_users.py
index 94c61adac3db..fbdc675e5dd2 100644
--- a/tests/python/sdk/test_users.py
+++ b/tests/python/sdk/test_users.py
@@ -5,7 +5,6 @@
 import io
 from logging import Logger
 from pathlib import Path
-from typing import Tuple
 
 import pytest
 from cvat_sdk import Client, models
@@ -17,8 +16,8 @@ class TestUserUsecases:
     def setup(
         self,
         tmp_path: Path,
-        fxt_login: Tuple[Client, str],
-        fxt_logger: Tuple[Logger, io.StringIO],
+        fxt_login: tuple[Client, str],
+        fxt_logger: tuple[Logger, io.StringIO],
         fxt_stdout: io.StringIO,
     ):
         self.tmp_path = tmp_path
diff --git a/tests/python/sdk/util.py b/tests/python/sdk/util.py
index 1686330ad9f1..4384a88d418d 100644
--- a/tests/python/sdk/util.py
+++ b/tests/python/sdk/util.py
@@ -3,8 +3,8 @@
 # SPDX-License-Identifier: MIT
 
 import textwrap
+from collections.abc import Container
 from pathlib import Path
-from typing import Container, Tuple
 from urllib.parse import urlparse
 
 import pytest
@@ -16,7 +16,7 @@ def make_pbar(file, **kwargs):
     return DeferredTqdmProgressReporter({"file": file, "mininterval": 0, **kwargs})
 
 
-def generate_coco_json(filename: Path, img_info: Tuple[Path, int, int]):
+def generate_coco_json(filename: Path, img_info: tuple[Path, int, int]):
     image_filename, image_width, image_height = img_info
 
     content = generate_coco_anno(
diff --git a/tests/python/shared/assets/cloudstorages.json b/tests/python/shared/assets/cloudstorages.json
index 4cda853ec930..8d8d92009aad 100644
--- a/tests/python/shared/assets/cloudstorages.json
+++ b/tests/python/shared/assets/cloudstorages.json
@@ -36,11 +36,11 @@
       ],
       "organization": 2,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       },
       "provider_type": "AWS_S3_BUCKET",
       "resource": "private",
diff --git a/tests/python/shared/assets/comments.json b/tests/python/shared/assets/comments.json
index f1f7457eae75..4681af9bd7dc 100644
--- a/tests/python/shared/assets/comments.json
+++ b/tests/python/shared/assets/comments.json
@@ -37,11 +37,11 @@
       "issue": 3,
       "message": "Another one issue",
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       },
       "updated_date": "2022-03-16T11:08:18.370000Z"
     },
@@ -51,11 +51,11 @@
       "issue": 2,
       "message": "Something should be here",
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       },
       "updated_date": "2022-03-16T11:07:22.173000Z"
     },
diff --git a/tests/python/shared/assets/cvat_db/data.json b/tests/python/shared/assets/cvat_db/data.json
index 7bb20c4507c8..5b30d421cb5a 100644
--- a/tests/python/shared/assets/cvat_db/data.json
+++ b/tests/python/shared/assets/cvat_db/data.json
@@ -10,14 +10,6 @@
 {
   "model": "auth.group",
   "pk": 2,
-  "fields": {
-    "name": "business",
-    "permissions": []
-  }
-},
-{
-  "model": "auth.group",
-  "pk": 3,
   "fields": {
     "name": "user",
     "permissions": []
@@ -25,7 +17,7 @@
 },
 {
   "model": "auth.group",
-  "pk": 4,
+  "pk": 3,
   "fields": {
     "name": "worker",
     "permissions": []
@@ -236,16 +228,16 @@
     "password": "md5$6TyZJsUJ2hAbICwZHKp4p0$961841748b31d28bcaf3094e549d2bd5",
     "last_login": "2022-09-28T12:17:51.373Z",
     "is_superuser": false,
-    "username": "business1",
-    "first_name": "Business",
-    "last_name": "First",
-    "email": "business1@cvat.org",
+    "username": "user7",
+    "first_name": "User",
+    "last_name": "Seventh",
+    "email": "user7@cvat.org",
     "is_staff": false,
     "is_active": true,
     "date_joined": "2021-12-14T18:33:06Z",
     "groups": [
       [
-        "business"
+        "user"
       ]
     ],
     "user_permissions": []
@@ -258,16 +250,16 @@
     "password": "md5$oLNLFFMdjViRqnAw1th3Zl$d816d16307053866451da43fb4443b66",
     "last_login": "2022-03-17T07:22:55.930Z",
     "is_superuser": false,
-    "username": "business2",
-    "first_name": "Business",
-    "last_name": "Second",
-    "email": "business2@cvat.org",
+    "username": "user8",
+    "first_name": "User",
+    "last_name": "Eighth",
+    "email": "user8@cvat.org",
     "is_staff": false,
     "is_active": true,
     "date_joined": "2021-12-14T18:34:01Z",
     "groups": [
       [
-        "business"
+        "user"
       ]
     ],
     "user_permissions": []
@@ -280,16 +272,16 @@
     "password": "md5$7ETBhORLrHl45WPL9CkxnN$af77496152b60ffc73ef877c99807385",
     "last_login": null,
     "is_superuser": false,
-    "username": "business3",
-    "first_name": "Business",
-    "last_name": "Third",
-    "email": "business3@cvat.org",
+    "username": "user9",
+    "first_name": "User",
+    "last_name": "Nineth",
+    "email": "user9@cvat.org",
     "is_staff": false,
     "is_active": true,
     "date_joined": "2021-12-14T18:34:34Z",
     "groups": [
       [
-        "business"
+        "user"
       ]
     ],
     "user_permissions": []
@@ -302,16 +294,16 @@
     "password": "md5$9huaZ72ncQGfmxUqU3Hwnz$6b010216eea87409f0aca7126bd80bbd",
     "last_login": null,
     "is_superuser": false,
-    "username": "business4",
-    "first_name": "Business",
-    "last_name": "Fourth",
-    "email": "business4@cvat.org",
+    "username": "user10",
+    "first_name": "User",
+    "last_name": "Tenth",
+    "email": "user10@cvat.org",
     "is_staff": false,
     "is_active": true,
     "date_joined": "2021-12-14T18:35:15Z",
     "groups": [
       [
-        "business"
+        "user"
       ]
     ],
     "user_permissions": []
@@ -734,7 +726,7 @@
   "pk": "53da3ff9e514d84b56b5170059ff0f595c34157b",
   "fields": {
     "user": [
-      "business2"
+      "user8"
     ],
     "created": "2022-03-17T07:22:55.921Z"
   }
@@ -754,7 +746,7 @@
   "pk": "c051fe19df24a0ac4c6bec5e635034271c9549dc",
   "fields": {
     "user": [
-      "business1"
+      "user7"
     ],
     "created": "2023-05-01T08:42:48.127Z"
   }
@@ -833,7 +825,7 @@
       "email": "org2@cvat.org"
     },
     "owner": [
-      "business1"
+      "user7"
     ]
   }
 },
@@ -881,7 +873,7 @@
   "pk": 4,
   "fields": {
     "user": [
-      "business1"
+      "user7"
     ],
     "organization": 1,
     "is_active": true,
@@ -894,7 +886,7 @@
   "pk": 5,
   "fields": {
     "user": [
-      "business1"
+      "user7"
     ],
     "organization": 2,
     "is_active": true,
@@ -907,7 +899,7 @@
   "pk": 6,
   "fields": {
     "user": [
-      "business2"
+      "user8"
     ],
     "organization": 2,
     "is_active": true,
@@ -1024,7 +1016,7 @@
   "pk": 15,
   "fields": {
     "user": [
-      "business2"
+      "user8"
     ],
     "organization": 1,
     "is_active": true,
@@ -1039,7 +1031,7 @@
     "created_date": "2022-01-19T13:54:42.005Z",
     "sent_date": "2022-01-19T13:54:42.005Z",
     "owner": [
-      "business1"
+      "user7"
     ],
     "membership": 10
   }
@@ -1051,7 +1043,7 @@
     "created_date": "2021-12-14T19:54:46.172Z",
     "sent_date": "2021-12-14T19:54:46.172Z",
     "owner": [
-      "business1"
+      "user7"
     ],
     "membership": 7
   }
@@ -1063,7 +1055,7 @@
     "created_date": "2022-01-19T13:54:42.015Z",
     "sent_date": "2022-01-19T13:54:42.015Z",
     "owner": [
-      "business1"
+      "user7"
     ],
     "membership": 11
   }
@@ -1099,7 +1091,7 @@
     "created_date": "2021-12-14T19:54:33.591Z",
     "sent_date": "2021-12-14T19:54:33.591Z",
     "owner": [
-      "business1"
+      "user7"
     ],
     "membership": 6
   }
@@ -1147,7 +1139,7 @@
     "created_date": "2021-12-14T19:55:13.745Z",
     "sent_date": "2021-12-14T19:55:13.745Z",
     "owner": [
-      "business1"
+      "user7"
     ],
     "membership": 9
   }
@@ -1171,7 +1163,7 @@
     "created_date": "2021-12-14T19:54:56.431Z",
     "sent_date": "2021-12-14T19:54:56.431Z",
     "owner": [
-      "business1"
+      "user7"
     ],
     "membership": 8
   }
@@ -3896,7 +3888,7 @@
     "updated_date": "2022-11-03T13:57:25.895Z",
     "name": "project1",
     "owner": [
-      "business1"
+      "user7"
     ],
     "assignee": [
       "user6"
@@ -3917,7 +3909,7 @@
     "updated_date": "2022-06-30T08:56:45.601Z",
     "name": "project2",
     "owner": [
-      "business1"
+      "user7"
     ],
     "assignee": [
       "user2"
@@ -4000,7 +3992,7 @@
       "user1"
     ],
     "assignee": [
-      "business4"
+      "user10"
     ],
     "assignee_updated_date": null,
     "bug_tracker": "",
@@ -4179,7 +4171,7 @@
       "admin1"
     ],
     "assignee": [
-      "business1"
+      "user7"
     ],
     "assignee_updated_date": "2024-09-23T08:09:45.461Z",
     "bug_tracker": "",
@@ -4281,7 +4273,7 @@
     "name": "task_2_org2",
     "mode": "annotation",
     "owner": [
-      "business2"
+      "user8"
     ],
     "assignee": [
       "worker2"
@@ -4337,7 +4329,7 @@
     "name": "task1_in_project1",
     "mode": "annotation",
     "owner": [
-      "business1"
+      "user7"
     ],
     "assignee": [
       "admin1"
@@ -4365,7 +4357,7 @@
     "name": "task1_in_project2",
     "mode": "annotation",
     "owner": [
-      "business1"
+      "user7"
     ],
     "assignee": [
       "user5"
@@ -4738,7 +4730,7 @@
       "user3"
     ],
     "assignee": [
-      "business1"
+      "user7"
     ],
     "assignee_updated_date": "2024-09-23T10:51:45.525Z",
     "bug_tracker": "",
@@ -12759,7 +12751,7 @@
   "pk": 10,
   "fields": {
     "user": [
-      "business1"
+      "user7"
     ],
     "rating": 0.0,
     "has_analytics_access": false
@@ -12770,7 +12762,7 @@
   "pk": 11,
   "fields": {
     "user": [
-      "business2"
+      "user8"
     ],
     "rating": 0.0,
     "has_analytics_access": false
@@ -12781,7 +12773,7 @@
   "pk": 12,
   "fields": {
     "user": [
-      "business3"
+      "user9"
     ],
     "rating": 0.0,
     "has_analytics_access": false
@@ -12792,7 +12784,7 @@
   "pk": 13,
   "fields": {
     "user": [
-      "business4"
+      "user10"
     ],
     "rating": 0.0,
     "has_analytics_access": false
@@ -12912,7 +12904,7 @@
     "position": "98.48046875, 696.72265625, 326.1220703125, 841.5859375",
     "job": 9,
     "owner": [
-      "business2"
+      "user8"
     ],
     "assignee": null,
     "resolved": false
@@ -12928,7 +12920,7 @@
     "position": "108.1845703125, 235.0, 720.0087890625, 703.3505859375",
     "job": 16,
     "owner": [
-      "business2"
+      "user8"
     ],
     "assignee": null,
     "resolved": false
@@ -13002,7 +12994,7 @@
     "updated_date": "2022-03-16T11:07:22.173Z",
     "issue": 2,
     "owner": [
-      "business2"
+      "user8"
     ],
     "message": "Something should be here"
   }
@@ -13015,7 +13007,7 @@
     "updated_date": "2022-03-16T11:08:18.370Z",
     "issue": 3,
     "owner": [
-      "business2"
+      "user8"
     ],
     "message": "Another one issue"
   }
@@ -13099,7 +13091,7 @@
     "resource": "private",
     "display_name": "Bucket 2",
     "owner": [
-      "business2"
+      "user8"
     ],
     "credentials": "minio_access_key minio_secret_key",
     "credentials_type": "KEY_SECRET_KEY_PAIR",
@@ -13563,7 +13555,7 @@
     "is_active": true,
     "enable_ssl": true,
     "owner": [
-      "business1"
+      "user7"
     ],
     "project": 1,
     "organization": null
@@ -13682,9 +13674,9 @@
         "user": {
           "id": 11,
           "url": "http://localhost:8080/api/users/11",
-          "username": "business2",
-          "last_name": "Second",
-          "first_name": "Business"
+          "username": "user8",
+          "last_name": "Eighth",
+          "first_name": "User"
         },
         "owner": {
           "id": 2,
@@ -13771,9 +13763,9 @@
         "user": {
           "id": 11,
           "url": "http://localhost:8080/api/users/11",
-          "username": "business2",
-          "last_name": "Second",
-          "first_name": "Business"
+          "username": "user8",
+          "last_name": "Eighth",
+          "first_name": "User"
         },
         "is_active": true,
         "invitation": "q8GWTPiR1Vz9DDO6MQo1B6pUBzW9GjDb6AUQPziAV62jD7OpCLZji0GS66C48wRX",
@@ -18172,6 +18164,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18180,6 +18173,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18194,6 +18188,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18202,6 +18197,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18216,6 +18212,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18224,6 +18221,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18238,6 +18236,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18246,6 +18245,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18260,6 +18260,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18268,6 +18269,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18282,6 +18284,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18290,6 +18293,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18304,6 +18308,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18312,6 +18317,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18326,6 +18332,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18334,6 +18341,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18348,6 +18356,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18356,6 +18365,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18370,6 +18380,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18378,6 +18389,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18392,6 +18404,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18400,6 +18413,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18414,6 +18428,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18422,6 +18437,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18436,6 +18452,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18444,6 +18461,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18458,6 +18476,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18466,6 +18485,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18480,6 +18500,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18488,6 +18509,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18502,6 +18524,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18510,6 +18533,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18524,6 +18548,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18532,6 +18557,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18546,6 +18572,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18554,6 +18581,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18568,6 +18596,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18576,6 +18605,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18590,6 +18620,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18598,6 +18629,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18612,6 +18644,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18620,6 +18653,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18634,6 +18668,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18642,6 +18677,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18656,6 +18692,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18664,6 +18701,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -18678,6 +18716,7 @@
     "oks_sigma": 0.09,
     "line_thickness": 0.01,
     "low_overlap_threshold": 0.8,
+    "point_size_base": "group_bbox_size",
     "compare_line_orientation": true,
     "line_orientation_threshold": 0.1,
     "compare_groups": true,
@@ -18686,6 +18725,7 @@
     "object_visibility_threshold": 0.05,
     "panoptic_comparison": true,
     "compare_attributes": true,
+    "match_empty_frames": false,
     "target_metric": "accuracy",
     "target_metric_threshold": 0.7,
     "max_validations_per_job": 0
@@ -19010,7 +19050,7 @@
       "user"
     ],
     "object_id": "10",
-    "object_repr": "business1",
+    "object_repr": "user7",
     "action_flag": 1,
     "change_message": "[{\"added\": {}}]"
   }
@@ -19028,7 +19068,7 @@
       "user"
     ],
     "object_id": "10",
-    "object_repr": "business1",
+    "object_repr": "user7",
     "action_flag": 2,
     "change_message": "[{\"changed\": {\"fields\": [\"First name\", \"Last name\"]}}]"
   }
@@ -19046,7 +19086,7 @@
       "user"
     ],
     "object_id": "10",
-    "object_repr": "business1",
+    "object_repr": "user7",
     "action_flag": 2,
     "change_message": "[{\"changed\": {\"fields\": [\"Last name\", \"Email address\", \"Groups\"]}}]"
   }
@@ -19064,7 +19104,7 @@
       "user"
     ],
     "object_id": "11",
-    "object_repr": "business2",
+    "object_repr": "user8",
     "action_flag": 1,
     "change_message": "[{\"added\": {}}]"
   }
@@ -19082,7 +19122,7 @@
       "user"
     ],
     "object_id": "11",
-    "object_repr": "business2",
+    "object_repr": "user8",
     "action_flag": 2,
     "change_message": "[{\"changed\": {\"fields\": [\"First name\", \"Last name\", \"Email address\", \"Groups\"]}}]"
   }
@@ -19100,7 +19140,7 @@
       "user"
     ],
     "object_id": "12",
-    "object_repr": "business3",
+    "object_repr": "user9",
     "action_flag": 1,
     "change_message": "[{\"added\": {}}]"
   }
@@ -19118,7 +19158,7 @@
       "user"
     ],
     "object_id": "12",
-    "object_repr": "business3",
+    "object_repr": "user9",
     "action_flag": 2,
     "change_message": "[{\"changed\": {\"fields\": [\"First name\", \"Last name\", \"Email address\", \"Groups\"]}}]"
   }
@@ -19136,7 +19176,7 @@
       "user"
     ],
     "object_id": "13",
-    "object_repr": "business4",
+    "object_repr": "user10",
     "action_flag": 1,
     "change_message": "[{\"added\": {}}]"
   }
@@ -19154,7 +19194,7 @@
       "user"
     ],
     "object_id": "13",
-    "object_repr": "business4",
+    "object_repr": "user10",
     "action_flag": 2,
     "change_message": "[{\"changed\": {\"fields\": [\"First name\", \"Last name\", \"Email address\", \"Groups\"]}}]"
   }
diff --git a/tests/python/shared/assets/invitations.json b/tests/python/shared/assets/invitations.json
index 6b0f24528202..9a58f4bbee09 100644
--- a/tests/python/shared/assets/invitations.json
+++ b/tests/python/shared/assets/invitations.json
@@ -21,11 +21,11 @@
       },
       "role": "maintainer",
       "user": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       }
     },
     {
@@ -113,11 +113,11 @@
         "slug": "org2"
       },
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "role": "maintainer",
       "user": {
@@ -138,11 +138,11 @@
         "slug": "org2"
       },
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "role": "supervisor",
       "user": {
@@ -163,11 +163,11 @@
         "slug": "org2"
       },
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "role": "supervisor",
       "user": {
@@ -188,11 +188,11 @@
         "slug": "org2"
       },
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "role": "worker",
       "user": {
@@ -213,11 +213,11 @@
         "slug": "org2"
       },
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "role": "worker",
       "user": {
@@ -238,19 +238,19 @@
         "slug": "org2"
       },
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "role": "maintainer",
       "user": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       }
     },
     {
@@ -271,11 +271,11 @@
       },
       "role": "maintainer",
       "user": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       }
     },
     {
diff --git a/tests/python/shared/assets/issues.json b/tests/python/shared/assets/issues.json
index 9aff3cf4b632..e719b30ac11d 100644
--- a/tests/python/shared/assets/issues.json
+++ b/tests/python/shared/assets/issues.json
@@ -72,11 +72,11 @@
       "id": 3,
       "job": 16,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       },
       "position": [
         108.1845703125,
@@ -98,11 +98,11 @@
       "id": 2,
       "job": 9,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       },
       "position": [
         98.48046875,
diff --git a/tests/python/shared/assets/memberships.json b/tests/python/shared/assets/memberships.json
index 9ae6bcc8d950..3c0be8035d2d 100644
--- a/tests/python/shared/assets/memberships.json
+++ b/tests/python/shared/assets/memberships.json
@@ -11,11 +11,11 @@
       "organization": 1,
       "role": "maintainer",
       "user": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       }
     },
     {
@@ -146,11 +146,11 @@
       "organization": 2,
       "role": "maintainer",
       "user": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       }
     },
     {
@@ -161,11 +161,11 @@
       "organization": 2,
       "role": "owner",
       "user": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       }
     },
     {
@@ -176,11 +176,11 @@
       "organization": 1,
       "role": "maintainer",
       "user": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       }
     },
     {
diff --git a/tests/python/shared/assets/organizations.json b/tests/python/shared/assets/organizations.json
index ad26620a27e0..8106c5b8b6a7 100644
--- a/tests/python/shared/assets/organizations.json
+++ b/tests/python/shared/assets/organizations.json
@@ -12,11 +12,11 @@
       "id": 2,
       "name": "Organization #2",
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "slug": "org2",
       "updated_date": "2021-12-14T19:51:38.667000Z"
diff --git a/tests/python/shared/assets/projects.json b/tests/python/shared/assets/projects.json
index f7c0c25b464e..19d345ee4e30 100644
--- a/tests/python/shared/assets/projects.json
+++ b/tests/python/shared/assets/projects.json
@@ -5,11 +5,11 @@
   "results": [
     {
       "assignee": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "assignee_updated_date": "2024-09-23T08:09:45.461000Z",
       "bug_tracker": "",
@@ -383,11 +383,11 @@
     },
     {
       "assignee": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 13,
-        "last_name": "Fourth",
+        "last_name": "Tenth",
         "url": "http://localhost:8080/api/users/13",
-        "username": "business4"
+        "username": "user10"
       },
       "assignee_updated_date": null,
       "bug_tracker": "",
@@ -553,11 +553,11 @@
       "name": "project2",
       "organization": 2,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "source_storage": {
         "cloud_storage_id": 3,
@@ -600,11 +600,11 @@
       "name": "project1",
       "organization": null,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "source_storage": null,
       "status": "annotation",
diff --git a/tests/python/shared/assets/quality_settings.json b/tests/python/shared/assets/quality_settings.json
index 54e0c18c63a3..7ddc589bc7bf 100644
--- a/tests/python/shared/assets/quality_settings.json
+++ b/tests/python/shared/assets/quality_settings.json
@@ -14,10 +14,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 2
@@ -33,10 +35,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 5
@@ -52,10 +56,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 6
@@ -71,10 +77,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 7
@@ -90,10 +98,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 8
@@ -109,10 +119,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 9
@@ -128,10 +140,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 11
@@ -147,10 +161,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 12
@@ -166,10 +182,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 13
@@ -185,10 +203,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 14
@@ -204,10 +224,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 15
@@ -223,10 +245,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 17
@@ -242,10 +266,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 18
@@ -261,10 +287,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 19
@@ -280,10 +308,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 20
@@ -299,10 +329,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 21
@@ -318,10 +350,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 22
@@ -337,10 +371,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 23
@@ -356,10 +392,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 24
@@ -375,10 +413,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 25
@@ -394,10 +434,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 26
@@ -413,10 +455,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 27
@@ -432,10 +476,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 28
@@ -451,10 +497,12 @@
       "line_orientation_threshold": 0.1,
       "line_thickness": 0.01,
       "low_overlap_threshold": 0.8,
+      "match_empty_frames": false,
       "max_validations_per_job": 0,
       "object_visibility_threshold": 0.05,
       "oks_sigma": 0.09,
       "panoptic_comparison": true,
+      "point_size_base": "group_bbox_size",
       "target_metric": "accuracy",
       "target_metric_threshold": 0.7,
       "task_id": 29
diff --git a/tests/python/shared/assets/tasks.json b/tests/python/shared/assets/tasks.json
index 5a28176ef5ec..cf2d63da785c 100644
--- a/tests/python/shared/assets/tasks.json
+++ b/tests/python/shared/assets/tasks.json
@@ -159,11 +159,11 @@
     },
     {
       "assignee": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "assignee_updated_date": "2024-09-23T10:51:45.525000Z",
       "bug_tracker": "",
@@ -890,11 +890,11 @@
       "organization": 2,
       "overlap": 0,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "project_id": 2,
       "segment_size": 11,
@@ -948,11 +948,11 @@
       "organization": null,
       "overlap": 0,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "project_id": 1,
       "segment_size": 5,
@@ -1048,11 +1048,11 @@
       "organization": 2,
       "overlap": 0,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 11,
-        "last_name": "Second",
+        "last_name": "Eighth",
         "url": "http://localhost:8080/api/users/11",
-        "username": "business2"
+        "username": "user8"
       },
       "project_id": null,
       "segment_size": 11,
diff --git a/tests/python/shared/assets/users.json b/tests/python/shared/assets/users.json
index d3a420b297a7..9c4dce1e4fdf 100644
--- a/tests/python/shared/assets/users.json
+++ b/tests/python/shared/assets/users.json
@@ -133,10 +133,10 @@
     },
     {
       "date_joined": "2021-12-14T18:35:15Z",
-      "email": "business4@cvat.org",
-      "first_name": "Business",
+      "email": "user10@cvat.org",
+      "first_name": "User",
       "groups": [
-        "business"
+        "user"
       ],
       "has_analytics_access": false,
       "id": 13,
@@ -144,16 +144,16 @@
       "is_staff": false,
       "is_superuser": false,
       "last_login": null,
-      "last_name": "Fourth",
+      "last_name": "Tenth",
       "url": "http://localhost:8080/api/users/13",
-      "username": "business4"
+      "username": "user10"
     },
     {
       "date_joined": "2021-12-14T18:34:34Z",
-      "email": "business3@cvat.org",
-      "first_name": "Business",
+      "email": "user9@cvat.org",
+      "first_name": "User",
       "groups": [
-        "business"
+        "user"
       ],
       "has_analytics_access": false,
       "id": 12,
@@ -161,16 +161,16 @@
       "is_staff": false,
       "is_superuser": false,
       "last_login": null,
-      "last_name": "Third",
+      "last_name": "Nineth",
       "url": "http://localhost:8080/api/users/12",
-      "username": "business3"
+      "username": "user9"
     },
     {
       "date_joined": "2021-12-14T18:34:01Z",
-      "email": "business2@cvat.org",
-      "first_name": "Business",
+      "email": "user8@cvat.org",
+      "first_name": "User",
       "groups": [
-        "business"
+        "user"
       ],
       "has_analytics_access": false,
       "id": 11,
@@ -178,16 +178,16 @@
       "is_staff": false,
       "is_superuser": false,
       "last_login": "2022-03-17T07:22:55.930000Z",
-      "last_name": "Second",
+      "last_name": "Eighth",
       "url": "http://localhost:8080/api/users/11",
-      "username": "business2"
+      "username": "user8"
     },
     {
       "date_joined": "2021-12-14T18:33:06Z",
-      "email": "business1@cvat.org",
-      "first_name": "Business",
+      "email": "user7@cvat.org",
+      "first_name": "User",
       "groups": [
-        "business"
+        "user"
       ],
       "has_analytics_access": false,
       "id": 10,
@@ -195,9 +195,9 @@
       "is_staff": false,
       "is_superuser": false,
       "last_login": "2022-09-28T12:17:51.373000Z",
-      "last_name": "First",
+      "last_name": "Seventh",
       "url": "http://localhost:8080/api/users/10",
-      "username": "business1"
+      "username": "user7"
     },
     {
       "date_joined": "2021-12-14T18:32:01Z",
diff --git a/tests/python/shared/assets/webhooks.json b/tests/python/shared/assets/webhooks.json
index da5b0f6837d9..b6a90828ee3c 100644
--- a/tests/python/shared/assets/webhooks.json
+++ b/tests/python/shared/assets/webhooks.json
@@ -95,11 +95,11 @@
       "is_active": true,
       "organization": null,
       "owner": {
-        "first_name": "Business",
+        "first_name": "User",
         "id": 10,
-        "last_name": "First",
+        "last_name": "Seventh",
         "url": "http://localhost:8080/api/users/10",
-        "username": "business1"
+        "username": "user7"
       },
       "project_id": 1,
       "target_url": "http://example.com/",
diff --git a/tests/python/shared/fixtures/data.py b/tests/python/shared/fixtures/data.py
index 13f128d2edf8..0f6fb6939544 100644
--- a/tests/python/shared/fixtures/data.py
+++ b/tests/python/shared/fixtures/data.py
@@ -5,8 +5,8 @@
 import json
 import operator
 from collections import defaultdict
+from collections.abc import Iterable
 from copy import deepcopy
-from typing import Iterable
 
 import pytest
 
diff --git a/tests/python/shared/fixtures/init.py b/tests/python/shared/fixtures/init.py
index b29c5c30528b..1f5d57ffc5d7 100644
--- a/tests/python/shared/fixtures/init.py
+++ b/tests/python/shared/fixtures/init.py
@@ -10,7 +10,7 @@
 from pathlib import Path
 from subprocess import PIPE, CalledProcessError, run
 from time import sleep
-from typing import List, Union
+from typing import Union
 
 import pytest
 import requests
@@ -158,13 +158,13 @@ def docker_exec(container, command, capture_output=True):
     return _run(f"docker exec -u root {PREFIX}_{container}_1 {command}", capture_output)
 
 
-def docker_exec_cvat(command: Union[List[str], str]):
+def docker_exec_cvat(command: Union[list[str], str]):
     base = f"docker exec {PREFIX}_cvat_server_1"
     _command = f"{base} {command}" if isinstance(command, str) else base.split() + command
     return _run(_command)
 
 
-def kube_exec_cvat(command: Union[List[str], str]):
+def kube_exec_cvat(command: Union[list[str], str]):
     pod_name = _kube_get_server_pod_name()
     base = f"kubectl exec {pod_name} --"
     _command = f"{base} {command}" if isinstance(command, str) else base.split() + command
diff --git a/tests/python/shared/utils/config.py b/tests/python/shared/utils/config.py
index f313334c797d..e65ac0b904a5 100644
--- a/tests/python/shared/utils/config.py
+++ b/tests/python/shared/utils/config.py
@@ -2,9 +2,9 @@
 #
 # SPDX-License-Identifier: MIT
 
+from collections.abc import Generator
 from contextlib import contextmanager
 from pathlib import Path
-from typing import Generator
 
 import requests
 from cvat_sdk.api_client import ApiClient, Configuration
diff --git a/tests/python/shared/utils/helpers.py b/tests/python/shared/utils/helpers.py
index 14015f4b2ad3..4855796a0a86 100644
--- a/tests/python/shared/utils/helpers.py
+++ b/tests/python/shared/utils/helpers.py
@@ -3,9 +3,10 @@
 # SPDX-License-Identifier: MIT
 
 import subprocess
+from collections.abc import Generator
 from contextlib import closing
 from io import BytesIO
-from typing import Generator, List, Optional, Tuple
+from typing import Optional
 
 import av
 import av.video.reformatter
@@ -27,10 +28,10 @@ def generate_image_file(filename="image.png", size=(100, 50), color=(0, 0, 0)):
 def generate_image_files(
     count: int,
     *,
-    prefixes: Optional[List[str]] = None,
-    filenames: Optional[List[str]] = None,
-    sizes: Optional[List[Tuple[int, int]]] = None,
-) -> List[BytesIO]:
+    prefixes: Optional[list[str]] = None,
+    filenames: Optional[list[str]] = None,
+    sizes: Optional[list[tuple[int, int]]] = None,
+) -> list[BytesIO]:
     assert not (prefixes and filenames), "prefixes cannot be used together with filenames"
     assert not prefixes or len(prefixes) == count
     assert not filenames or len(filenames) == count
diff --git a/tests/python/shared/utils/resource_import_export.py b/tests/python/shared/utils/resource_import_export.py
index 37983dbd1478..c8b4fd7ca93b 100644
--- a/tests/python/shared/utils/resource_import_export.py
+++ b/tests/python/shared/utils/resource_import_export.py
@@ -1,10 +1,10 @@
 import functools
 import json
-from abc import ABC, abstractstaticmethod
+from abc import ABC, abstractmethod
 from contextlib import ExitStack
 from http import HTTPStatus
 from time import sleep
-from typing import Any, Dict, Optional, TypeVar
+from typing import Any, Optional, TypeVar
 
 import pytest
 
@@ -17,7 +17,7 @@
 IMPORT_FORMAT = "CVAT 1.1"
 
 
-def _make_custom_resource_params(resource: str, obj: str, cloud_storage_id: int) -> Dict[str, Any]:
+def _make_custom_resource_params(resource: str, obj: str, cloud_storage_id: int) -> dict[str, Any]:
     return {
         "filename": FILENAME_TEMPLATE.format(obj, resource),
         "location": "cloud_storage",
@@ -25,7 +25,7 @@ def _make_custom_resource_params(resource: str, obj: str, cloud_storage_id: int)
     }
 
 
-def _make_default_resource_params(resource: str, obj: str) -> Dict[str, Any]:
+def _make_default_resource_params(resource: str, obj: str) -> dict[str, Any]:
     return {
         "filename": FILENAME_TEMPLATE.format(obj, resource),
     }
@@ -33,7 +33,7 @@ def _make_default_resource_params(resource: str, obj: str) -> Dict[str, Any]:
 
 def _make_export_resource_params(
     resource: str, is_default: bool = True, **kwargs
-) -> Dict[str, Any]:
+) -> dict[str, Any]:
     func = _make_default_resource_params if is_default else _make_custom_resource_params
     params = func(resource, **kwargs)
     if resource != "backup":
@@ -43,7 +43,7 @@ def _make_export_resource_params(
 
 def _make_import_resource_params(
     resource: str, is_default: bool = True, **kwargs
-) -> Dict[str, Any]:
+) -> dict[str, Any]:
     func = _make_default_resource_params if is_default else _make_custom_resource_params
     params = func(resource, **kwargs)
     if resource != "backup":
@@ -52,7 +52,8 @@ def _make_import_resource_params(
 
 
 class _CloudStorageResourceTest(ABC):
-    @abstractstaticmethod
+    @staticmethod
+    @abstractmethod
     def _make_client():
         pass
 
@@ -64,7 +65,7 @@ def setup(self, admin_user: str):
         with self.exit_stack:
             yield
 
-    def _ensure_file_created(self, func: T, storage: Dict[str, Any]) -> T:
+    def _ensure_file_created(self, func: T, storage: dict[str, Any]) -> T:
         @functools.wraps(func)
         def wrapper(*args, **kwargs):
             filename = kwargs["filename"]
@@ -219,7 +220,7 @@ def _import_dataset_from_cloud_storage(
             response = get_method(user, url, action="import_status", rq_id=rq_id)
             status = response.status_code
 
-    def _import_resource(self, cloud_storage: Dict[str, Any], resource_type: str, *args, **kwargs):
+    def _import_resource(self, cloud_storage: dict[str, Any], resource_type: str, *args, **kwargs):
         methods = {
             "annotations": self._import_annotations_from_cloud_storage,
             "dataset": self._import_dataset_from_cloud_storage,
@@ -234,7 +235,7 @@ def _import_resource(self, cloud_storage: Dict[str, Any], resource_type: str, *a
 
         return methods[resource_type](*args, **kwargs)
 
-    def _export_resource(self, cloud_storage: Dict[str, Any], *args, **kwargs):
+    def _export_resource(self, cloud_storage: dict[str, Any], *args, **kwargs):
         org_id = cloud_storage["organization"]
         if org_id:
             kwargs.setdefault("org_id", org_id)