diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 65b8f70a13a3fad79b52003389809f3a478d4674..b8edc13c13aa3b760eecf77254c689b85cc3fcec 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -14,6 +14,8 @@ Added
 
 * GraphQL schema for Rooms
 * [Dev] UpdateIndicator Vue Component to display the status of interactive pages
+* [Dev] DeleteDialog Vue Component to unify item deletion in the new frontend
+
 
 Changed
 ~~~~~~~
@@ -28,7 +30,14 @@ Fixed
 * 404 page was sometimes shown while the page was still loading.
 * Setting of page height in the iframe was not working correctly.
 * App switched to offline state when the user was logged out/in.
-* Phone numbers couldn't be regional.
+* The `Stop Impersonation` button is not shown due to an oversee when changing the type of the whoAmI query to an object of UserType
+* Offline fallback page for legacy pages was misleading sometimes.
+* Route changes in the Legacy-Component iframe didn't trigger a scroll to the top
+* Query strings did not get passed when navigating legacy pages inside of the SPA.
+* Retry button on error 500 page did not trigger a reload of the page.
+* When the Celery worker wasn't able to execute all tasks in time, notifications were sent multiple times.
+* Changing the maintenance mode state spawned another SPA instance in the iframe
+* Phone numbers couldn't be in regional format.
 
 `3.0b3`_ - 2023-03-19
 ---------------------
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 92c120886e4a530cc566b8489cb09d3e38f5ba2e..efd6e34edae2dd9f6e01e02cc9c8c9a669ce7cd0 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -19,6 +19,7 @@ from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget,
 from dynamic_preferences.forms import PreferenceForm
 from guardian.shortcuts import assign_perm
 from invitations.forms import InviteForm
+from maintenance_mode.core import get_maintenance_mode
 from material import Fieldset, Layout, Row
 
 from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
@@ -860,3 +861,11 @@ class OAuthApplicationForm(forms.ModelForm):
             "redirect_uris",
             "skip_authorization",
         )
