diff --git a/lib/server/api-docs/API.md b/lib/server/api-docs/API.md index 5441019907..40491c9aa2 100644 --- a/lib/server/api-docs/API.md +++ b/lib/server/api-docs/API.md @@ -548,3 +548,48 @@ curl -g -X POST "$BOOKKEEPING_URL/api/gaqDetectors?token=$TOKEN" \ -H 'content-type: application/json' \ -d '{"dataPassId":3,"runNumbers":[49,56],"detectorIds":[4,7]}' ``` + +## End of shift reports + +End-of-shift (EoS) reports written by the shifters. + +### `POST /eos-report` - Create the end-of-shift report of the calling user (not for LLM use) + +Operation id: `createEosReport` + +Creates the end-of-shift (EoS) report of the **pending shift** of the calling user (see `GET /shift-data`) as a log entry. The start of the shift is +deduced from the current time and must **not** be given in the body. + +The text of the report is generated from the body and from the data of the shift (issues, environments, runs...), the log is linked to the runs and to +the environments of the shift and is tagged with the tags of the shift type that exist. + +**Not meant for LLM or MCP tool use** (`x-llm-exclude`). + +| Parameter | In | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| `reportType` | query | string | yes | Kind of shift the report is about. | + +Request body (JSON): + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `shifterName` | string | no | Name of the shifter. | +| `traineeName` | string | no | | +| `lhcTransitions` | string | no | Transitions of the LHC machine during the shift. | +| `shiftFlow` | string | no | | +| `infoForNextShifter` | string | no | | +| `infoForRmRc` | string | no | Information for the run coordinator and the resource manager. | +| `runs` | array | no | Accepted but not used. | +| `typeSpecific` | object \| null | yes | Depends on `reportType`, and must be given (`null` for `SLIMOS`): - `ECS`: `{ environmentComments, runComments }`, objects mapping an environment id or a run number to a comment - `QC/PDP`: `{ runComments }` - `SL`: `{ magnets }` - `DCS`: `{ alerts }` | + +Responses: + +- `201`: The log entry created for the report. +- `400`: The request failed validation. HTTP status is 400 although each error's `status` field is `"422"`. `source.pointer` has the form `/data/attributes//`. +- `404`: The calling user is not known to Bookkeeping. + +```bash +curl -g -X POST "$BOOKKEEPING_URL/api/eos-report?reportType=ECS&token=$TOKEN" \ + -H 'content-type: application/json' \ + -d '{"shifterName":"John Doe","shiftFlow":"The shift flow","infoForNextShifter":"Info for the next shifter","typeSpecific":{"environmentComments":{"ENV1":"A comment about the environment"},"runComments":{"200":"A comment about the run"}}}' +``` diff --git a/lib/server/api-docs/index.html b/lib/server/api-docs/index.html index 0025838022..d0f22f83d4 100644 --- a/lib/server/api-docs/index.html +++ b/lib/server/api-docs/index.html @@ -986,6 +986,105 @@

GAQ detectors

-d '{"dataPassId":3,"runNumbers":[49,56],"detectorIds":[4,7]}' +

End of shift reports

+

End-of-shift (EoS) reports written by the shifters.

+
+

POST /eos-report - Create the end-of-shift report of the calling user not for LLM use

+ +

Operation id: createEosReport

+

Creates the end-of-shift (EoS) report of the pending shift of the calling user (see GET /shift-data) as a log entry. The start of the shift is +deduced from the current time and must not be given in the body.

+

The text of the report is generated from the body and from the data of the shift (issues, environments, runs...), the log is linked to the runs and to +the environments of the shift and is tagged with the tags of the shift type that exist.

+

Not meant for LLM or MCP tool use (x-llm-exclude).

+ + + + + + + + + + + + + + + + + +
ParameterInTypeRequiredNotes
reportTypequerystringyesKind of shift the report is about.
+

Request body (JSON):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredNotes
shifterNamestringnoName of the shifter.
traineeNamestringno
lhcTransitionsstringnoTransitions of the LHC machine during the shift.
shiftFlowstringno
infoForNextShifterstringno
infoForRmRcstringnoInformation for the run coordinator and the resource manager.
runsarraynoAccepted but not used.
typeSpecificobject | nullyesDepends on reportType, and must be given (null for SLIMOS): - ECS: { environmentComments, runComments }, objects mapping an environment id or a run number to a comment - QC/PDP: { runComments } - SL: { magnets } - DCS: { alerts }
+

Responses:

+
    +
  • 201: The log entry created for the report.
  • +
  • 400: The request failed validation. HTTP status is 400 although each error's status field is "422". source.pointer has the form /data/attributes/<query|params|body>/<field>.
  • +
  • 404: The calling user is not known to Bookkeeping.
  • +
+
curl -g -X POST "$BOOKKEEPING_URL/api/eos-report?reportType=ECS&token=$TOKEN" \
+  -H 'content-type: application/json' \
+  -d '{"shifterName":"John Doe","shiftFlow":"The shift flow","infoForNextShifter":"Info for the next shifter","typeSpecific":{"environmentComments":{"ENV1":"A comment about the environment"},"runComments":{"200":"A comment about the run"}}}'
+
+
Generated from openapi.yaml by npm run docs:api · ALICE Bookkeeping · internal use only
diff --git a/lib/server/api-docs/openapi.yaml b/lib/server/api-docs/openapi.yaml index bbb30a0192..229601b0e2 100644 --- a/lib/server/api-docs/openapi.yaml +++ b/lib/server/api-docs/openapi.yaml @@ -55,6 +55,8 @@ tags: description: LHC periods with statistics on their physics runs. - name: GAQ detectors description: Detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass. + - name: End of shift reports + description: End-of-shift (EoS) reports written by the shifters. paths: /tags: @@ -686,6 +688,54 @@ paths: errors: - { status: 404, title: Not found, detail: Data pass with this id (99999) could not be found } + /eos-report: + post: + tags: [End of shift reports] + operationId: createEosReport + x-llm-exclude: true + summary: Create the end-of-shift report of the calling user + description: | + Creates the end-of-shift (EoS) report of the **pending shift** of the calling user (see `GET /shift-data`) as a log entry. The start of the shift is + deduced from the current time and must **not** be given in the body. + + The text of the report is generated from the body and from the data of the shift (issues, environments, runs...), the log is linked to the runs and to + the environments of the shift and is tagged with the tags of the shift type that exist. + parameters: + - name: reportType + in: query + required: true + description: Kind of shift the report is about. + schema: { type: string, enum: [ECS, QC/PDP, SLIMOS, SL, DCS], examples: [ECS] } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CreateEosReport' } + example: + shifterName: John Doe + shiftFlow: The shift flow + infoForNextShifter: Info for the next shifter + typeSpecific: + environmentComments: { ENV1: A comment about the environment } + runComments: { '200': A comment about the run } + responses: + '201': + description: The log entry created for the report. + content: + application/json: + schema: { $ref: '#/components/schemas/EosReportResponse' } + example: + data: { id: 120, title: End of shift report - ECS - 17/03/2023 Morning } + '400': { $ref: '#/components/responses/ValidationError' } + '404': + description: The calling user is not known to Bookkeeping. + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + example: + errors: + - { status: 404, title: Not found, detail: User with this external id (1) could not be found } + components: securitySchemes: jwtQuery: @@ -1045,3 +1095,60 @@ components: data: type: array items: { $ref: '#/components/schemas/GaqDetectorEntry' } + CreateEosReport: + type: object + required: [typeSpecific] + properties: + shifterName: { type: string, description: Name of the shifter. } + traineeName: { type: string } + lhcTransitions: { type: string, description: Transitions of the LHC machine during the shift. } + shiftFlow: { type: string } + infoForNextShifter: { type: string } + infoForRmRc: { type: string, description: Information for the run coordinator and the resource manager. } + runs: + type: array + description: Accepted but not used. + items: { type: object, additionalProperties: true } + typeSpecific: + type: [object, 'null'] + description: | + Depends on `reportType`, and must be given (`null` for `SLIMOS`): + - `ECS`: `{ environmentComments, runComments }`, objects mapping an environment id or a run number to a comment + - `QC/PDP`: `{ runComments }` + - `SL`: `{ magnets }` + - `DCS`: `{ alerts }` + properties: + environmentComments: { type: object, additionalProperties: { type: string }, description: ECS only. Comment by environment id. } + runComments: { type: object, additionalProperties: { type: string }, description: ECS and QC/PDP only. Comment by run number. } + magnets: + type: object + description: SL only. Configuration of the magnets at the start and at the end of the shift, and in between. + properties: + start: { $ref: '#/components/schemas/MagnetsConfiguration' } + intermediates: + type: array + items: + type: object + required: [timestamp] + properties: + timestamp: { type: string, format: date-time } + magnetsConfiguration: { $ref: '#/components/schemas/MagnetsConfiguration' } + end: { $ref: '#/components/schemas/MagnetsConfiguration' } + alerts: { type: string, description: DCS only. The DCS alerts. } + MagnetsConfiguration: + type: object + properties: + solenoid: { type: string, examples: [30kA +452mT] } + dipole: { type: string, examples: [6kA +681mT] } + EosReportResponse: + type: object + required: [data] + properties: + data: + type: object + description: The log entry created for the report (see the Logs resource, not yet documented). + required: [id] + properties: + id: { type: integer } + title: { type: string, description: 'Title of the report, e.g. `End of shift report - ECS - 17/03/2023 Morning`.' } + additionalProperties: true diff --git a/lib/server/controllers/eosReport.controller.js b/lib/server/controllers/eosReport.controller.js index 12dedc0042..55cb918830 100644 --- a/lib/server/controllers/eosReport.controller.js +++ b/lib/server/controllers/eosReport.controller.js @@ -14,17 +14,26 @@ const { dtoValidator } = require('../utilities/index.js'); const { CreateEosReportDto } = require('../../domain/dtos/eosReport/CreateEosReportDto.js'); const { eosReportService } = require('../services/eosReport/EosReportService.js'); const { shiftService } = require('../services/shift/ShiftService.js'); +const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js'); -// eslint-disable-next-line jsdoc/require-param /** * Route handler to create an EOS report log entry + * + * @param {Object} request The *request* object represents the HTTP request and has properties for the request query + * string, parameters, body, HTTP headers, and so on. + * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets an + * HTTP request. + * @returns {Promise} resolves once the response has been sent */ const createEosReportHandler = async (request, response) => { const value = await dtoValidator(CreateEosReportDto, request, response); + if (!value) { + return; + } - const userIdentifier = { externalUserId: value?.session?.externalId }; + const userIdentifier = { externalUserId: value.session?.externalId }; - if (value) { + try { const content = await eosReportService.createLogEntry( value.query.reportType, { @@ -34,6 +43,8 @@ const createEosReportHandler = async (request, response) => { userIdentifier, ); response.status(201).json({ data: content }); + } catch (error) { + updateExpressResponseFromNativeError(response, error); } }; diff --git a/test/api/eosReport.test.js b/test/api/eosReport.test.js index c0c3283876..a44559904f 100644 --- a/test/api/eosReport.test.js +++ b/test/api/eosReport.test.js @@ -17,10 +17,20 @@ const { server } = require('../../lib/application.js'); const { expect } = require('chai'); const { emptyECSEosReportRequest } = require('../mocks/mock-ecs-eos-report.js'); +/** + * Return the validation errors of a response as an object mapping the pointer of each error to its detail + * + * @param {object} response the response + * @return {Object} the details of the errors by pointer + */ +const getValidationErrors = (response) => Object.fromEntries( + response.body.errors.map(({ source: { pointer }, detail }) => [pointer.replace('/data/attributes/', ''), detail]), +); + module.exports = () => { before(resetDatabaseContent); - describe('GET /api/eos-report', () => { + describe('POST /api/eos-report', () => { it('should successfully generate an ECS EOS report log and return its id', async () => { const reportRequest = { ...emptyECSEosReportRequest }; delete reportRequest.shiftStart; @@ -30,5 +40,29 @@ module.exports = () => { expect(response.body.data).to.be.an('object'); expect(response.body.data.id).to.equal(120); }); + + it('should return 400 if the report type is not a known one', async () => { + const response = await request(server) + .post('/api/eos-report?reportType=UNKNOWN') + .send({ shifterName: 'John Doe', typeSpecific: {} }); + + expect(response.status).to.equal(400); + expect(getValidationErrors(response)['query/reportType']) + .to.equal('"query.reportType" must be one of [ECS, QC/PDP, SLIMOS, SL, DCS]'); + }); + + it('should return 400 if the type specific part of the report is missing', async () => { + const response = await request(server).post('/api/eos-report?reportType=ECS').send({ shifterName: 'John Doe' }); + + expect(response.status).to.equal(400); + expect(getValidationErrors(response)).to.eql({ 'body/typeSpecific': '"body.typeSpecific" is required' }); + }); + + it('should return 400 if the start of the shift is given, as it is deduced from the current time', async () => { + const response = await request(server).post('/api/eos-report?reportType=ECS').send({ ...emptyECSEosReportRequest }); + + expect(response.status).to.equal(400); + expect(getValidationErrors(response)).to.eql({ 'body/shiftStart': '"body.shiftStart" is not allowed' }); + }); }); };