Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions lib/server/api-docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<query|params|body>/<field>`.
- `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"}}}'
```
99 changes: 99 additions & 0 deletions lib/server/api-docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,105 @@ <h2>GAQ detectors</h2>
-d &#39;{&quot;dataPassId&quot;:3,&quot;runNumbers&quot;:[49,56],&quot;detectorIds&quot;:[4,7]}&#39;
</code></pre>
</details>
<h2>End of shift reports</h2>
<p>End-of-shift (EoS) reports written by the shifters.</p>
<details>
<summary><h3><span class="method post">POST</span> <code>/eos-report</code> - Create the end-of-shift report of the calling user <span class="no-llm" title="Changes data: not meant for LLM or MCP tool use (x-llm-exclude)">not for LLM use</span></h3></summary>

<p>Operation id: <code>createEosReport</code></p>
<p>Creates the end-of-shift (EoS) report of the <strong>pending shift</strong> of the calling user (see <code>GET /shift-data</code>) as a log entry. The start of the shift is
deduced from the current time and must <strong>not</strong> be given in the body.</p>
<p>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.</p>
<p><strong>Not meant for LLM or MCP tool use</strong> (<code>x-llm-exclude</code>).</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>In</th>
<th>Type</th>
<th>Required</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td><code>reportType</code></td>
<td>query</td>
<td>string</td>
<td>yes</td>
<td>Kind of shift the report is about.</td>
</tr>
</tbody></table>
<p>Request body (JSON):</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td><code>shifterName</code></td>
<td>string</td>
<td>no</td>
<td>Name of the shifter.</td>
</tr>
<tr>
<td><code>traineeName</code></td>
<td>string</td>
<td>no</td>
<td></td>
</tr>
<tr>
<td><code>lhcTransitions</code></td>
<td>string</td>
<td>no</td>
<td>Transitions of the LHC machine during the shift.</td>
</tr>
<tr>
<td><code>shiftFlow</code></td>
<td>string</td>
<td>no</td>
<td></td>
</tr>
<tr>
<td><code>infoForNextShifter</code></td>
<td>string</td>
<td>no</td>
<td></td>
</tr>
<tr>
<td><code>infoForRmRc</code></td>
<td>string</td>
<td>no</td>
<td>Information for the run coordinator and the resource manager.</td>
</tr>
<tr>
<td><code>runs</code></td>
<td>array</td>
<td>no</td>
<td>Accepted but not used.</td>
</tr>
<tr>
<td><code>typeSpecific</code></td>
<td>object | null</td>
<td>yes</td>
<td>Depends on <code>reportType</code>, and must be given (<code>null</code> for <code>SLIMOS</code>): - <code>ECS</code>: <code>{ environmentComments, runComments }</code>, objects mapping an environment id or a run number to a comment - <code>QC/PDP</code>: <code>{ runComments }</code> - <code>SL</code>: <code>{ magnets }</code> - <code>DCS</code>: <code>{ alerts }</code></td>
</tr>
</tbody></table>
<p>Responses:</p>
<ul>
<li><code>201</code>: The log entry created for the report.</li>
<li><code>400</code>: The request failed validation. HTTP status is 400 although each error&#39;s <code>status</code> field is <code>&quot;422&quot;</code>. <code>source.pointer</code> has the form <code>/data/attributes/&lt;query|params|body&gt;/&lt;field&gt;</code>.</li>
<li><code>404</code>: The calling user is not known to Bookkeeping.</li>
</ul>
<pre><code class="language-bash">curl -g -X POST &quot;$BOOKKEEPING_URL/api/eos-report?reportType=ECS&amp;token=$TOKEN&quot; \
-H &#39;content-type: application/json&#39; \
-d &#39;{&quot;shifterName&quot;:&quot;John Doe&quot;,&quot;shiftFlow&quot;:&quot;The shift flow&quot;,&quot;infoForNextShifter&quot;:&quot;Info for the next shifter&quot;,&quot;typeSpecific&quot;:{&quot;environmentComments&quot;:{&quot;ENV1&quot;:&quot;A comment about the environment&quot;},&quot;runComments&quot;:{&quot;200&quot;:&quot;A comment about the run&quot;}}}&#39;
</code></pre>
</details>

</main>
<footer>Generated from openapi.yaml by <code>npm run docs:api</code> · ALICE Bookkeeping · internal use only</footer>
Expand Down
107 changes: 107 additions & 0 deletions lib/server/api-docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
17 changes: 14 additions & 3 deletions lib/server/controllers/eosReport.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>} 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,
{
Expand All @@ -34,6 +43,8 @@ const createEosReportHandler = async (request, response) => {
userIdentifier,
);
response.status(201).json({ data: content });
} catch (error) {
updateExpressResponseFromNativeError(response, error);
}
};

Expand Down
36 changes: 35 additions & 1 deletion test/api/eosReport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>} 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;
Expand All @@ -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' });
});
});
};
Loading