This is the codebase for a digital implementation of the "FüSim MANV" (Führungssimulation Massenanfall von Verletzen), a German simulation system for training emergency medical services leadership personnel on how to manage Mass Casualty Incidents.
You can try it out at https://fuesim-manv.de/.
A screenshot of a part of an MCI exercise with initially ca. 50 patients at the Brandenburg Gate.
The concept is as follows:
This simulation has been designed in cooperation with and with support from the Federal Academy for Civil Protection and Civil Defence of the Federal Office of Civil Protection and Disaster Assistance Germany, who are the original copyright holders of the analog "FüSim MANV" simulation system, and the Malteser Hilfsdienst e.V. Berlin as well as the Johanniter Akademie NRW, Campus Münster der Johanniter-Unfall-Hilfe e.V.
The simulation is implemented as a web application with an Angular frontend and NodeJS backend.
This project is currently developed as a bachelor project at the HPI. You can find the official project website here.
--recurse-submodules
when cloning the repo or run git submodule update --init --recursive
if you have cloned the repo already to get its contents.git clone https://github.com/hpi-sam/digital-fuesim-manv
. To be able to run migration tests, you also have to clone the submodules: use git clone --recurse-submodules https://github.com/hpi-sam/digital-fuesim-manv
or run git submodule update --init --recursive
if you have cloned the repo already.npm run setup
from the root folder.env.example
file to ./.env
and adjust the settings as you need them. Note that some of the variables are explained under the next point.@recommended
filter in the extensions panel..vscode/*.example
. Crete a copy of those files removing the .example
and adjust them to your needs. The files without .example
-Extensions are untracked so your adjustments won't be committed automatically.If you are using vscode, you can run the task Start all
to start everything in one go.
Note that this tries to start the database using docker compose
. In case this fails please start the database in another way (see this section in the backend README).
If you're not using a database anyway, you could use the task Start all but database
instead.
/shared
and run npm run watch
/frontend
and run npm run start
/backend
and run npm run start
You need to have docker
installed.
docker compose
needs to be installed. Note that, depending on your setup, you may use docker-compose
instead of docker compose
. In this case, just replace the space in the commands with a dash (-
). For more information, see the relevant section of the documentation.docker compose up -d
in the root directory. This also starts the database. If you don't want to start the database run docker compose up -d digital-fuesim-manv
instead.docker run -p -d 80:80 digitalfuesimmanv/dfm
.The server will start listening using nginx on port 80
for all services (frontend, API, WebSockets).
Note the database requirements depicted in the installation section.
docker compose build
docker build -f docker/Dockerfile -t digital-fuesim-manv .
.env
(under Linux, this would be e.g. cp .env.example .env
)npm run prettier
or npm run prettier:windows
in the root to format all files and make the CI happy. Please use the vscode extension.npm run lint:fix
in the root to lint (and auto fix if possible) all files. Please use the vscode extension.There are already the following debug configurations for vscode saved:
Launch Frontend [Chrome]
Launch Frontend [Firefox]
(You have to install an extra extension)Debug Jest Tests
In addition, you can make use of the following browser extensions:
Whenever adding a new action or new state altering ui components (things that a user can use to alter the state in new ways) one should add exports of exercises in which the new features where tested to the Public test scenarios
The test scenarios are stored in a submodule. Use --recurse-submodules
when cloning the repo or run git submodule update --init --recursive
if you have cloned the repo already.
If you wish to run the migration tests use npm run test:migration
We are using Jest for our unit tests.
You can run it during the development
npm run test:watch
in the root, /shared
, /backend
or /frontend
folderWe are using cypress to run the end-to-end tests. You can find the code under /frontend/cypress
in the repository.
To run the tests locally, it is recommended to use the vscode task Start all & cypress
. Alternatively, you can start the frontend and backend manually and then run npm run cy:open
in /frontend
.
If you only want to check whether the tests pass, you can run npm run cy:run
in /frontend
instead.
You can run the benchmarks via npm run benchmark
in the root folder.
Look at the benchmark readme for more information.
Object
wherever possible.some(_item => ...)
)dependencies
should be used for packages that must be installed when running the app (e.g. express
), whereas devDependencies
should be used for packages only required for developing, debugging, building, or testing (e.g. jest
), which includes all @types
packages. We try to follow this route even for the frontend and the backend, although it is not important there. See also this answer on StackOverflow for more information about the differences./**
* Here is a description of the class/function/variable/etc.
*
* @param myParam a description of the parameter
* @returns a nice variable that is bigger than {@link myVariable}
* @throws myError when something goes wrong
*/
TODO
to mark things that need to be done later. Whether an issue should be created is an individual decision.
// TODO [engine:node@>=8]: We can use async/await now.
// TODO [typescript@>=4.9]: Use satisfies https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator
Version numbers follow the pattern ${major}.${minor}.${patch}
. major
, minor
and patch
are decimal numbers without leading zeroes, similar to SemVer. But since we do not have a public API, we do not adhere to SemVer.
The major version is updated for breaking changes, i.e. old state exports of configured exercises that have never been started, cannot be imported.
The minor version is updated with every release on main
. State exports of configured exercises from older minor versions that have never been started must successfully import and started exercises should be importable and behave consistently with older versions, although this is not strictly required.
The patch versions is incremented if and only if critical issues on main
are being fixed during a milestone.
Every time a part of the version number is updated, all numbers to the right are reset to zero.
For each new release, pull requests both to main
and dev
are created from the same release/
branch. For scheduled releases, such PRs are created by the Create Release PR
workflow.
With every significant PR into dev
, the change must be briefly described in CHANGELOG.md. Pay attention to Keep a Changelog.
The Create Release PR
workflow accepts a new version number, updates the version in all relevant source files and moves the Unreleased
section in CHANGELOG.md to a release heading, creating a new Unreleased
section. It then prepares two draft PRs, one into dev
and one into main
with these changes. They have to be marked as ready to run the pipeline and need approval. Merge them without rebase (use merge commit option).
Upon pushing to main
or dev
, GitHub Actions will build and push docker containers to Docker Hub tagged latest
and dev
. latest
is additionally tagged with the current version number on main
and a GitHub release is created.
This repository is a monorepo that consists of the following packages:
Each package has its own README.md
file with additional documentation. Please check them out before you start working on the project.
One server can host multiple exercises. Multiple clients can join an exercise. A client can only join one exercise at a time.
This is a real-time application.
Each client is connected to the server via a WebSocket connection. This means you can send and listen for events over a two-way communication channel. Via socket.io it is also possible to make use of a more classic request-response API via acknowledgments.
We borrow these core concepts from Redux.
A JSON object is an object whose properties are only the primitives string
, number
, boolean
or null
or another JSON object or an array of any of these (only state - no functions
).
Any object reference can't occur more than once anywhere in a JSON object (including nested objects). This means especially that no circular references are possible.
An immutable object is an object whose state cannot be modified after it is created. In the code immutability is conveyed via typescripts readonly and the helper type Immutable<T>
.
A state is an immutable JSON object. Each client as well as the server has a global state for an exercise. The single point of truth for all states of an exercise is the server. All these states should be synchronized.
You can find the exercise state here.
An action is an immutable JSON object that describes what should change in a state. The changes described by each action are atomic - this means either all or none of the changes described by an action are applied.
Actions cannot be applied in parallel. The order of actions is important.
It is a bad practice to encode part of the state in the action (or values derived/calculated from it). Instead, you should only read the state in the accompanying reducer.
A reducer is a pure function (no side effects!) that takes a state and an action of a specific type and returns a new state where the changes described in the action are applied. A state can only be modified by a reducer.
To be able to apply certain optimizations, it is advisable (but not necessary or guaranteed) that the reducer only changes the references of properties that have been changed.
You can find all exercise actions and reducers here. Please orient yourself on the already implemented actions, and don't forget to register them in shared/src/store/action-reducers/action-reducers.ts
It isn't necessary to copy the whole immutable object by value if it should be updated. Instead, only the objects that were modified should be shallow copied recursively. Immer provides a simple way to do this.
Because the state is immutable and reducers (should) only update the properties that have changed, you can short circuit in comparisons between immutable objects, if the references of objects in a property are equal. Therefore it is very performant to compare two states in the same context.
To save a state it is enough to save its reference. Therefore it is very performant as well. If the state would have to be changed, a new reference is created as the state is immutable.
Large values (images, large text, binary, etc.) are not directly stored in the state. Instead, the store only contains UUIDs that identify the blob. The blob can be retrieved via a separate (yet to be implemented) REST API.
The blob that belongs to a UUID cannot be changed or deleted while the state is still saved on the server. To change a blob, a new one should be uploaded and the old UUID in the state replaced with the new one.
If an action would add a new blobId to the state, the blob should have previously been uploaded to the server.
A blob should only be downloaded on demand (lazy) and cached.
getState
.performAction
and applied to them too. Due to the maintained packet ordering via a WebSocket and the fact that the synchronization of the state in the backend works synchronously, it is impossible for a client to receive actions out of order or receive actions already included in the state received by getState
.proposeAction
.performAction
.performAction
was broadcasted.A consequence of the synchronization strategy described before is that it takes one roundtrip from the client to the server and back to get the correct state on the client that initiated the action. This can lead to a bad user experience because of high latency.
This is where optimistic updates come into play. We just assume optimistically that the proposed action will be applied on the server. Therefore we can apply the action on the client directly without waiting for a performAction
from the server.
If the server rejects the proposal or a race condition occurs, the client corrects its state again. In our case, the optimisticActionHandler encapsulates this functionality.
The state in the frontend is not guaranteed to be correct. It is only guaranteed to automatically correct itself.
If you need to read from the state to change it, you should do this inside the action reducer because the currentState
passed into a reducer is always guaranteed to be correct.
Digital Fuesim MANV Copyright (C) 2023 See README.md#contributors for authors/contributors (bottom of this document).
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
Some files are excluded to the mentioned license, read LICENSE-README.md for further information to which files these applies.
Keep in mind, that if you don't use docker you have to run npm run licensing:all
before building the frontend
to give users the option to download a copy of the source code of this software (and a list of attributions of third-parties, e.g. their libraries) or use another way that lets users download the source code of this software for free. If you didn't modify the source code, a link to this github repository is enough, but you can do both. If you run npm run deployment
this command is already included.
Lukas Hagen 💻 👀 Student 2022/23 |
Nils Hanff 💻 👀 Student 2022/23 |
Benildur Nickel 💻 👀 Student 2022/23 |
Lukas Radermacher 💻 👀 Student 2022/23 |
Julian Schmidt 💻 👀 Student 2021/22 |
Clemens Schielicke 💻 👀 Student 2021/22 |
Florian Krummrey 💻 Student 2021/22 |
Marvin Müller-Mettnau 💻 📦 Student 2021/22 |
Matthias Barkowsky 📆 Supervisor 2021-23 |
Christian Zöllner 📆 Supervisor 2021-23 |