+
+
+class MaintenanceModeForm(forms.Form):
+    maintenance_mode = forms.BooleanField(
+        required=False,
+        initial=not get_maintenance_mode(),
+        widget=forms.HiddenInput,
+    )
diff --git a/aleksis/core/frontend/components/LegacyBaseTemplate.vue b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
index 30ef189b7daa037bc47fe711515e112b8391b377..790fc10c592bac5acc1e3bbadabcae63e065804f 100644
--- a/aleksis/core/frontend/components/LegacyBaseTemplate.vue
+++ b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
@@ -59,13 +59,14 @@ export default {
       const location = this.$refs.contentIFrame.contentWindow.location;
       const url = new URL(location);
       const path = url.pathname.replace(/^\/django/, "");
+      const pathWithQueryString = path + encodeURI(url.search);
       const routePath =
         path.charAt(path.length - 1) === "/" &&
         this.$route.path.charAt(path.length - 1) !== "/"
           ? this.$route.path + "/"
           : this.$route.path;
       if (path !== routePath) {
-        this.$router.push(path);
+        this.$router.push(pathWithQueryString);
       }
 
       // Show loader if iframe starts to change its content, even if the $route stays the same
@@ -103,6 +104,10 @@ export default {
     $route() {
       // Show loading animation once route changes
       this.$root.contentLoading = true;
+
+      // Scroll to top only when route changes to not affect form submits etc.
+      // A small duration to avoid flashing of the UI
+      this.$vuetify.goTo(0, { duration: 10 });
     },
   },
   name: "LegacyBaseTemplate",
diff --git a/aleksis/core/frontend/components/app/whoAmI.graphql b/aleksis/core/frontend/components/app/whoAmI.graphql
index 12235e0ed7c320f46f0442c23e4a75dbba44c6cd..0b2877bd2cf15b5134c8e70978fb6b2218414e3a 100644
--- a/aleksis/core/frontend/components/app/whoAmI.graphql
+++ b/aleksis/core/frontend/components/app/whoAmI.graphql
@@ -3,6 +3,7 @@ query ($permissions: [String]!) {
     username
     isAuthenticated
     isAnonymous
+    isImpersonate
     person {
       photo {
         url
@@ -10,7 +11,6 @@ query ($permissions: [String]!) {
       fullName
       avatarUrl
       isDummy
-      isImpersonate
     }
     permissions: globalPermissionsByName(permissions: $permissions) {
       name
diff --git a/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a56e3960cd1ae29e60f966a44ae69adeb0332949
--- /dev/null
+++ b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue
@@ -0,0 +1,136 @@
+<template>
+  <ApolloMutation
+    v-if="dialogOpen"
+    :mutation="gqlMutation"
+    :variables="{ id: item.id }"
+    :update="update"
+    @done="close(true)"
+  >
+    <template #default="{ mutate, loading, error }">
+      <v-dialog v-model="dialogOpen" max-width="500px">
+        <v-card>
+          <v-card-title class="text-h5">
+            <slot name="title">
+              {{ $t("actions.confirm_deletion") }}
+            </slot>
+          </v-card-title>
+          <v-card-text>
+            <slot name="body">
+              <p class="text-body-1">{{ nameOfObject }}</p>
+            </slot>
+          </v-card-text>
+          <v-card-actions>
+            <v-spacer></v-spacer>
+            <v-btn text @click="close(false)" :disabled="loading">
+              <slot name="cancelContent">
+                {{ $t("actions.cancel") }}
+              </slot>
+            </v-btn>
+            <v-btn
+              color="error"
+              text
+              @click="mutate"
+              :loading="loading"
+              :disabled="loading"
+            >
+              <slot name="deleteContent">
+                {{ $t("actions.delete") }}
+              </slot>
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-dialog>
+      <v-snackbar :value="error !== null">
+        {{ error }}
+
+        <template #action="{ attrs }">
+          <v-btn color="primary" text v-bind="attrs" @click="error = null" icon>
+            <v-icon>$close</v-icon>
+          </v-btn>
+        </template>
+      </v-snackbar>
+    </template>
+  </ApolloMutation>
+</template>
+
+<script>
+export default {
+  name: "DeleteDialog",
+  computed: {
+    nameOfObject() {
+      return this.itemAttribute in this.item || {}
+        ? this.item[this.itemAttribute]
+        : this.item.toString();
+    },
+    dialogOpen: {
+      get() {
+        return this.value;
+      },
+
+      set(val) {
+        this.$emit("input", val);
+      },
+    },
+  },
+  methods: {
+    update(store) {
+      if (!this.gqlQuery) {
+        // There is no GraphQL query to update
+        return;
+      }
+
+      // Read the data from cache for query
+      const storedData = store.readQuery({ query: this.gqlQuery });
+
+      if (!storedData) {
+        // There are no data in the cache yet
+        return;
+      }
+
+      const storedDataKey = Object.keys(storedData)[0];
+
+      // Remove item from stored data
+      const index = storedData[storedDataKey].findIndex(
+        (m) => m.id === this.item.id
+      );
+      storedData[storedDataKey].splice(index, 1);
+
+      // Write data back to the cache
+      store.writeQuery({ query: this.gqlQuery, data: storedData });
+    },
+    close(success) {
+      this.$emit("input", false);
+      if (success) {
+        this.$emit("success");
+      } else {
+        this.$emit("cancel");
+      }
+    },
+  },
+  props: {
+    value: {
+      type: Boolean,
+      required: true,
+    },
+    item: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+    itemAttribute: {
+      type: String,
+      required: false,
+      default: "name",
+    },
+    gqlMutation: {
+      type: Object,
+      required: true,
+    },
+    gqlQuery: {
+      type: Object,
+      required: false,
+      default: null,
+    },
+  },
+};
+</script>
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index ff9693d369fea8f9ce44e07d5cc1dd4b89aae1aa..5235e23265346a5842cccf793e67987459a5b40f 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -75,7 +75,12 @@
     "back": "Back",
     "search": "Search",
     "edit": "Edit",
-    "close": "Close"
+    "close": "Close",
+    "cancel": "Cancel",
+    "confirm_deletion": "Are you sure you want to delete this item?",
+    "delete": "Delete",
+    "stop_editing": "Stop editing",
+    "save": "Save"
   },
   "administration": {
     "backend_admin": {
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 1a370c2c68af1569f92a9cc09ada7f8b5f671649..964306358262c94687f53400b4adc134bf2ef118 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -769,7 +769,7 @@ class Notification(ExtensibleModel, TimeStampedModel):
     def send(self, resend: bool = False) -> Optional[AsyncResult]:
         """Send the notification to the recipient."""
         if not self.sent or resend:
-            return send_notification.delay(self.pk, resend=True)
+            return send_notification.delay(self.pk, resend=resend)
 
     class Meta:
         verbose_name = _("Notification")
diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py
index e927dadccaaab7b2bf39c2110286970222f70f5b..e3478a34ff62be4859d8d9e88a8d73322796ad82 100644
--- a/aleksis/core/schema/base.py
+++ b/aleksis/core/schema/base.py
@@ -1,3 +1,6 @@
+from django.db.models import Model
+from django.core.exceptions import PermissionDenied
+
 import graphene
 from graphene_django import DjangoObjectType
 
@@ -24,3 +27,23 @@ class FieldFileType(graphene.ObjectType):
 
     def resolve_absolute_url(root, info, **kwargs):
         return info.context.build_absolute_uri(root.url) if root else ""
+
+
+class DeleteMutation(graphene.Mutation):
+    """Mutation to delete an object."""
+
+    klass: Model = None
+    permission_required: str = ""
+    ok = graphene.Boolean()
+
+    class Arguments:
+        id = graphene.ID()  # noqa
+
+    @classmethod
+    def mutate(cls, root, info, **kwargs):
+        obj = cls.klass.objects.get(pk=kwargs["id"])
+        if info.context.user.has_perm(cls.permission_required, obj):
+            obj.delete()
+            return cls(ok=True)
+        else:
+            raise PermissionDenied()
diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py
index d5bdb6096b033e4be9a5f3c24565cb695d0e6890..6eed201487348034b2d9b1cba616046b1794dbf2 100644
--- a/aleksis/core/schema/person.py
+++ b/aleksis/core/schema/person.py
@@ -10,7 +10,7 @@ from guardian.shortcuts import get_objects_for_user
 
 from ..forms import PersonForm
 from ..models import DummyPerson, Person
-from ..util.core_helpers import get_site_preferences, has_person, is_impersonate
+from ..util.core_helpers import get_site_preferences, has_person
 from .base import FieldFileType
 from .notification import NotificationType
 
@@ -65,7 +65,6 @@ class PersonType(DjangoObjectType):
     unread_notifications_count = graphene.Int()
 
     is_dummy = graphene.Boolean()
-    is_impersonate = graphene.Boolean()
     preferences = graphene.Field(PersonPreferencesType)
 
     can_edit_person = graphene.Boolean()
@@ -199,9 +198,6 @@ class PersonType(DjangoObjectType):
     def resolve_is_dummy(root: Union[Person, DummyPerson], info, **kwargs):
         return root.is_dummy if hasattr(root, "is_dummy") else False
 
-    def resolve_is_impersonate(root: Person, info, **kwargs):
-        return is_impersonate(info.context)
-
     def resolve_notifications(root: Person, info, **kwargs):
         if has_person(info.context.user) and info.context.user.person == root:
             return root.notifications.filter(send_at__lte=timezone.now()).order_by(
diff --git a/aleksis/core/schema/user.py b/aleksis/core/schema/user.py
index 1c1ff08ea2c4c77a6ab68af9e389a36db66a764c..7104a64b6acfa4f0891a9371304df9609569723d 100644
--- a/aleksis/core/schema/user.py
+++ b/aleksis/core/schema/user.py
@@ -12,6 +12,7 @@ class UserType(graphene.ObjectType):
 
     is_authenticated = graphene.Boolean(required=True)
     is_anonymous = graphene.Boolean(required=True)
+    is_impersonate = graphene.Boolean()
 
     person = graphene.Field(PersonType)
 
diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html
index d008cd5405f4e5eee73ddf175cab87057e563785..a2ea5c902dba89f34241403e85d0e8af8d8f9a5d 100644
--- a/aleksis/core/templates/500.html
+++ b/aleksis/core/templates/500.html
@@ -16,7 +16,7 @@
           {% endblocktrans %}
         </p>
         {% include "core/partials/admins_list.html" %}
-        <a href="javascript:window.location.reload()" class="btn green waves-effect waves-light">
+        <a onClick="window.location.reload();" class="btn secondary waves-effect waves-light">
           <i class="material-icons left">refresh</i>
           {% trans "Retry" %}
         </a>
diff --git a/aleksis/core/templates/core/pages/system_status.html b/aleksis/core/templates/core/pages/system_status.html
index 7f0e7bff0840029b500620662cd4681ac6bddded..00c0010d9f150fbd6e0b6bd3cf7f60684c08b350 100644
--- a/aleksis/core/templates/core/pages/system_status.html
+++ b/aleksis/core/templates/core/pages/system_status.html
@@ -13,30 +13,34 @@
 
       {# Maintenance mode #}
       <div class="row">
-        {% if maintenance_mode %}
-          <a class="btn-flat btn-flat-medium right waves-effect waves-red no-padding"
-             href="{% url 'maintenance_mode_off' %}">
-            <i class="material-icons iconify small red-text center" data-icon="mdi:power"></i>
-          </a>
-          <div>
-            <p class="flow-text">{% blocktrans %}Maintenance mode enabled{% endblocktrans %}</p>
-            <p class="grey-text">
-              {% blocktrans %}
-                Only admin and visitors from internal IPs can access the site.
-              {% endblocktrans %}
-            </p>
-          </div>
-          <span class="badge badge-danger mdi mdi-power"><a href="{% url 'maintenance_mode_off' %}"></a></span>
-        {% else %}
-          <a class="btn-flat btn-flat-medium right waves-effect waves-green no-padding"
-             href="{% url 'maintenance_mode_on' %}">
-            <i class="material-icons iconify small green-text center" data-icon="mdi:power"></i>
-          </a>
-          <div>
-            <p class="flow-text">{% blocktrans %}Maintenance mode disabled{% endblocktrans %}</p>
-            <p class="grey-text">{% blocktrans %}Everyone can access the site.{% endblocktrans %}</p>
-          </div>
-        {% endif %}
+        <form method="POST">
+          {% csrf_token %}
+          {{ form }}
+
+          {% if maintenance_mode %}
+            <button class="btn-flat btn-flat-medium right waves-effect waves-red no-padding"
+               type="submit">
+              <i class="material-icons iconify small red-text center" data-icon="mdi:power"></i>
+            </button>
+            <div>
+              <p class="flow-text">{% blocktrans %}Maintenance mode enabled{% endblocktrans %}</p>
+              <p class="grey-text">
+                {% blocktrans %}
+                  Only admin and visitors from internal IPs can access the site.
+                {% endblocktrans %}
+              </p>
+            </div>
+          {% else %}
+            <button class="btn-flat btn-flat-medium right waves-effect waves-green no-padding"
+               type="submit">
+              <i class="material-icons iconify small green-text center" data-icon="mdi:power"></i>
+            </button>
+            <div>
+              <p class="flow-text">{% blocktrans %}Maintenance mode disabled{% endblocktrans %}</p>
+              <p class="grey-text">{% blocktrans %}Everyone can access the site.{% endblocktrans %}</p>
+            </div>
+          {% endif %}
+        </form>
       </div>
 
       {# Debug mode #}
diff --git a/aleksis/core/templates/offline.html b/aleksis/core/templates/offline.html
index 77537353bc1615426bab6e836519954c4a803da6..8bb50d53c202d2688e8c0da947b080cad1c52f28 100644
--- a/aleksis/core/templates/offline.html
+++ b/aleksis/core/templates/offline.html
@@ -7,15 +7,18 @@
 {% block content %}
   <h3>
     <i class="material-icons iconify left medium" style="font-size: 2.92rem;" data-icon="mdi:wifi-strength-alert-outline"></i>
-    {% blocktrans %}Page not available offline.{% endblocktrans %}
+    {% blocktrans %}No connection to server.{% endblocktrans %}
   </h3>
 
   <p class="flow-text">
     {% blocktrans %}
-      This page is not available offline. Since you probably don't have an internet connection, check to see if your WiFi
-      or mobile data is turned on and try again. If you think you are connected, please contact the system
-      administrators:
+      This page is not available without a connection to the server. Please check your internet connection and try again.
+      If you are connected and the error persists, please contact the system administrators:
     {% endblocktrans %}
   </p>
   {% include "core/partials/admins_list.html" %}
+  <a onClick="window.location.reload();" class="btn secondary waves-effect waves-light">
+    <i class="material-icons left">refresh</i>
+    {% trans "Retry" %}
+  </a>
 {% endblock %}
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index e9536003df379d5f97d1cf895d27c2a7b1623600..3fd874f4f920ca871083c34a11e921e6181742b3 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -16,6 +16,7 @@ from django.http import (
     Http404,
     HttpRequest,
     HttpResponse,
+    HttpResponseBadRequest,
     HttpResponseRedirect,
     HttpResponseServerError,
     JsonResponse,
@@ -52,6 +53,7 @@ from haystack.query import SearchQuerySet
 from haystack.utils.loading import UnifiedIndex
 from health_check.views import MainView
 from invitations.views import SendInvite
+from maintenance_mode.core import set_maintenance_mode
 from oauth2_provider.exceptions import OAuthToolkitError
 from oauth2_provider.models import get_application_model
 from oauth2_provider.views import AuthorizationView
@@ -85,6 +87,7 @@ from .forms import (
     EditGroupTypeForm,
     GroupPreferenceForm,
     InvitationCodeForm,
+    MaintenanceModeForm,
     OAuthApplicationForm,
     PersonForm,
     PersonPreferenceForm,
@@ -505,9 +508,29 @@ class SystemStatus(PermissionRequiredMixin, MainView):
             "status_code": status_code,
             "tasks": task_results,
             "DEBUG": settings.DEBUG,
+            "form": MaintenanceModeForm(),
         }
         return self.render_to_response(context, status=status_code)
 
+    def post(self, request, *args, **kwargs):
+        form = MaintenanceModeForm(request.POST)
+
+        if form.is_valid():
+            mode = form.cleaned_data.get("maintenance_mode")
+        else:
+            return HttpResponseBadRequest()
+
+        if not request.user.is_superuser:
+            return self.handle_no_permission()
+
+        set_maintenance_mode(mode)
+        if mode:
+            messages.success(request, _("Maintenance mode was turned on successfully."))
+        else:
+            messages.success(request, _("Maintenance mode was turned off successfully."))
+
+        return self.get(request, *args, **kwargs)
+
 
 class TestPDFGenerationView(PermissionRequiredMixin, RenderPDFView):
     template_name = "core/pages/test_pdf.html"
diff --git a/aleksis/core/vite.config.js b/aleksis/core/vite.config.js
index 604dbdae5410acc61fb5699b9a797c5906ee69de..287534f096c186bc428de742c4e5ff56b71e8eab 100644
--- a/aleksis/core/vite.config.js
+++ b/aleksis/core/vite.config.js
@@ -239,7 +239,7 @@ export default defineConfig({
               plugins: [
                 {
                   fetchDidSucceed: async ({ request, response }) => {
-                    if (response.status < 400) {
+                    if (response.status < 500) {
                       return response;
                     }
                     throw new Error(
diff --git a/pyproject.toml b/pyproject.toml
index 9a3318eaef990a7445fd14bcbabad0885b1ac6b2..e4de1fd18c27222094738b4ff926335dc9f9a3c0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -100,7 +100,7 @@ django-favicon-plus-reloaded = "^1.1.5"
 django-health-check = "^3.12.1"
 psutil = "^5.7.0"
 celery-progress = "^0.1.0"
-django-cachalot = "^2.3.2"
+django-cachalot = "^2.5.3"
 django-prometheus = "^2.1.0"
 django-model-utils = "^4.0.0"
 bs4 = "^0.0.1"