Repository: AliceO2Group/Bookkeeping
Pull request #1881: [O2B-1407] Add possibility to lock RCT flags
Author: login: martinboulais
Labels: name: frontend
name: backend
Created: 2025-04-07T14:12:43Z | Merged: 2025-04-10T10:37:48Z

#### I have a JIRA ticket
- [X] branch and/or PR name(s) include(s) JIRA ID
- [X] issue has "Fix version" assigned
- [X] issue "Status" is set to "In review"
- [X] PR labels are selected

Notable changes for users:
- Added possibility to lock data passes

Notable changes for developers:
- N/A

Changes made to the database:
- Added `frozen` flag to datapasses table



Diff:
diff --git a/lib/database/adapters/DataPassAdapter.js b/lib/database/adapters/DataPassAdapter.js
index 12baf24f21..d4ca53fd80 100644
--- a/lib/database/adapters/DataPassAdapter.js
+++ b/lib/database/adapters/DataPassAdapter.js
@@ -35,6 +35,7 @@ class DataPassAdapter {
             name,
             versions,
             skimmingStage,
+            isFrozen,
         } = databaseObject;
 
         const runsCount = databaseObject.get('runsCount');
@@ -49,6 +50,7 @@ class DataPassAdapter {
             pdpBeamType,
             runsCount,
             simulationPassesCount,
+            isFrozen,
         };
     }
 }
diff --git a/lib/database/migrations/v1/20250328072732-add-datapass-freeze-flag.js b/lib/database/migrations/v1/20250328072732-add-datapass-freeze-flag.js
new file mode 100644
index 0000000000..cea6bb887e
--- /dev/null
+++ b/lib/database/migrations/v1/20250328072732-add-datapass-freeze-flag.js
@@ -0,0 +1,16 @@
+'use strict';
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+    up: async (queryInterface, Sequelize) => await queryInterface.addColumn(
+        'data_passes',
+        'is_frozen',
+        {
+            type: Sequelize.DataTypes.BOOLEAN,
+            allowNull: false,
+            defaultValue: false,
+        },
+    ),
+
+    down: async (queryInterface) => await queryInterface.removeColumn('data_passes', 'is_frozen'),
+};
diff --git a/lib/database/models/dataPass.js b/lib/database/models/dataPass.js
index 5d18d1a274..57bd840357 100644
--- a/lib/database/models/dataPass.js
+++ b/lib/database/models/dataPass.js
@@ -32,6 +32,11 @@ module.exports = (sequelize) => {
                 type: Sequelize.ENUM(...SKIMMING_STAGES),
                 allowNull: true,
             },
+            isFrozen: {
+                type: Sequelize.BOOLEAN,
+                allowNull: false,
+                defaultValue: false,
+            },
         },
         { tableName: 'data_passes' },
     );
diff --git a/lib/database/models/typedefs/SequelizeDataPass.js b/lib/database/models/typedefs/SequelizeDataPass.js
index 69ccabd52d..cfe217bf40 100644
--- a/lib/database/models/typedefs/SequelizeDataPass.js
+++ b/lib/database/models/typedefs/SequelizeDataPass.js
@@ -19,4 +19,5 @@
  * @property {string} skimmingStage
  * @property {number} runsCount
  * @property {number} simulationPassesCount
+ * @property {boolean} isFrozen
  */
diff --git a/lib/database/models/typedefs/SequelizeQcFlagVerification.js b/lib/database/models/typedefs/SequelizeQcFlagVerification.js
index 6bb6baa209..ddc4636518 100644
--- a/lib/database/models/typedefs/SequelizeQcFlagVerification.js
+++ b/lib/database/models/typedefs/SequelizeQcFlagVerification.js
@@ -14,10 +14,10 @@
 /**
  * @typedef SequelizeQcFlagVerification
  *
- * @param {number} id
- * @param {string} comment
+ * @property {number} id
+ * @property {string} comment
  * @property {number} flagId
  * @property {number} createdById
  * @property {SequelizeUser} createdBy
- * @param {number} createdAt
+ * @property {number} createdAt
  */
diff --git a/lib/domain/entities/DataPass.js b/lib/domain/entities/DataPass.js
index b9ec6d2a4e..87124c9cf1 100644
--- a/lib/domain/entities/DataPass.js
+++ b/lib/domain/entities/DataPass.js
@@ -19,4 +19,5 @@
  * @property {string} skimmingStage
  * @property {number} runsCount
  * @property {number} simulationPassesCount
+ * @property {boolean} isFrozen
  */
diff --git a/lib/domain/entities/qcFlagVerification.js b/lib/domain/entities/qcFlagVerification.js
index 80237c17f8..75049ba352 100644
--- a/lib/domain/entities/qcFlagVerification.js
+++ b/lib/domain/entities/qcFlagVerification.js
@@ -14,10 +14,10 @@
 /**
  * @typedef QcFlagVerification
  *
- * @param {number} id
- * @param {string} comment
+ * @property {number} id
+ * @property {string} comment
  * @property {number} flagId
  * @property {number} createdById
  * @property {User} createdBy
- * @param {number} createdAt
+ * @property {number} createdAt
  */
