diff --git a/Dockerfile b/Dockerfile
index 756cc84e15c90c223d4b9b43494897e63e59cff7..e4a4ffd4a49e0975e7d4e85db3ca915edc9e9c1a 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,10 @@
 FROM nginx:stable-alpine
 
+# Empty by default
+ARG GIT_COMMIT_HASH=
+
+ENV VIDEOAG_API_GIT_COMMIT_HASH $GIT_COMMIT_HASH
+
 RUN mkdir -p /code
 WORKDIR /code
 
diff --git a/api_specification.md b/api_specification.md
index 7a12ff651d90d5a94636fb99283522105a8eefdc..883dcf838ad6dcc77e4589c8837b1189f3a2ca8f 100644
--- a/api_specification.md
+++ b/api_specification.md
@@ -1,4 +1,4 @@
-# Specification of the Web API for the Video-AG Website (v0.49).
+# Specification of the Web API for the Video-AG Website (v0.50).
 
 ## Introduction
 
@@ -49,34 +49,38 @@ The changelog can be found [at the end](#changelog).
 
 ## Routes
 
-### Miscellaneous
+### Site status
 
----
+The current site status can be retrieved with `GET /status`. It can be in three states
 
-#### `GET /status`
-
-###### Response:
+* `available`: The site is running and all routes can be used
+* `readonly`: The site cannot update any data. You can only call routes for which is specified:
+  * 'This may be used while the site is [read-only](#site-status).'
+* `unavailable`: The site is unavailable and the only route you can call is `GET /status`
 
-Returns `200 OK` or `503 SERVICE UNAVAILABLE`
-
-| Field  | Type      | Notes                                     |
-|--------|-----------|-------------------------------------------|
-| status | id_string | possible values: `running`, `unavailable` |
+### Miscellaneous
 
 ---
 
-#### `GET /announcements`
+#### `GET /status`
+
+This may be used even if the site is read-only or disabled.
 
 ###### Response:
 
-| Field         | Type                                   | Notes |
-|---------------|----------------------------------------|-------|
-| announcements | array of [announcement](#announcement) |       |
+| Field           | Type                                   | Notes                                                   |
+|-----------------|----------------------------------------|---------------------------------------------------------|
+| status          | id_string                              | possible values: `available`, `readonly`, `unavailable` |
+| is_debug        | boolean                                |                                                         |
+| git_commit_hash | ?string                                |                                                         |
+| announcements   | array of [announcement](#announcement) |                                                         |
 
 <details>
 <summary>Example: <code>GET /announcement</code></summary>
 <pre>
 {
+    "status": "running",
+    "is_debug": true,
     "announcements": [
         {
             "type": "info",
@@ -95,8 +99,21 @@ Returns `200 OK` or `503 SERVICE UNAVAILABLE`
 
 ---
 
+#### `GET /is_running`
+
+This may be used even if the site is read-only or disabled.
+
+###### Response:
+
+Returns `200 OK` if the site is ready to answer requests, otherwise `503 SERVICE UNAVAILABLE`. The status may still be
+`unavailable` if this return `200 OK`!
+
+---
+
 #### `GET /homepage`
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 A [homepage](#homepage) object.
@@ -144,6 +161,8 @@ included for moderators.
 
 #### `GET /courses`
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 | Field   | Type                     | Notes                                                           |
@@ -196,6 +215,8 @@ included for moderators.
 |------------------|----------|---------------------------------------|---------|
 | include_lectures | ?boolean | If `true`, `lectures` will be present | false   |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 A [course](#course) object.
@@ -274,6 +295,8 @@ A [course](#course) object.
 |---------------|------|-------|
 | lecture_id    | int  |       |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 | Field   | Type                | Notes                                            |
@@ -355,6 +378,8 @@ A [course](#course) object.
 |---------------|--------|-------------------------------------------------------------------------------------------|
 | search_term   | string | The term to search for. May not be empty (only whitespace) or longer than 1024 characters |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 | Field           | Type                         | Notes                                                                                                                                   |
@@ -405,6 +430,8 @@ Response: `200 OK`
 
 If the specified lecture does not exist or does not have password authentication, an [api error](#api_error) is returned.
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 `200 OK` if the username and password are correct for the specified lecture. Otherwise, an [api error](#api_error) (Usually `authentication_failed`).
@@ -423,6 +450,8 @@ The username and password are saved, so that different lectures with the same lo
 
 If the specified lecture does not exist, a (random) result or an [api error](#api_error) may be returned.
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 `200 OK` if login was successful. Otherwise, an [api error](#api_error) (Usually `authentication_failed`).
@@ -440,6 +469,8 @@ If the specified lecture does not exist, a (random) result or an [api error](#ap
 |-------|--------|-----------------------------------|
 | type  | string | Possible values: `rwth`, `moodle` |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 `202 ACCEPTED` with body:
@@ -461,6 +492,8 @@ status: `/authentication/status`.
 
 If the specified lecture does not exist, a (random) result or an [api error](#api_error) may be returned.
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 Also finishes the last authentication started with `/authentication/start`. If that authentication is not yet complete,
@@ -477,6 +510,8 @@ Also finishes the last authentication started with `/authentication/start`. If t
 
 #### `POST /authentication/moderator_logout`
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 Only logs moderators out.
@@ -520,6 +555,8 @@ restrictions may apply for which the configuration must be queried. Most notable
 | object_type   | id_string | Name of object (as used in this document) which has a configuration (See [above](#Objects-with-configuration-overview)) |
 | object_id     | int       |                                                                                                                         |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 Returns the configuration for the object of the specified type and id or an [api error](#api_error) if it does not exist.
@@ -657,6 +694,8 @@ Response: <code>200 OK</code>
 |---------------|-----------|-------------------------------------------------------------------------------------------------------------------------|
 | object_type   | id_string | Name of object (as used in this document) which has a configuration (See [above](#Objects-with-configuration-overview)) |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 Returns the fields which may need to be configured to create a new object. For the creation all returned fields **may** be specified. All fields which do not have a default value **must** be specified.
@@ -808,6 +847,8 @@ could not be restored (Does not exist, not deleted).
 | object_id        | ?int       | Only return entries for objects with this id                                                         |
 | field            | ?id_string | Only return entries for this field. `object_type` must be set                                        |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 | Field      | Type                                         | Notes |
@@ -823,6 +864,8 @@ The following routes are only for moderators.
 
 #### `GET /sorter/log`
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 | Field     | Type                                                       | Notes |
@@ -852,6 +895,8 @@ The following routes are only for moderators.
 
 #### `GET /users`
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 Returns a list of all users (moderators)
@@ -864,6 +909,8 @@ Returns a list of all users (moderators)
 
 #### `GET /user/me/settings`
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 A [settings](#settings) object
@@ -899,6 +946,8 @@ The following routes are only for moderators.
 | worker_id        | ?string    | Only get jobs for this worker. May be `none` to only get jobs assigned to no worker                  |
 | state            | ?job_state | Only get jobs in this state                                                                          |
 
+This may be used while the site is [read-only](#site-status).
+
 ###### Response:
 
 | Field      | Type                       | Notes |
@@ -986,14 +1035,14 @@ Additionally, the following objects may appear as the type of some field:
 
 #### announcement
 
-| Field                | Type                    | Notes                                                                              |
-|----------------------|-------------------------|------------------------------------------------------------------------------------|
-| id                   | int                     |                                                                                    |
-| type                 | announcement_type       |                                                                                    |
-| visibility           | announcement_visibility |                                                                                    |
-| text                 | +string                 |                                                                                    |
-| is_currently_visible | !boolean                | If false, this announcement currently cannot be seen by non-moderators             |
-| has_expired          | !boolean                | If true, this announcement has expired. Never true, if `is_currently_visible=true` |
+| Field                | Type                    | Notes                                                                                                                   |
+|----------------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------|
+| id                   | ?int                    | If the id is missing, this is a static announcement which can not be modified                                           |
+| type                 | announcement_type       |                                                                                                                         |
+| visibility           | announcement_visibility |                                                                                                                         |
+| text                 | +string                 |                                                                                                                         |
+| is_currently_visible | ?!boolean               | Not present on static announcements. If false, this announcement currently cannot be seen by non-moderators             |
+| has_expired          | ?!boolean               | Not present on static announcements. If true, this announcement has expired. Never true, if `is_currently_visible=true` |
 
 #### announcement_configuration
 
@@ -1456,7 +1505,7 @@ Possible `error_code`:
 | internal_server_error                 | 500       |                                                      |
 | lecture_has_no_password               | 400       |                                                      |
 | authentication_failed                 | 403       |                                                      |
-| authentication_servers_not_available  | 503       |                                                      |
+| authentication_not_available          | 503       |                                                      |
 | unauthorized                          | 401       |                                                      |
 | csrf_token_invalid                    | 403       | CSRF token must be included as `X-Csrf-Token` Header |
 | access_forbidden                      | 403       |                                                      |
@@ -1465,9 +1514,19 @@ Possible `error_code`:
 | object_error                          | 400       |                                                      |
 | modification_unexpected_current_value | 409       |                                                      |
 | too_many_suggestions                  | 403       |                                                      |
+| site_is_readonly                      | 503       |                                                      |
+| site_is_disabled                      | 503       |                                                      |
 
 ## Changelog
 
+### v0.50
+
+* Removed `GET /announcements`
+* Updated `GET /status`. Among other things, this now returns the announcements
+* Changed name of error type `authentication_servers_not_available` to `authentication_not_available`
+* Added error types `site_is_readonly` and `site_is_disabled`
+* Added `GET /is_running`
+
 ### v0.49
 
 * Added `GET /status`
diff --git a/config/live_config.json b/config/live_config.json
new file mode 100644
index 0000000000000000000000000000000000000000..d99ed9de3a06539a0801a69d2ee2d84a397c33fd
--- /dev/null
+++ b/config/live_config.json
@@ -0,0 +1,5 @@
+{
+  "readonly": false,
+  "disabled": false,
+  "static_announcements": []
+}
\ No newline at end of file
diff --git a/config/uwsgi_example.ini b/config/uwsgi_example.ini
index 994f5b7a42a8e881adfd184194ac07ed53aba254..29deeb6212cf18b83f96b6354e9ae7a4e65d552d 100644
--- a/config/uwsgi_example.ini
+++ b/config/uwsgi_example.ini
@@ -1,13 +1,22 @@
 [uwsgi]
+strict = true # Fail on invalid/unknown options
+
 uid = uwsgi
 gid = uwsgi
+
 chmod-socket = 666
-manage-script-name = true
 socket = /uwsgi/uwsgi.sock
+
+manage-script-name = true
 chdir = ./src/
 mount = /=app:api_app
-need-app = true
-enable-threads = true
-master = true
+need-app = true # Fail if startup throws exception
+
+master = true # Master manages our workers (in our case, just one worker)
+processes = 1 # One process. With multiple processes, global variables would not be shared
+lazy-apps = true # Fork before initializing application (Otherwise scheduler is running in master)
+single-interpreter = true
 enable-threads = true
+threads = 2
+
 # do not specify python plugin. uwsgi is already being installed with pip
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 9a60c89e7718d97e159c219f277ef152d46a9154..30f40bde8fd2404ed448a0da704c8e20d04cf473 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -9,5 +9,6 @@ services:
       host.docker.internal: host-gateway
     environment:
       - VIDEOAG_API_CONFIG=../config/api_example_config.py
+      - VIDEOAG_API_LIVE_CONFIG=../config/live_config.json
       - VIDEOAG_NGINX_CONFIG=/code/config/nginx_example.conf
       - VIDEOAG_UWSGI_CONFIG=/code/config/uwsgi_example.ini
\ No newline at end of file
diff --git a/src/api/__init__.py b/src/api/__init__.py
index 3fe56fed131bd28e742032461961ca62dff5b42d..29b46eb7ea80bdf5211a1e2cae5f827aae4bcc3b 100644
--- a/src/api/__init__.py
+++ b/src/api/__init__.py
@@ -1,2 +1,3 @@
 # noinspection PyUnresolvedReferences
 from app import api_app as app, api_config as config
+from api.live_config import config as live_config
diff --git a/src/api/announcement.py b/src/api/announcement.py
new file mode 100644
index 0000000000000000000000000000000000000000..3964771ed3464e6c7d876330c5bc6db75f76e7d5
--- /dev/null
+++ b/src/api/announcement.py
@@ -0,0 +1,56 @@
+from datetime import datetime
+
+from api.database import db_pool, PreparedStatement
+from api.authentication import is_moderator
+
+
+_SQL_GET_ANNOUNCEMENTS = PreparedStatement("""
+SELECT * FROM "announcements"
+WHERE NOT "deleted"
+    AND (? OR (
+            "visible"
+        AND (("time_expire" IS NULL) OR "time_expire" > ?)
+        AND (("time_publish" IS NULL) OR "time_publish" <= ?)
+    ))
+""")
+
+
+def query_announcements():
+    is_mod = is_moderator()
+    current_time: datetime = datetime.now()
+    with db_pool.start_read_transaction() as trans:
+        announcements_db = trans.execute_statement_and_close(
+            _SQL_GET_ANNOUNCEMENTS, is_mod, current_time, current_time)
+    
+    announcements_json = []
+    for announcement_db in announcements_db:
+        announcement_json = {
+            "id": announcement_db["id"],
+            "text": announcement_db["text"]
+        }
+        if is_mod:
+            time_publish = announcement_db["time_publish"]
+            time_expire = announcement_db["time_expire"]
+            has_not_expired = time_expire is None or time_expire > current_time
+            announcement_json["is_currently_visible"] = (announcement_db["visible"]
+                                                         and (time_publish is None or time_publish <= current_time)
+                                                         and has_not_expired)
+            announcement_json["has_expired"] = not has_not_expired
+        announcements_json.append(announcement_json)
+        level_db: int = announcement_db["level"]
+        if level_db == 0:
+            announcement_json["type"] = "info"
+            announcement_json["visibility"] = "only_main_page"
+        elif level_db == 1:
+            announcement_json["type"] = "info"
+            announcement_json["visibility"] = "all_pages"
+        elif level_db == 2:
+            announcement_json["type"] = "warning"
+            announcement_json["visibility"] = "all_pages"
+        elif level_db == 3:
+            announcement_json["type"] = "important"
+            announcement_json["visibility"] = "all_pages_and_embed"
+        else:
+            raise Exception("Unknown announcement level: " + str(level_db))
+    
+    return announcements_json
diff --git a/src/api/live_config.py b/src/api/live_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..8bb05cc2d88da823ada12be9f9f0c8c2477695b2
--- /dev/null
+++ b/src/api/live_config.py
@@ -0,0 +1,79 @@
+import os
+from pathlib import Path
+
+from api.miscellaneous import *
+from api.objects.types import TYPE_STRING_LONG
+
+_LIVE_CONFIG_PATH_STRING = os.environ.get("VIDEOAG_API_LIVE_CONFIG", None)
+_LIVE_CONFIG_PATH = None if _LIVE_CONFIG_PATH_STRING is None else Path(_LIVE_CONFIG_PATH_STRING).resolve()
+
+
+class LiveConfig:
+    
+    def __init__(self):
+        super().__init__()
+        self._disabled: bool = False
+        self._readonly: bool = False
+        self._static_announcements: list[dict] = []
+    
+    def reset(self):
+        self._disabled = False
+        self._readonly = False
+        self._static_announcements = []
+    
+    def update(self, json_config: CJsonObject):
+        """
+        Raises an exception if the file has wrong format/values. In this case, nothing is updated
+        """
+        disable = json_config.get_bool("disabled", False)
+        readonly = json_config.get_bool("readonly", False) or disable
+        static_announcements = []
+        unsafe_announcements = json_config.get_array("static_announcements")
+        if unsafe_announcements is not None:
+            for unsafe_announcement in unsafe_announcements:
+                unsafe_announcement = unsafe_announcement.as_object()
+                
+                type = unsafe_announcement.get_string("type", 100)
+                if type not in ["info", "warning", "important"]:
+                    unsafe_announcement.get("type").raise_error("Unknown type")
+                visibility = unsafe_announcement.get_string("visibility", 100)
+                if visibility not in ["only_main_page", "all_pages", "all_pages_and_embed"]:
+                    unsafe_announcement.get("visibility").raise_error("Unknown visibility")
+                text = unsafe_announcement.get_string("text", TYPE_STRING_LONG.get_max_string_length())
+                static_announcements.append({
+                    "type": type,
+                    "visibility": visibility,
+                    "text": text
+                })
+        
+        # Assign to global vars only if whole file parsed successfully
+        self._readonly = readonly
+        self._disabled = disable
+        self._static_announcements = static_announcements
+    
+    def is_disabled(self) -> bool:
+        return self._disabled
+    
+    def is_readonly(self) -> bool:
+        return self._readonly
+    
+    def get_static_announcements(self) -> list[dict]:
+        """
+        Dicts contain (as in the api specification): type, visibility, text
+        """
+        return self._static_announcements
+
+
+# Do not modify directly! Variable is imported by others
+config: LiveConfig = LiveConfig()
+
+
+@scheduled_function(10, 1)
+def _update_live_config():
+    if _LIVE_CONFIG_PATH is None:
+        return
+    if not _LIVE_CONFIG_PATH.exists():
+        config.reset()
+        return
+    config.update(CJsonObject(json.loads(_LIVE_CONFIG_PATH.read_text(encoding="UTF-8"))))
+    # Exception is caught and logged by scheduler
diff --git a/src/api/miscellaneous/__init__.py b/src/api/miscellaneous/__init__.py
index 51703ce6cfb82a28dbe2ff04b82a12b6a8347f3e..48441a1722c87110b868ff4e58bde31312a89211 100644
--- a/src/api/miscellaneous/__init__.py
+++ b/src/api/miscellaneous/__init__.py
@@ -23,7 +23,9 @@ from api.miscellaneous.errors import (ApiError, ApiClientException, api_on_error
                                       ERROR_UNKNOWN_OBJECT,
                                       ERROR_OBJECT_ERROR,
                                       ERROR_MODIFICATION_UNEXPECTED_CURRENT_VALUE,
-                                      ERROR_TOO_MANY_SUGGESTIONS)
+                                      ERROR_TOO_MANY_SUGGESTIONS,
+                                      ERROR_SITE_IS_READONLY,
+                                      ERROR_SITE_IS_DISABLED)
 from api.miscellaneous.rate_limiter import IntervalRateLimiter, HostBasedCounterRateLimiter, create_configured_host_rate_limiters
 from api.miscellaneous.diagnostics import DIAGNOSTICS_TRACKER, DiagnosticsCounter, DiagnosticsDataProvider
 from api.miscellaneous.scheduler import scheduled_function
diff --git a/src/api/miscellaneous/errors.py b/src/api/miscellaneous/errors.py
index b088f4609a4535cfcb736d19d074725e0f757ac1..d2cdb08954aaf2099d2aba2349fab98245ae6dc5 100644
--- a/src/api/miscellaneous/errors.py
+++ b/src/api/miscellaneous/errors.py
@@ -67,6 +67,10 @@ def ERROR_OBJECT_ERROR(message: str) -> ApiError:
                     message)
 ERROR_TOO_MANY_SUGGESTIONS = ApiError("too_many_suggestions", HTTP_403_FORBIDDEN,
                                       "Due to too many suggestion, no more are accepted")
+ERROR_SITE_IS_READONLY = ApiError("site_is_readonly", HTTP_503_SERVICE_UNAVAILABLE,
+                                  "The site is currently in read-only mode")
+ERROR_SITE_IS_DISABLED = ApiError("site_is_disabled", HTTP_503_SERVICE_UNAVAILABLE,
+                                  "The site is currently disabled")
 
 ALL_ERRORS_RANDOM = [
     ERROR_BAD_REQUEST(),
@@ -85,5 +89,7 @@ ALL_ERRORS_RANDOM = [
     ERROR_RATE_LIMITED,
     ERROR_UNKNOWN_OBJECT,
     ERROR_OBJECT_ERROR("?"),
-    ERROR_TOO_MANY_SUGGESTIONS
-]
\ No newline at end of file
+    ERROR_TOO_MANY_SUGGESTIONS,
+    ERROR_SITE_IS_READONLY,
+    ERROR_SITE_IS_DISABLED
+]
diff --git a/src/api/routes/site.py b/src/api/routes/site.py
index 4f9de73f70d360e07b5d2b615f5820a06d7afbad..57992c77029c8c2e81b470865d4790517f0a01d0 100644
--- a/src/api/routes/site.py
+++ b/src/api/routes/site.py
@@ -1,80 +1,46 @@
-from datetime import datetime, date, timedelta
+from datetime import date, timedelta
 
 from api.routes import *
 from api.course import (COURSE_SECONDARY_DB_SELECTION,
                         course_db_to_json_no_lectures, course_secondary_db_to_json_no_lectures,
                         lecture_db_to_json_no_chapters_media, lecture_queue_query_auth,
                         COURSE_SECONDARY_COLUMN_EMBED_INVISIBLE)
+from api.announcement import query_announcements
+
+_GIT_COMMIT_HASH = os.environ.get("VIDEOAG_API_GIT_COMMIT_HASH", None)
+if _GIT_COMMIT_HASH is not None and len(_GIT_COMMIT_HASH) == 0:
+    _GIT_COMMIT_HASH = None
 
 
 @api_route("/status", ["GET"])
 def api_route_status():
-    # noinspection PyBroadException
-    try:
-        with db_pool.start_read_transaction() as trans:
-            trans.execute_statement_and_close("SELECT NULL")
-    except Exception:
-        return {
-            "status": "unavailable"
-        }, HTTP_503_SERVICE_UNAVAILABLE
-    return {
-        "status": "running"
+    status = "available"
+    announcements = []
+    announcements.extend(api.live_config.get_static_announcements())
+    
+    if api.live_config.is_disabled():
+        status = "unavailable"
+    else:
+        if api.live_config.is_readonly():
+            status = "readonly"
+        # noinspection PyBroadException
+        try:
+            announcements.extend(query_announcements())
+        except Exception:
+            status = "unavailable"
+    response = {
+        "status": status,
+        "is_debug": DEBUG_ENABLED,
+        "announcements": announcements
     }
+    if _GIT_COMMIT_HASH is not None:
+        response["git_commit_hash"] = _GIT_COMMIT_HASH
+    return response
 
 
-_SQL_GET_ANNOUNCEMENTS = PreparedStatement("""
-SELECT * FROM "announcements"
-WHERE NOT "deleted"
-    AND (? OR (
-            "visible"
-        AND (("time_expire" IS NULL) OR "time_expire" > ?)
-        AND (("time_publish" IS NULL) OR "time_publish" <= ?)
-    ))
-""")
-
-
-@api_route("/announcements", ["GET"])
-def api_route_announcements():
-    is_mod = is_moderator()
-    current_time: datetime = datetime.now()
-    with db_pool.start_read_transaction() as trans:
-        announcements_db = trans.execute_statement_and_close(
-            _SQL_GET_ANNOUNCEMENTS, is_mod, current_time, current_time)
-    
-    announcements_json = []
-    for announcement_db in announcements_db:
-        announcement_json = {
-            "id": announcement_db["id"],
-            "text": announcement_db["text"]
-        }
-        if is_mod:
-            time_publish = announcement_db["time_publish"]
-            time_expire = announcement_db["time_expire"]
-            has_not_expired = time_expire is None or time_expire > current_time
-            announcement_json["is_currently_visible"] = (announcement_db["visible"]
-                                                         and (time_publish is None or time_publish <= current_time)
-                                                         and has_not_expired)
-            announcement_json["has_expired"] = not has_not_expired
-        announcements_json.append(announcement_json)
-        level_db: int = announcement_db["level"]
-        if level_db == 0:
-            announcement_json["type"] = "info"
-            announcement_json["visibility"] = "only_main_page"
-        elif level_db == 1:
-            announcement_json["type"] = "info"
-            announcement_json["visibility"] = "all_pages"
-        elif level_db == 2:
-            announcement_json["type"] = "warning"
-            announcement_json["visibility"] = "all_pages"
-        elif level_db == 3:
-            announcement_json["type"] = "important"
-            announcement_json["visibility"] = "all_pages_and_embed"
-        else:
-            raise Exception("Unknown announcement level: " + str(level_db))
-    
-    return {
-        "announcements": announcements_json
-    }
+@api_route("/is_running", ["GET"])
+def api_route_is_running():
+    return {}, HTTP_200_OK
 
 
 _SQL_GET_UPCOMING_LECTURES = PreparedStatement(f"""