diff --git a/lib/public/components/qcFlags/qcFlagCreationPanelLink.js b/lib/public/components/qcFlags/qcFlagCreationPanelLink.js
index 01a78b58d3..53bd9c4502 100644
--- a/lib/public/components/qcFlags/qcFlagCreationPanelLink.js
+++ b/lib/public/components/qcFlags/qcFlagCreationPanelLink.js
@@ -47,6 +47,10 @@ export const qcFlagCreationPanelLink = (
     let options = {};
     let pageName = '';
     if (dataPass) {
+        if (dataPass.isFrozen) {
+            return h('button.btn.btn-primary', { disabled: true, title: 'Datapass is frozen' }, content);
+        }
+
         options = { dataPassId: dataPass.id };
         pageName = 'qc-flag-creation-for-data-pass';
     } else if (simulationPass) {
diff --git a/lib/public/views/QcFlags/Create/QcFlagCreationModel.js b/lib/public/views/QcFlags/Create/QcFlagCreationModel.js
index efa7bd652c..748bfe58d8 100644
--- a/lib/public/views/QcFlags/Create/QcFlagCreationModel.js
+++ b/lib/public/views/QcFlags/Create/QcFlagCreationModel.js
@@ -19,7 +19,7 @@ import { detectorsProvider } from '../../../services/detectors/detectorsProvider
 
 /**
  * @typedef QcFlagCreationFormData
- * @param {string} comment
+ * @property {string} comment
  */
 
 /**
diff --git a/lib/public/views/QcFlags/Create/forDataPass/QcFlagCreationForDataPassPage.js b/lib/public/views/QcFlags/Create/forDataPass/QcFlagCreationForDataPassPage.js
index d432537c82..caa3c11706 100644
--- a/lib/public/views/QcFlags/Create/forDataPass/QcFlagCreationForDataPassPage.js
+++ b/lib/public/views/QcFlags/Create/forDataPass/QcFlagCreationForDataPassPage.js
@@ -29,30 +29,30 @@ import errorAlert from '../../../../components/common/errorAlert.js';
 export const QcFlagCreationForDataPassPage = ({ qcFlags: { creationForDataPassModel: qcFlagCreationForDataPassModel } }) => {
     const { dataPass: remoteDataPass } = qcFlagCreationForDataPassModel;
 
-    return [
-        h('h2.g2.flex-row.items-center', [
-            h('div', 'Create QC flag'),
-            remoteDataPass?.match({
-                Success: ({ id, name }) => [
-                    h('', ' for datapass '),
-                    frontLink(name, 'runs-per-data-pass', { dataPassId: id }),
-                ],
-                Failure: () => tooltip(h('.f3', iconWarning()), 'Not able to load data pass info'),
-                Loading: () => h('', spinner({ size: 1, absolute: false })),
-                NotAsked: () => tooltip(h('.f3', iconWarning()), 'No data pass data was asked for'),
-            }),
-        ]),
-        h(
-            '.flex-column.g3.flex-grow',
-            creationFormComponent(
-                qcFlagCreationForDataPassModel,
-                qcFlagCreationForDataPassModel.qcFlagCreationModel.match({
-                    Success: (qcFlagCreationModel) => qcFlagCreationComponent(qcFlagCreationModel),
-                    Loading: () => spinner({ absolute: false }),
-                    Failure: (errors) => errorAlert(errors),
-                    NotAsked: () => null,
-                }),
-            ),
-        ),
-    ];
+    return remoteDataPass?.match({
+        Success: ({ id, name, isFrozen }) => [
+            h('h2.g2.flex-row.items-center', [
+                h('div', 'Create QC flag'),
+                h('', ' for datapass '),
+                frontLink(name, 'runs-per-data-pass', { dataPassId: id }),
+            ]),
+            isFrozen
+                ? errorAlert({ title: 'Datapass is frozen' })
+                : h(
+                    '.flex-column.g3.flex-grow',
+                    creationFormComponent(
+                        qcFlagCreationForDataPassModel,
+                        qcFlagCreationForDataPassModel.qcFlagCreationModel.match({
+                            Success: (qcFlagCreationModel) => qcFlagCreationComponent(qcFlagCreationModel),
+                            Loading: () => spinner({ absolute: false }),
+                            Failure: (errors) => errorAlert(errors),
+                            NotAsked: () => null,
+                        }),
+                    ),
+                ),
+        ],
+        Failure: () => tooltip(h('.f3', iconWarning()), 'Not able to load data pass info'),
+        Loading: () => h('', spinner({ size: 1, absolute: false })),
+        NotAsked: () => tooltip(h('.f3', iconWarning()), 'No data pass data was asked for'),
+    });
 };
diff --git a/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js
index d1d5c8169a..e8c0a5bc64 100644
--- a/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js
+++ b/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js
@@ -93,7 +93,7 @@ export const createRunDetectorsAsyncQcActiveColumns = (
 
                     const runExcludedFromQc = isRunNotSubjectToQc(run);
 
-                    const checkbox = !runExcludedFromQc && detectorUserHasAccessTo && h(
+                    const checkbox = !runExcludedFromQc && !dataPass?.isFrozen && detectorUserHasAccessTo && h(
                         'input.select-multi-flag',
                         {
                             type: 'checkbox',
diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js
index 901d6ce3ad..a2345cdce5 100644
--- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js
+++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js
@@ -62,6 +62,12 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo
         this._runDetectorsSelectionModel = new RunDetectorsSelectionModel();
         this._runDetectorsSelectionModel.bubbleTo(this);
 
+        this._freezeOrUnfreezeActionState$ = new ObservableData(RemoteData.notAsked());
+        this._freezeOrUnfreezeActionState$.bubbleTo(this);
+
+        this._discardAllQcFlagsActionState$ = new ObservableData(RemoteData.notAsked());
+        this._discardAllQcFlagsActionState$.bubbleTo(this);
+
         this.patchDisplayOptions({ horizontalScrollEnabled: true });
     }
 
@@ -154,17 +160,79 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo
         this._gaqNotBadFractionFilterModel?.reset(); // Reset is called in super class
     }
 
+    /**
+     * Freeze the current data pass
+     *
+     * @return {Promise<void>} Resolves once the data pass is frozen
+     */
+    async freezeDataPass() {
+        this._freezeOrUnfreezeActionState$.setCurrent(RemoteData.loading());
+        try {
+            await jsonPatch(
+                buildUrl('/api/dataPasses/freeze', { dataPassId: this._dataPassId }),
+                {},
+            );
+            this._freezeOrUnfreezeActionState$.setCurrent(RemoteData.success(null));
+            await this.load();
+        } catch (error) {
+            this._freezeOrUnfreezeActionState$.setCurrent(RemoteData.failure(error));
+        }
+    }
+
+    /**
+     * Unfreeze the current data pass
+     *
+     * @return {Promise<void>} Resolves once the data pass is unfrozen
+     */
+    async unfreezeDataPass() {
+        this._freezeOrUnfreezeActionState$.setCurrent(RemoteData.loading());
+        try {
+            await jsonPatch(
+                buildUrl('/api/dataPasses/unfreeze', { dataPassId: this._dataPassId }),
+                {},
+            );
+            this._freezeOrUnfreezeActionState$.setCurrent(RemoteData.success(null));
+            await this.load();
+        } catch (error) {
+            this._freezeOrUnfreezeActionState$.setCurrent(RemoteData.failure(error));
+        }
+    }
+
+    /**
+     * Return the state of the freeze/unfreeze action
+     *
+     * @return {RemoteData<null, ApiError>} the freeze/unfreeze action state
+     */
+    get freezeOrUnfreezeActionState() {
+        return this._freezeOrUnfreezeActionState$.getCurrent();
+    }
+
     /**
      * Discard all the QC for the data-pass
      *
      * @return {Promise<void>} resolves once flags has been deleted
      */
     async discardAllQcFlags() {
-        await jsonFetch(
-            buildUrl('/api/qcFlags/perDataPass', { dataPassId: this._dataPassId }),
-            { method: 'DELETE' },
-        );
-        await this.load();
+        this._discardAllQcFlagsActionState$.setCurrent(RemoteData.loading());
+        try {
+            await jsonFetch(
+                buildUrl('/api/qcFlags/perDataPass', { dataPassId: this._dataPassId }),
+                { method: 'DELETE' },
+            );
+            this._discardAllQcFlagsActionState$.setCurrent(RemoteData.success(null));
+            await this.load();
+        } catch (error) {
+            this._discardAllQcFlagsActionState$.setCurrent(RemoteData.failure(error));
+        }
+    }
+
+    /**
+     * Return the state of the discard all QC flags action
+     *
+     * @return {RemoteData<null, ApiError>} the QC flags action state
+     */
+    get discardAllQcFlagsActionState() {
+        return this._discardAllQcFlagsActionState$.getCurrent();
     }
 
     /**
diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js
index 8a79208187..a5db236d19 100644
--- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js
+++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js
@@ -123,6 +123,8 @@ export const RunsPerDataPassOverviewPage = ({
         pdpBeamType,
         markAsSkimmableRequestResult,
         skimmableRuns: remoteSkimmableRuns,
+        freezeOrUnfreezeActionState,
+        discardAllQcFlagsActionState,
     } = perDataPassOverviewModel;
 
     const commonTitle = h('h2#breadcrumb-header', { style: 'white-space: nowrap;' }, 'Physics Runs');
@@ -247,17 +249,42 @@ export const RunsPerDataPassOverviewPage = ({
                                         dataPassId,
                                     },
                                 ),
-                                sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && h(
-                                    'button.btn.btn-danger',
-                                    {
-                                        onclick: () => {
-                                            if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) {
-                                                perDataPassOverviewModel.discardAllQcFlags();
-                                            }
+                                sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && [
+                                    h(
+                                        'button.btn.btn-danger',
+                                        {
+                                            ...freezeOrUnfreezeActionState.match({
+                                                Loading: () => ({
+                                                    disabled: true,
+                                                    title: 'Loading',
+                                                }),
+                                                Other: () => ({}),
+                                            }),
+                                            onclick: () => dataPass.isFrozen
+                                                ? perDataPassOverviewModel.unfreezeDataPass()
+                                                : perDataPassOverviewModel.freezeDataPass(),
                                         },
-                                    },
-                                    'Delete ALL QC flags',
-                                ),
+                                        `${dataPass.isFrozen ? 'Unfreeze' : 'Freeze'} the data pass`,
+                                    ),
+                                    h(
+                                        'button.btn.btn-danger',
+                                        {
+                                            ...discardAllQcFlagsActionState.match({
+                                                Loading: () => ({
+                                                    disabled: true,
+                                                    title: 'Loading',
+                                                }),
+                                                Other: () => ({}),
+                                            }),
+                                            onclick: () => {
+                                                if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) {
+                                                    perDataPassOverviewModel.discardAllQcFlags();
+                                                }
+                                            },
+                                        },
+                                        'Delete ALL QC flags',
+                                    ),
+                                ],
                             ]),
                             { alignment: 'right' },
                         )),
@@ -266,6 +293,14 @@ export const RunsPerDataPassOverviewPage = ({
                         Failure: (errors) => errorAlert(errors),
                         Other: () => null,
                     }),
+                    freezeOrUnfreezeActionState.match({
+                        Failure: (errors) => errorAlert(errors),
+                        Other: () => null,
+                    }),
+                    discardAllQcFlagsActionState.match({
+                        Failure: (errors) => errorAlert(errors),
+                        Other: () => null,
+                    }),
                     h('.flex-column.w-100', [
                         table(
                             runs,
diff --git a/lib/server/controllers/dataPasses.controller.js b/lib/server/controllers/dataPasses.controller.js
index 6fdf9d54ea..592c00c41c 100644
--- a/lib/server/controllers/dataPasses.controller.js
+++ b/lib/server/controllers/dataPasses.controller.js
@@ -55,6 +55,46 @@ const listDataPassesHandler = async (req, res) => {
     }
 };
 
+// eslint-disable-next-line valid-jsdoc
+/**
+ * Freeze the given data pass
+ */
+const freezeHandler = async (req, res) => {
+    const validatedDTO = await dtoValidator(
+        DtoFactory.queryOnly({ dataPassId: Joi.number().required() }),
+        req,
+        res,
+    );
+    if (validatedDTO) {
+        try {
+            await dataPassService.setFrozenState({ id: validatedDTO.query.dataPassId }, true);
+            res.sendStatus(204);
+        } catch (error) {
+            updateExpressResponseFromNativeError(res, error);
+        }
+    }
+};
+
+// eslint-disable-next-line valid-jsdoc
+/**
+ * Un-freeze the given data pass
+ */
+const unfreezeHandler = async (req, res) => {
+    const validatedDTO = await dtoValidator(
+        DtoFactory.queryOnly({ dataPassId: Joi.number().required() }),
+        req,
+        res,
+    );
+    if (validatedDTO) {
+        try {
+            await dataPassService.setFrozenState({ id: validatedDTO.query.dataPassId }, false);
+            res.sendStatus(204);
+        } catch (error) {
+            updateExpressResponseFromNativeError(res, error);
+        }
+    }
+};
+
 // eslint-disable-next-line valid-jsdoc
 /**
  * Set given data pass (for PROTON_PROTON runs) as skimmable
@@ -124,6 +164,8 @@ const updateReadyForSkimmingRunsHandler = async (req, res) => {
 
 exports.DataPassesController = {
     listDataPassesHandler,
+    freezeHandler,
+    unfreezeHandler,
     markAsSkimmableHandler,
     fetchSkimmableRunsHandler,
     updateReadyForSkimmingRunsHandler,
diff --git a/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js b/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js
index b0b5e0d554..3a757e4a12 100644
--- a/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js
+++ b/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js
@@ -19,36 +19,36 @@ const VALID_DATA_PASS_NAME_REGEX = /^LHC\d\d[a-zA-Z]+_([a-z]pass|skimming|skimme
 
 /**
  * @typedef MonAlisaDataPassVersion
- * @param {string} name
- * @param {number} reconstructedEvents
- * @param {string} description
- * @param {number} outputSize
- * @param {number} lastSeen
+ * @property {string} name
+ * @property {number} reconstructedEvents
+ * @property {string} description
+ * @property {number} outputSize
+ * @property {number} lastSeen
  */
 
 /**
  * @typedef DataPassVersionDetails
- * @param {number[]} runNumbers
+ * @property {number[]} runNumbers
  */
 
 /**
  * @typedef MonAlisaSimulationPass
- * @param {string} name
- * @param {string} jiraID
- * @param {string} anchor_production
- * @param {string} anchor_pass
- * @param {string} runList
- * @param {string} generator
- * @param {string} requested_events
- * @param {string} generated_events
- * @param {string} output_size
+ * @property {string} name
+ * @property {string} jiraID
+ * @property {string} anchor_production
+ * @property {string} anchor_pass
+ * @property {string} runList
+ * @property {string} generator
+ * @property {string} requested_events
+ * @property {string} generated_events
+ * @property {string} output_size
  */
 
 /**
  * @typedef SimulationPassAssociations
- * @param {string[]} lhcPeriods
- * @param {string[]} dataPassesSuffixes
- * @param {number[]} runNumbers
+ * @property {string[]} lhcPeriods
+ * @property {string[]} dataPassesSuffixes
+ * @property {number[]} runNumbers
  */
 
 /**
diff --git a/lib/server/routers/dataPasses.router.js b/lib/server/routers/dataPasses.router.js
index 3e4fd0b5be..34f97d2547 100644
--- a/lib/server/routers/dataPasses.router.js
+++ b/lib/server/routers/dataPasses.router.js
@@ -12,6 +12,8 @@
  */
 
 const { DataPassesController } = require('../controllers/dataPasses.controller.js');
+const { rbacMiddleware } = require('../middleware/rbac.middleware.js');
+const { BkpRoles } = require('../../domain/enums/BkpRoles.js');
 
 exports.dataPassesRouter = {
     path: '/dataPasses',
@@ -19,6 +21,16 @@ exports.dataPassesRouter = {
     controller: DataPassesController.listDataPassesHandler,
 
     children: [
+        {
+            path: '/freeze',
+            method: 'patch',
+            controller: [rbacMiddleware([BkpRoles.DPG_ASYNC_QC_ADMIN]), DataPassesController.freezeHandler],
+        },
+        {
+            path: '/unfreeze',
+            method: 'patch',
+            controller: [rbacMiddleware([BkpRoles.DPG_ASYNC_QC_ADMIN]), DataPassesController.unfreezeHandler],
+        },
         {
             path: '/skimming',
 
diff --git a/lib/server/services/dataPasses/DataPassService.js b/lib/server/services/dataPasses/DataPassService.js
index b27fbf843e..b5ef0b64f4 100644
--- a/lib/server/services/dataPasses/DataPassService.js
+++ b/lib/server/services/dataPasses/DataPassService.js
@@ -25,8 +25,8 @@ const { LogManager } = require('@aliceo2/web-ui');
 
 /**
  * @typedef DataPassIdentifier
- * @param {number} [id]
- * @param {string} [name]
+ * @property {number} [id]
+ * @property {string} [name]
  */
 
 /**
@@ -150,6 +150,21 @@ class DataPassService {
         };
     }
 
+    /**
+     * Freeze/unfreeze a given data pass
+     *
+     * @param {DataPassIdentifier} dataPassIdentifier the identifier of the data pass to freeze/unfreeze
+     * @param {boolean} isFrozen if true, data pass will be frozen, else it will be unfrozen (independently of the current state)
+     * @return {Promise<void>} resolves once the data pass freeze state has been updated
+     */
+    async setFrozenState(dataPassIdentifier, isFrozen) {
+        return dataSource.transaction(async () => {
+            // Check that the data pass exists
+            const dataPass = await getOneDataPassOrFail(dataPassIdentifier);
+            await DataPassRepository.update(dataPass, { isFrozen });
+        });
+    }
+
     /**
      * Set given production as skimmable
      *
diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js
index fda0c33f80..42b992768b 100644
--- a/lib/server/services/qualityControlFlag/QcFlagService.js
+++ b/lib/server/services/qualityControlFlag/QcFlagService.js
@@ -15,6 +15,7 @@ const {
     repositories: {
         QcFlagRepository,
         DataPassQcFlagRepository,
+        DataPassRepository,
         SimulationPassQcFlagRepository,
         RunRepository,
         QcFlagVerificationRepository,
@@ -142,6 +143,10 @@ class QcFlagService {
 
             validateUserDetectorAccess(userRoles, detector.name);
 
+            if (dataPass?.isFrozen) {
+                throw new Error('Cannot create QC flag when datapass is frozen');
+            }
+
             const targetRun = await this._getQcTargetRun(
                 runNumber,
                 detector,
@@ -225,6 +230,10 @@ class QcFlagService {
                 throw new ConflictError('Cannot delete QC flag which is verified');
             }
 
+            if (qcFlag.dataPasses[0]?.isFrozen) {
+                throw new Error('Cannot delete QC flag when datapass is frozen');
+            }
+
             const dataPassId = qcFlag.dataPasses[0]?.id;
             const simulationPassId = qcFlag.simulationPasses[0]?.id;
 
@@ -290,6 +299,11 @@ class QcFlagService {
      */
     async deleteAllForDataPass(dataPassId) {
         return dataSource.transaction(async () => {
+            const dataPass = await DataPassRepository.findOne({ where: { id: dataPassId } });
+            if (!dataPass) {
+                throw new NotFoundError(`No datapass found with id ${dataPassId}`);
+            }
+
             // Sequelize destroy requires a where and can't work only using association
             const effectivePeriodIds = (await QcFlagEffectivePeriodRepository.findAll({
                 attributes: ['id'],
diff --git a/lib/server/services/run/RunService.js b/lib/server/services/run/RunService.js
index 779cdb1c8f..5e8d061a44 100644
--- a/lib/server/services/run/RunService.js
+++ b/lib/server/services/run/RunService.js
@@ -47,8 +47,8 @@ const { flpRoleService } = require('../flp/FlpRoleService.js');
 
 /**
  * @typedef MagnetsCurrentLevels level of current for ALICE magnets
- * @param {number} l3
- * @param {number} dipole
+ * @property {number} l3
+ * @property {number} dipole
  */
 
 /**
diff --git a/lib/server/services/simulationPasses/SimulationPassService.js b/lib/server/services/simulationPasses/SimulationPassService.js
index 57d02788e4..ef4d410ee7 100644
--- a/lib/server/services/simulationPasses/SimulationPassService.js
+++ b/lib/server/services/simulationPasses/SimulationPassService.js
@@ -20,8 +20,8 @@ const { getOneSimulationPassOrFail } = require('./getOneSimulationPassOrFail.js'
 
 /**
  * @typedef SimulationPassIdentifier
- * @param {number} [id]
- * @param {string} [name]
+ * @property {number} [id]
+ * @property {string} [name]
  */
 
 /**
diff --git a/test/api/dataPasses.test.js b/test/api/dataPasses.test.js
index 801c64861e..9612479a10 100644
--- a/test/api/dataPasses.test.js
+++ b/test/api/dataPasses.test.js
@@ -19,12 +19,15 @@ const { SkimmingStage } = require('../../lib/domain/enums/SkimmingStage');
 const { DataPassRepository, RunRepository, DataPassVersionRepository, DataPassRunRepository } = require('../../lib/database/repositories');
 const { dataPassService } = require('../../lib/server/services/dataPasses/DataPassService');
 const { Op } = require('sequelize');
+const { buildUrl } = require('../../lib/utilities/buildUrl.js');
+const { BkpRoles } = require('../../lib/domain/enums/BkpRoles.js');
 
 const LHC22b_apass1 = {
     id: 1,
     name: 'LHC22b_apass1',
     pdpBeamType: 'pp',
     skimmingStage: SkimmingStage.SKIMMABLE,
+    isFrozen: false,
     versions: [
         {
             id: 1,
@@ -353,4 +356,32 @@ module.exports = () => {
             ]);
         });
     });
+
+    describe('PATCH /api/dataPasses/freeze', async () => {
+        it('should successfully freeze a given data pass', async () => {
+            const dataPassId = 1;
+            const response = await request(server).patch(buildUrl(
+                '/api/dataPasses/freeze',
+                { dataPassId, token: BkpRoles.DPG_ASYNC_QC_ADMIN },
+            )).send({});
+            expect(response.status).to.be.equal(204);
+
+            const { isFrozen } = await dataPassService.getByIdentifier({ id: dataPassId });
+            expect(isFrozen).to.be.true;
+        });
+    });
+
+    describe('PATCH /api/dataPasses/freeze', async () => {
+        it('should successfully unfreeze a given data pass', async () => {
+            const dataPassId = 1;
+            const response = await request(server).patch(buildUrl(
+                '/api/dataPasses/unfreeze',
+                { dataPassId, token: BkpRoles.DPG_ASYNC_QC_ADMIN },
+            )).send({});
+            expect(response.status).to.be.equal(204);
+
+            const { isFrozen } = await dataPassService.getByIdentifier({ id: dataPassId });
+            expect(isFrozen).to.be.false;
+        });
+    });
 };
diff --git a/test/api/qcFlags.test.js b/test/api/qcFlags.test.js
index 12aded8026..a9bf0b7155 100644
--- a/test/api/qcFlags.test.js
+++ b/test/api/qcFlags.test.js
@@ -17,6 +17,7 @@ const { server } = require('../../lib/application');
 const { resetDatabaseContent } = require('../utilities/resetDatabaseContent.js');
 const { qcFlagService } = require('../../lib/server/services/qualityControlFlag/QcFlagService');
 const { BkpRoles } = require('../../lib/domain/enums/BkpRoles.js');
+const { dataPassService } = require('../../lib/server/services/dataPasses/DataPassService.js');
 
 module.exports = () => {
     before(resetDatabaseContent);
@@ -495,6 +496,33 @@ module.exports = () => {
                 },
             ]);
         });
+
+        it('should fail to create a QC flag on a frozen data pass', async () => {
+            const qcFlagCreationParameters = {
+                from: new Date('2019-08-09 01:29:50').getTime(),
+                to: new Date('2019-08-09 05:40:00').getTime(),
+                comment: 'VERY INTERESTING REMARK',
+                flagTypeId: 2,
+                runNumber: 106,
+                dataPassId: 1,
+                dplDetectorId: 1,
+            };
+
+            await dataPassService.setFrozenState({ id: 1 }, true);
+
+            const response = await request(server).post('/api/qcFlags?token=admin').send(qcFlagCreationParameters);
+            expect(response.status).to.be.equal(400);
+            const { errors } = response.body;
+            expect(errors).to.be.eql([
+                {
+                    status: 400,
+                    title: 'Service unavailable',
+                    detail: 'Cannot create QC flag when datapass is frozen',
+                },
+            ]);
+
+            await dataPassService.setFrozenState({ id: 1 }, false);
+        });
     });
 
     describe('DELETE /api/qcFlags/:id', () => {
@@ -511,6 +539,7 @@ module.exports = () => {
                 },
             ]);
         });
+
         it('should fail to delete QC flag when not being admin', async () => {
             const id = 5;
             const response = await request(server).delete(`/api/qcFlags/${id}`);
@@ -523,6 +552,23 @@ module.exports = () => {
                 },
             ]);
         });
+
+        it('should fail to delete a QC flag in a frozen data pass', async () => {
+            await dataPassService.setFrozenState({ id: 1 }, true);
+            const id = 2;
+            const response = await request(server).delete(`/api/qcFlags/${id}?token=admin`);
+
+            expect(response.status).to.be.equal(400);
+            expect(response.body).to.eql({
+                errors: [{
+                    status: 400,
+                    title: 'Service unavailable',
+                    detail: 'Cannot delete QC flag when datapass is frozen',
+                }],
+            });
+            await dataPassService.setFrozenState({ id: 1 }, false);
+        });
+
         it('should successfully delete QC flag as admin', async () => {
             const id = 2;
             const response = await request(server).delete(`/api/qcFlags/${id}?token=admin`);
diff --git a/test/lib/server/services/dataPasses/DataPassService.test.js b/test/lib/server/services/dataPasses/DataPassService.test.js
index 65f7a8b14f..42782c98eb 100644
--- a/test/lib/server/services/dataPasses/DataPassService.test.js
+++ b/test/lib/server/services/dataPasses/DataPassService.test.js
@@ -28,6 +28,7 @@ const LHC22b_apass1 = {
     name: 'LHC22b_apass1',
     pdpBeamType: 'pp',
     skimmingStage: SkimmingStage.SKIMMABLE,
+    isFrozen: false,
     versions: [
         {
             id: 1,
@@ -221,4 +222,40 @@ module.exports = () => {
             ]);
         });
     });
+
+    describe('Updating', () => {
+        it('should successfully freeze a given data pass', async () => {
+            await dataPassService.setFrozenState({ id: 1 }, true);
+            const dataPass = await dataPassService.getByIdentifier({ id: 1 });
+            expect(dataPass.isFrozen).to.be.true;
+        });
+
+        it('should fail to freeze a not existing data pass', async () => {
+            await assert.rejects(
+                () => dataPassService.setFrozenState({ name: 'do-not-exist' }, true),
+                new NotFoundError('Data pass with this name (do-not-exist) could not be found'),
+            );
+            await assert.rejects(
+                () => dataPassService.setFrozenState({ id: 99999999 }, true),
+                new NotFoundError('Data pass with this id (99999999) could not be found'),
+            );
+        });
+
+        it('should successfully unfreeze a given data pass', async () => {
+            await dataPassService.setFrozenState({ id: 1 }, false);
+            const dataPass = await dataPassService.getByIdentifier({ id: 1 });
+            expect(dataPass.isFrozen).to.be.false;
+        });
+
+        it('should fail to unfreeze a not existing data pass', async () => {
+            await assert.rejects(
+                () => dataPassService.setFrozenState({ name: 'do-not-exist' }, false),
+                new NotFoundError('Data pass with this name (do-not-exist) could not be found'),
+            );
+            await assert.rejects(
+                () => dataPassService.setFrozenState({ id: 99999999 }, false),
+                new NotFoundError('Data pass with this id (99999999) could not be found'),
+            );
+        });
+    });
 };
diff --git a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js
index 1b0bf3781a..a3d3c18d3d 100644
--- a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js
+++ b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js
@@ -23,6 +23,7 @@ const { runService } = require('../../../../../lib/server/services/run/RunServic
 const { gaqDetectorService } = require('../../../../../lib/server/services/gaq/GaqDetectorsService');
 const { gaqService } = require('../../../../../lib/server/services/qualityControlFlag/GaqService.js');
 const { qcFlagSummaryService } = require('../../../../../lib/server/services/qualityControlFlag/QcFlagSummaryService.js');
+const { dataPassService } = require('../../../../../lib/server/services/dataPasses/DataPassService.js');
 
 /**
  * Get effective part and periods of Qc flag
@@ -295,7 +296,7 @@ module.exports = () => {
         });
     });
 
-    describe('Creating Quality Control Flag for data pass', () => {
+    describe('Creating Quality Control Flag for data pass', async () => {
         it('should fail to create quality control flag due to incorrect external user id', async () => {
             const qcFlag = {
                 from: new Date('2019-08-09 01:29:50').getTime(),
@@ -415,6 +416,37 @@ module.exports = () => {
             );
         });
 
+        it('should fail to create QC flag on a frozen data pass', async () => {
+            const qcFlag = [
+                {
+                    from: new Date('2019-08-09 01:29:50').getTime(),
+                    to: new Date('2019-08-09 03:20:00').getTime(),
+                    comment: 'Very interesting remark',
+                    flagTypeId: 3,
+                },
+            ];
+
+            const scope = {
+                runNumber: 106,
+                dataPassIdentifier: { id: 1 },
+                detectorIdentifier: { detectorId: 1 },
+            };
+
+            const relations = { user: { roles: ['admin'], externalUserId: 456 } };
+
+            // Freeze temporary data pass 1
+
+            await dataPassService.setFrozenState({ id: 1 }, true);
+
+            await assert.rejects(
+                () => qcFlagService.create(qcFlag, scope, relations),
+                // eslint-disable-next-line max-len
+                new BadParameterError('Cannot create QC flag when datapass is frozen'),
+            );
+
+            await dataPassService.setFrozenState({ id: 1 }, false);
+        });
+
         it('should successfully create quality control and update ', async () => {
             const qcFlags = [
                 {
@@ -1151,7 +1183,7 @@ module.exports = () => {
             expect(fetchedFlagWithSimulationPass.simulationPasses.map(({ id }) => id)).to.have.all.members([scope.simulationPassIdentifier.id]);
         });
 
-        it('should throw when trying to create a flag with data pass and simulation pass at the same time', async () => {
+        it('should fail to create a flag with data pass and simulation pass at the same time', async () => {
             const scope = {
                 runNumber: 106,
                 dataPassIdentifier: { id: 1 },
@@ -1238,7 +1270,7 @@ module.exports = () => {
         });
     });
 
-    describe('Deleting Quality Control Flag', () => {
+    describe('Deleting Quality Control Flag', async () => {
         const runNumber = 445566;
         const dataPassId = 3;
         const detectorId = 1;
@@ -1276,6 +1308,17 @@ module.exports = () => {
             );
         });
 
+        it('should fail to delete QC flag of frozen data pass', async () => {
+            const id = 1;
+
+            await dataPassService.setFrozenState({ id: 1 }, true);
+            await assert.rejects(
+                () => qcFlagService.delete(id),
+                new ConflictError('Cannot delete QC flag when datapass is frozen'),
+            );
+            await dataPassService.setFrozenState({ id: 1 }, false);
+        });
+
         it('should successfully delete QC flag of dataPass', async () => {
             const id = 1;
 
@@ -1460,7 +1503,14 @@ module.exports = () => {
         });
     });
 
-    describe('Verifying Quality Control Flag', () => {
+    describe('Verifying Quality Control Flag', async () => {
+        it('should throw when trying to validate deleted QC flag', async () => {
+            await assert.rejects(
+                () => qcFlagService.verifyFlag({ flagId: 1, comment: 'Verification' }, {}),
+                new BadParameterError('QC flag 1 is already discarded and cannot be verified'),
+            );
+        });
+
         it('should successfully verify QC flag when not being owner', async () => {
             const qcFlag = {
                 flagId: 3,
@@ -1485,6 +1535,7 @@ module.exports = () => {
                     comment,
                 });
             }
+
             {
                 const fetchedQcFlag = await qcFlagService.getById(qcFlag.flagId);
                 const { verifications } = fetchedQcFlag;
@@ -1498,11 +1549,44 @@ module.exports = () => {
             }
         });
 
-        it('should throw when trying to validate deleted QC flag', async () => {
-            await assert.rejects(
-                () => qcFlagService.verifyFlag({ flagId: 1, comment: 'Verification' }, {}),
-                new BadParameterError('QC flag 1 is already discarded and cannot be verified'),
-            );
+        it('should successfully verify QC flag when data pass is frozen', async () => {
+            const qcFlag = {
+                flagId: 3,
+                comment: 'Some other comments',
+            };
+
+            const relations = {
+                user: { roles: ['det-cpv'], userId: 1 },
+            };
+
+            {
+                await dataPassService.setFrozenState({ id: 1 }, true);
+                const verifiedFlag = await qcFlagService.verifyFlag(qcFlag, relations);
+                await dataPassService.setFrozenState({ id: 1 }, false);
+
+                const { verifications } = verifiedFlag;
+                expect(verifications).to.be.an('array');
+                expect(verifications).to.be.lengthOf(2);
+                const [, { createdBy, createdById, comment, flagId }] = verifications;
+                expect({ flagId, createdBy, createdById, comment }).to.eql({
+                    flagId: qcFlag.flagId,
+                    createdById: 1,
+                    createdBy: { id: 1, externalId: 1, name: 'John Doe' },
+                    comment,
+                });
+            }
+
+            {
+                const fetchedQcFlag = await qcFlagService.getById(qcFlag.flagId);
+                const { verifications } = fetchedQcFlag;
+                const [, { createdBy, createdById, comment, flagId }] = verifications;
+                expect({ createdBy, createdById, comment, flagId }).to.be.eql({
+                    flagId: qcFlag.flagId,
+                    createdById: 1,
+                    createdBy: { id: 1, externalId: 1, name: 'John Doe' },
+                    comment,
+                });
+            }
         });
     });
 
diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js
index 439e0e0d11..6390ac369e 100644
--- a/test/public/runs/runsPerDataPass.overview.test.js
+++ b/test/public/runs/runsPerDataPass.overview.test.js
@@ -459,10 +459,58 @@ module.exports = () => {
         await expectInnerText(page, '#row108-readyForSkimming', 'not ready');
     });
 
+    describe('Freeze/unfreeze of data pass', async () => {
+        before(async () => {
+            await page.evaluate((role) => {
+                // eslint-disable-next-line no-undef
+                sessionService.get().token = role;
+
+                // eslint-disable-next-line no-undef
+                sessionService.get().access.push(role);
+            }, BkpRoles.DPG_ASYNC_QC_ADMIN);
+        });
+
+        it('should successfully freeze a given data pass', async () => {
+            await pressElement(page, '#actions-dropdown-button .popover-trigger', true);
+            const popoverSelector = await getPopoverSelector(await page.waitForSelector('#actions-dropdown-button .popover-trigger'));
+
+            await expectInnerText(page, `${popoverSelector} button:nth-child(3)`, 'Freeze the data pass');
+            await pressElement(page, `${popoverSelector} button:nth-child(3)`);
+        });
+
+        it('should successfully disable QC flag creation when data pass is frozen', async () => {
+            await waitForTableLength(page, 3);
+            await page.waitForSelector('.select-multi-flag', { hidden: true });
+            await pressElement(page, '#actions-dropdown-button .popover-trigger');
+            await page.waitForSelector('#set-qc-flags-trigger[disabled]');
+            await page.waitForSelector('#row107-ACO-text button[disabled]');
+        });
+
+        it('should successfully un-freeze a given data pass', async () => {
+            const popoverSelector = await getPopoverSelector(await page.waitForSelector('#actions-dropdown-button .popover-trigger'));
+
+            await expectInnerText(page, `${popoverSelector} button:nth-child(3)`, 'Unfreeze the data pass');
+            await pressElement(page, `${popoverSelector} button:nth-child(3)`);
+        });
+
+        it('should successfully enable QC flag creation when data pass is un-frozen', async () => {
+            await waitForTableLength(page, 3);
+            await pressElement(page, '.select-multi-flag');
+            await pressElement(page, '#actions-dropdown-button .popover-trigger');
+            await page.waitForSelector('#set-qc-flags-trigger[disabled]', { hidden: true });
+            await page.waitForSelector('#set-qc-flags-trigger');
+            await page.waitForSelector('#row107-ACO-text a');
+        });
+
+        after(async () => {
+            await pressElement(page, '#actions-dropdown-button .popover-trigger', true);
+        });
+    });
+
     it('should successfully not display button to discard all QC flags for the data pass', async () => {
         await pressElement(page, '#actions-dropdown-button .popover-trigger', true);
         const popoverSelector = await getPopoverSelector(await page.waitForSelector('#actions-dropdown-button .popover-trigger'));
-        await page.waitForSelector(`${popoverSelector} button:nth-child(3)`, { hidden: true });
+        await page.waitForSelector(`${popoverSelector} button:nth-child(4)`, { hidden: true });
         await pressElement(page, '#actions-dropdown-button .popover-trigger', true);
     });
 
@@ -478,7 +526,7 @@ module.exports = () => {
         // Press again actions dropdown to re-trigger render
         await pressElement(page, '#actions-dropdown-button .popover-trigger', true);
         await setConfirmationDialogToBeAccepted(page);
-        await pressElement(page, `${popoverSelector} button:nth-child(3)`, true);
+        await pressElement(page, `${popoverSelector} button:nth-child(4)`, true);
         await pressElement(page, '#actions-dropdown-button .popover-trigger', true);
         await waitForTableLength(page, 3);
         // Processing of data might take a bit of time, but then expect QC flag button to be there


Reviews:
user: login: graduta
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-09T10:15:05Z
user: login: martinboulais
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:16:25Z
user: login: graduta
body: Thank you for the quick updates!
Now that you have added the back-end check for user's role, it looks like there is a bug in the front-end when:
- clickin on Freeze
- server returns 403
- error is not displayed to the user but in the console I can see a problem:
```
Uncaught (in promise)
freezeDataPass http://localhost:4000/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js:163
```
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-09T14:45:17Z
user: login: graduta
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T10:11:27Z

Review comments:
diff_hunk: @@ -415,6 +416,37 @@ module.exports = () => {
             );
         });
 
+        it('should fail to create QC flag on a frozen data pass', async () => {
+            const qcFlag = [
+                {
+                    from: new Date('2019-08-09 01:29:50').getTime(),
+                    to: new Date('2019-08-09 03:20:00').getTime(),
+                    comment: 'Very interesting remark',
+                    flagTypeId: 3,
+                },
+            ];
+
+            const scope = {
+                runNumber: 106,
+                dataPassIdentifier: { id: 1 },
+                detectorIdentifier: { detectorId: 1 },
+            };
+
+            const relations = { user: { roles: ['admin'], externalUserId: 456 } };
+
+            // Freeze temporary data pass 1
+
+            await dataPassService.setFrozenState({ id: 1 }, true);
+
+            await assert.rejects(
+                () => qcFlagService.create(qcFlag, scope, relations),
+                // eslint-disable-next-line max-len
path: test/lib/server/services/qualityControlFlag/QcFlagService.test.js
user: login: graduta
body: Given the use of new line, I do not think the disable of eslint is still needed here
created_at: 2025-04-09T09:51:51Z
author_association: MEMBER
diff_hunk: @@ -1460,7 +1503,14 @@ module.exports = () => {
         });
     });
 
-    describe('Verifying Quality Control Flag', () => {
+    describe('Verifying Quality Control Flag', async () => {
+        it('should throw when trying to validate deleted QC flag', async () => {
path: test/lib/server/services/qualityControlFlag/QcFlagService.test.js
user: login: graduta
body: Is it `validate` the same as the `verify`? If so, perhaps we only use one throughout the code base? 
created_at: 2025-04-09T09:52:47Z
author_association: MEMBER
diff_hunk: @@ -221,4 +222,18 @@ module.exports = () => {
             ]);
         });
     });
+
+    describe('Updating', () => {
path: test/lib/server/services/dataPasses/DataPassService.test.js
user: login: graduta
body: Given that the `DataPassService` might reject the toggle of freeze flag, should we perhaps have one test on that as well?
created_at: 2025-04-09T09:56:11Z
author_association: MEMBER
diff_hunk: @@ -19,6 +19,16 @@ exports.dataPassesRouter = {
     controller: DataPassesController.listDataPassesHandler,
 
     children: [
+        {
path: lib/server/routers/dataPasses.router.js
user: login: graduta
body: Can you please confirm there is no user permission needed to freeze/unfreeze? 
I see in the front-end check that the user should have a certain role. It would be wise to add it on back-end as well, right?
created_at: 2025-04-09T09:57:28Z
author_association: MEMBER
path: lib/server/controllers/dataPasses.controller.js
user: login: graduta
body: Given that the only difference in this action is the flag that is to be set `true/false` would it not make sense to have only one handler so that we reduce duplication of code?
The DataPassService uses only one method already
created_at: 2025-04-09T09:58:54Z
author_association: MEMBER
diff_hunk: @@ -35,6 +35,7 @@ class DataPassAdapter {
             name,
             versions,
             skimmingStage,
+            frozen,
path: lib/database/adapters/DataPassAdapter.js
user: login: graduta
body: Normally for boolean values I would recommend using the descriptive convention, in this case`isFrozen`.
What do you think?
created_at: 2025-04-09T10:03:33Z
author_association: MEMBER
diff_hunk: @@ -247,17 +247,28 @@ export const RunsPerDataPassOverviewPage = ({
                                         dataPassId,
                                     },
                                 ),
-                                sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && h(
-                                    'button.btn.btn-danger',
-                                    {
-                                        onclick: () => {
-                                            if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) {
-                                                perDataPassOverviewModel.discardAllQcFlags();
-                                            }
+                                sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && [
+                                    h(
+                                        'button.btn.btn-danger',
+                                        {
+                                            onclick: () => dataPass.frozen
+                                                ? perDataPassOverviewModel.unfreezeDataPass()
+                                                : perDataPassOverviewModel.freezeDataPass(),
                                         },
-                                    },
-                                    'Delete ALL QC flags',
-                                ),
+                                        `${dataPass.frozen ? 'Unfreeze' : 'Freeze'} the data pass`,
+                                    ),
+                                    !dataPass.frozen && h(
path: lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js
user: login: graduta
body: I think it will be a bit confusing for the user not to see the delete button when the pass is frozen. What do you think about disabling the delete button with an informative message? Similar to for example the `+ QC Flag` button
created_at: 2025-04-09T10:11:41Z
author_association: MEMBER
diff_hunk: @@ -1460,7 +1503,14 @@ module.exports = () => {
         });
     });
 
-    describe('Verifying Quality Control Flag', () => {
+    describe('Verifying Quality Control Flag', async () => {
+        it('should throw when trying to validate deleted QC flag', async () => {
path: test/lib/server/services/qualityControlFlag/QcFlagService.test.js
user: login: martinboulais
body: I agree with you, but this change is simply that I moved the code from bellow. I suppose this rename do not belong to this PR?
created_at: 2025-04-09T13:57:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -415,6 +416,37 @@ module.exports = () => {
             );
         });
 
+        it('should fail to create QC flag on a frozen data pass', async () => {
+            const qcFlag = [
+                {
+                    from: new Date('2019-08-09 01:29:50').getTime(),
+                    to: new Date('2019-08-09 03:20:00').getTime(),
+                    comment: 'Very interesting remark',
+                    flagTypeId: 3,
+                },
+            ];
+
+            const scope = {
+                runNumber: 106,
+                dataPassIdentifier: { id: 1 },
+                detectorIdentifier: { detectorId: 1 },
+            };
+
+            const relations = { user: { roles: ['admin'], externalUserId: 456 } };
+
+            // Freeze temporary data pass 1
+
+            await dataPassService.setFrozenState({ id: 1 }, true);
+
+            await assert.rejects(
+                () => qcFlagService.create(qcFlag, scope, relations),
+                // eslint-disable-next-line max-len
path: test/lib/server/services/qualityControlFlag/QcFlagService.test.js
user: login: martinboulais
body: I agree, done
created_at: 2025-04-09T13:57:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,4 +222,18 @@ module.exports = () => {
             ]);
         });
     });
+
+    describe('Updating', () => {
path: test/lib/server/services/dataPasses/DataPassService.test.js
user: login: martinboulais
body: I agree, added
created_at: 2025-04-09T14:04:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -415,6 +416,37 @@ module.exports = () => {
             );
         });
 
+        it('should fail to create QC flag on a frozen data pass', async () => {
+            const qcFlag = [
+                {
+                    from: new Date('2019-08-09 01:29:50').getTime(),
+                    to: new Date('2019-08-09 03:20:00').getTime(),
+                    comment: 'Very interesting remark',
+                    flagTypeId: 3,
+                },
+            ];
+
+            const scope = {
+                runNumber: 106,
+                dataPassIdentifier: { id: 1 },
+                detectorIdentifier: { detectorId: 1 },
+            };
+
+            const relations = { user: { roles: ['admin'], externalUserId: 456 } };
+
+            // Freeze temporary data pass 1
+
+            await dataPassService.setFrozenState({ id: 1 }, true);
+
+            await assert.rejects(
+                () => qcFlagService.create(qcFlag, scope, relations),
+                // eslint-disable-next-line max-len
path: test/lib/server/services/qualityControlFlag/QcFlagService.test.js
user: login: martinboulais
body: Indeed, fixed
created_at: 2025-04-09T14:10:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1460,7 +1503,14 @@ module.exports = () => {
         });
     });
 
-    describe('Verifying Quality Control Flag', () => {
+    describe('Verifying Quality Control Flag', async () => {
+        it('should throw when trying to validate deleted QC flag', async () => {
path: test/lib/server/services/qualityControlFlag/QcFlagService.test.js
user: login: martinboulais
body: I agree, but I just moved the test here. This change is quite big and should be in a dedicated PR
created_at: 2025-04-09T14:10:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,16 @@ exports.dataPassesRouter = {
     controller: DataPassesController.listDataPassesHandler,
 
     children: [
+        {
path: lib/server/routers/dataPasses.router.js
user: login: martinboulais
body: There is, nice catch!
created_at: 2025-04-09T14:10:50Z
author_association: CONTRIBUTOR
path: lib/server/controllers/dataPasses.controller.js
user: login: martinboulais
body: I think that it is more clean to have `freeze` and `unfreeze` URLs than something like `set-frozen` I guess? 
Only the boilerplate code is dupplicated, no logic is dupplicated. What do you think?
created_at: 2025-04-09T14:13:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +35,7 @@ class DataPassAdapter {
             name,
             versions,
             skimmingStage,
+            frozen,
path: lib/database/adapters/DataPassAdapter.js
user: login: martinboulais
body: I agree, done
created_at: 2025-04-09T14:13:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -247,17 +247,28 @@ export const RunsPerDataPassOverviewPage = ({
                                         dataPassId,
                                     },
                                 ),
-                                sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && h(
-                                    'button.btn.btn-danger',
-                                    {
-                                        onclick: () => {
-                                            if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) {
-                                                perDataPassOverviewModel.discardAllQcFlags();
-                                            }
+                                sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && [
+                                    h(
+                                        'button.btn.btn-danger',
+                                        {
+                                            onclick: () => dataPass.frozen
+                                                ? perDataPassOverviewModel.unfreezeDataPass()
+                                                : perDataPassOverviewModel.freezeDataPass(),
                                         },
-                                    },
-                                    'Delete ALL QC flags',
-                                ),
+                                        `${dataPass.frozen ? 'Unfreeze' : 'Freeze'} the data pass`,
+                                    ),
+                                    !dataPass.frozen && h(
path: lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js
user: login: martinboulais
body: Agreed, but this is a leftover: the button should still be there even if the datapass is frozen
created_at: 2025-04-09T14:15:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-07T14:15:43Z
body: ## [Codecov](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) Report
Attention: Patch coverage is `29.54545%` with `62 lines` in your changes missing coverage. Please review.
> Project coverage is 44.79%. Comparing base [(`ea6ec5c`)](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/commit/ea6ec5c21b1156f5dd7ea178ad855694fbf01b60?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) to head [(`db382ae`)](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/commit/db382ae55ab72ab6c50c65daacfbc0d3fcc8bab9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) | Patch % | Lines |
|---|---|---|
| [...uns/RunPerDataPass/RunsPerDataPassOverviewModel.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fpublic%2Fviews%2FRuns%2FRunPerDataPass%2FRunsPerDataPassOverviewModel.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL3B1YmxpYy92aWV3cy9SdW5zL1J1blBlckRhdGFQYXNzL1J1bnNQZXJEYXRhUGFzc092ZXJ2aWV3TW9kZWwuanM=) | 0.00% | [28 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |
| [...Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fpublic%2Fviews%2FRuns%2FRunPerDataPass%2FRunsPerDataPassOverviewPage.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL3B1YmxpYy92aWV3cy9SdW5zL1J1blBlckRhdGFQYXNzL1J1bnNQZXJEYXRhUGFzc092ZXJ2aWV3UGFnZS5qcw==) | 0.00% | [16 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |
| [...reate/forDataPass/QcFlagCreationForDataPassPage.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fpublic%2Fviews%2FQcFlags%2FCreate%2FforDataPass%2FQcFlagCreationForDataPassPage.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL3B1YmxpYy92aWV3cy9RY0ZsYWdzL0NyZWF0ZS9mb3JEYXRhUGFzcy9RY0ZsYWdDcmVhdGlvbkZvckRhdGFQYXNzUGFnZS5qcw==) | 0.00% | [11 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |
| [...blic/components/qcFlags/qcFlagCreationPanelLink.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fpublic%2Fcomponents%2FqcFlags%2FqcFlagCreationPanelLink.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL3B1YmxpYy9jb21wb25lbnRzL3FjRmxhZ3MvcWNGbGFnQ3JlYXRpb25QYW5lbExpbmsuanM=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |
| [lib/server/controllers/dataPasses.controller.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fserver%2Fcontrollers%2FdataPasses.controller.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL3NlcnZlci9jb250cm9sbGVycy9kYXRhUGFzc2VzLmNvbnRyb2xsZXIuanM=) | 85.71% | [2 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |
| [...ions/v1/20250328072732-add-datapass-freeze-flag.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fdatabase%2Fmigrations%2Fv1%2F20250328072732-add-datapass-freeze-flag.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL2RhdGFiYXNlL21pZ3JhdGlvbnMvdjEvMjAyNTAzMjgwNzI3MzItYWRkLWRhdGFwYXNzLWZyZWV6ZS1mbGFnLmpz) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |
| [.../ActiveColumns/runDetectorsAsyncQcActiveColumns.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fpublic%2Fviews%2FRuns%2FActiveColumns%2FrunDetectorsAsyncQcActiveColumns.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL3B1YmxpYy92aWV3cy9SdW5zL0FjdGl2ZUNvbHVtbnMvcnVuRGV0ZWN0b3JzQXN5bmNRY0FjdGl2ZUNvbHVtbnMuanM=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |
| [...erver/services/qualityControlFlag/QcFlagService.js](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&filepath=lib%2Fserver%2Fservices%2FqualityControlFlag%2FQcFlagService.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group#diff-bGliL3NlcnZlci9zZXJ2aWNlcy9xdWFsaXR5Q29udHJvbEZsYWcvUWNGbGFnU2VydmljZS5qcw==) | 85.71% | [1 Missing :warning: ](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1881      +/-   ##
==========================================
- Coverage   44.82%   44.79%   -0.04%     
==========================================
  Files        1012     1013       +1     
  Lines       17005    17077      +72     
  Branches     3066     3077      +11     
==========================================
+ Hits         7622     7649      +27     
- Misses       9383     9428      +45     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/AliceO2Group/Bookkeeping/pull/1881?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AliceO2Group).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: martinboulais
created_at: 2025-04-10T07:45:55Z
body: > Thank you for the quick updates! Now that you have added the back-end check for user's role, it looks like there is a bug in the front-end when:
> 
>     * clickin on Freeze
> 
>     * server returns 403
> 
>     * error is not displayed to the user but in the console I can see a problem:
> 
> 
> ```
> Uncaught (in promise)
> freezeDataPass http://localhost:4000/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js:163
> ```

The error itself is because the frontend roles toggle is impacting only the frontend, not the backend.

You are perfectly correct about the missing error display, I fixed it
author_association: CONTRIBUTOR