diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po
index 89ce478d592bcb0675a2fe010a746fe16bb68a47..fe2cb0db938a0fca183f517592fe9de4423e6bf9 100644
--- a/src/backoffice/locale/de/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/de/LC_MESSAGES/django.po
@@ -665,32 +665,26 @@ msgstr "Assembly bearbeiten"
 msgid "AssemblyTeam__message"
 msgstr "Nachricht senden"
 
-msgid "AssemblyTeam__position__status"
-msgstr "Status"
-
-msgid "AssemblyTeam__position__point"
-msgstr "POI"
-
-msgid "AssemblyTeam__position__boundary"
-msgstr "Umrandung"
-
-msgid "AssemblyTeam__position-final"
-msgstr "final"
+msgid "AssemblyTeam__lastnote"
+msgstr "letzte Notiz"
 
-msgid "AssemblyTeam__position-preview"
-msgstr "Vorschau"
+# use translation from core
+msgid "Rooms"
+msgstr "Raum"
 
-msgid "AssemblyTeam__position-draft"
-msgstr "Entwurf"
+# use translation from core
+msgid "Room__blocked__help"
+msgstr ""
 
-msgid "AssemblyTeam__position-internal"
-msgstr "nur Assembly"
+# use translation from core
+msgid "Room__blocked"
+msgstr ""
 
-msgid "AssemblyTeam__position-none"
-msgstr "nicht verf."
+msgid "Room__location_point"
+msgstr "Position (Karte)"
 
-msgid "AssemblyTeam__lastnote"
-msgstr "letzte Notiz"
+msgid "Room__edit__position"
+msgstr "Position des Raums bearbeiten"
 
 msgid "Assembly__edit__hierarchy"
 msgstr "Hierarchie/Art der Assembly ändern"
@@ -723,6 +717,13 @@ msgstr "Verwaltende der Assemblies informieren"
 msgid "AssemblyTeam__edit_position-save"
 msgstr "Position speichern im Status:"
 
+# use translation from core
+msgid "Room__location_floor"
+msgstr ""
+
+msgid "Room__location_boundaries"
+msgstr "Bereiche"
+
 msgid "AssemblyTeam__edit_position__tooltip-none"
 msgstr "Als 'keine Position' behandeln, auch wenn ggf. bereits eine gespeichert ist - z.B. ein alter Standort."
 
@@ -779,6 +780,30 @@ msgstr "Nachrichten Empfänger"
 msgid "AssemblyTeam__message__send_button"
 msgstr "Nachricht versenden"
 
+msgid "AssemblyTeam__position__status"
+msgstr "Status"
+
+msgid "AssemblyTeam__position__point"
+msgstr "POI"
+
+msgid "AssemblyTeam__position__boundary"
+msgstr "Umrandung"
+
+msgid "AssemblyTeam__position-final"
+msgstr "final"
+
+msgid "AssemblyTeam__position-preview"
+msgstr "Vorschau"
+
+msgid "AssemblyTeam__position-draft"
+msgstr "Entwurf"
+
+msgid "AssemblyTeam__position-internal"
+msgstr "nur Assembly"
+
+msgid "AssemblyTeam__position-none"
+msgstr "nicht verf."
+
 msgid "nav_active_tab_sr_marker"
 msgstr "(ausgewählt)"
 
diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po
index 82947e614461b264e496cb4e90c3f05195999048..3c10871739105cefd64e89e40e6bcb5a66ae58c0 100644
--- a/src/backoffice/locale/en/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/en/LC_MESSAGES/django.po
@@ -665,32 +665,26 @@ msgstr "edit assembly"
 msgid "AssemblyTeam__message"
 msgstr "send a message"
 
-msgid "AssemblyTeam__position__status"
-msgstr "status"
-
-msgid "AssemblyTeam__position__point"
-msgstr "POI"
-
-msgid "AssemblyTeam__position__boundary"
-msgstr "boundary"
-
-msgid "AssemblyTeam__position-final"
-msgstr "final"
+msgid "AssemblyTeam__lastnote"
+msgstr "latest note"
 
-msgid "AssemblyTeam__position-preview"
-msgstr "preview"
+# use translation from core
+msgid "Rooms"
+msgstr ""
 
-msgid "AssemblyTeam__position-draft"
-msgstr "draft"
+# use translation from core
+msgid "Room__blocked__help"
+msgstr ""
 
-msgid "AssemblyTeam__position-internal"
-msgstr "assembly"
+# use translation from core
+msgid "Room__blocked"
+msgstr ""
 
-msgid "AssemblyTeam__position-none"
-msgstr "none"
+msgid "Room__location_point"
+msgstr "POI"
 
-msgid "AssemblyTeam__lastnote"
-msgstr "latest note"
+msgid "Room__edit__position"
+msgstr "placement of the room"
 
 msgid "Assembly__edit__hierarchy"
 msgstr "change assembly's categorization"
@@ -723,6 +717,13 @@ msgstr "inform the assembly's management"
 msgid "AssemblyTeam__edit_position-save"
 msgstr "save the position with this state:"
 
+# use translation from core
+msgid "Room__location_floor"
+msgstr ""
+
+msgid "Room__location_boundaries"
+msgstr "boundaries"
+
 msgid "AssemblyTeam__edit_position__tooltip-none"
 msgstr "handle this assembly as 'no position', even if a location may be set (e.g. old one)"
 
@@ -779,6 +780,30 @@ msgstr "message recipients"
 msgid "AssemblyTeam__message__send_button"
 msgstr "Send the message"
 
+msgid "AssemblyTeam__position__status"
+msgstr "status"
+
+msgid "AssemblyTeam__position__point"
+msgstr "POI"
+
+msgid "AssemblyTeam__position__boundary"
+msgstr "boundary"
+
+msgid "AssemblyTeam__position-final"
+msgstr "final"
+
+msgid "AssemblyTeam__position-preview"
+msgstr "preview"
+
+msgid "AssemblyTeam__position-draft"
+msgstr "draft"
+
+msgid "AssemblyTeam__position-internal"
+msgstr "assembly"
+
+msgid "AssemblyTeam__position-none"
+msgstr "none"
+
 msgid "nav_active_tab_sr_marker"
 msgstr "(current)"
 
diff --git a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
index 2630e8434dfba6dc443dac9747cc0c2c8fc8176b..2fc1e5149665d2246259c5cd0d37f40900f360b5 100644
--- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
+++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
@@ -228,6 +228,32 @@
           </div>
         </div>
       {% endif %}
+
+      <div class="card">
+        <div class="card-header">{% trans "Rooms" %}</div>
+        <div class="card-body">
+          {% for room in assembly.rooms.all %}
+            <span class="fs-0 text-muted">
+              {{ room.get_room_type_display }}
+              <abbr title="{{ room.name }}">{{ room.slug }}</abbr>
+              {% if room.blocked %}
+                <abbr class="text-danger" title="{% trans "Room__blocked__help" %}">{% trans "Room__blocked" %}</abbr>
+              {% endif %}
+            </span>
+            <br />
+            <span class="text-{% if room.location_state == room.assembly.LocationState.NONE %}danger{% elif room.location_state == room.assembly.LocationState.FINAL %}success{% else %}warning{% endif %}">
+              <abbr title="{% trans "Room__location_point" %}"><i class="bi bi-pin-map-fill"></i></abbr>
+              {{ room.get_location_state_display }}
+            </span>
+            <a class="btn btn-sm btn-outline-primary"
+               href="{% url "backoffice:assemblyteam-editposition-room" pk=room.pk %}"
+               title="{% trans "Room__edit__position" %}"><i class="bi bi-pencil"></i></a>
+            {% if not forloop.last %}<hr>{% endif %}
+          {% empty %}
+            <span class="text-muted">-/-</span>
+          {% endfor %}
+        </div>
+      </div>
     </div>
   </div>
 
diff --git a/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html b/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html
new file mode 100644
index 0000000000000000000000000000000000000000..7ed3706a6013a757730668d606cfd1b23c812423
--- /dev/null
+++ b/src/backoffice/templates/backoffice/assemblyteam_editposition_room.html
@@ -0,0 +1,89 @@
+{% extends "backoffice/base.html" %}
+{% load i18n %}
+{% load static %}
+{% load widget_tweaks %}
+
+{% block content %}
+  {% if form.errors %}
+    <div class="row">
+      <div class="col-md-12">
+        <div class="alert alert-danger">{{ form.errors }}</div>
+      </div>
+    </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-12">
+      <div class="card mb-3">
+        <div class="card-header">
+          <strong>{% trans "Room__edit__position" %}</strong>:
+          <code>{{ room.slug }}</code>
+          {{ room.name }}
+        </div>
+        <div class="card-body">
+          <form action="{% url 'backoffice:assemblyteam-editposition-room' pk=room.id %}"
+                method="post">
+            {% csrf_token %}
+            <input type="hidden" name="value" value="{{ new_value }}">
+
+            {% if room.blocked %}
+              <div class="alert alert-warning">
+                <i class="bi bi-sign-do-not-enter"></i> {% trans "Room__blocked__help" %}
+              </div>
+            {% endif %}
+
+            {% include "core/map.html" with map_config=conference.map_config.backoffice poi_id="poi" areas_id="areas" floor_id="floor" assembly=room.assembly %}
+
+            <div class="d-{% if debug %}block{% else %}none{% endif %}">
+              <div class="row mb-3">
+                <label class="col-sm-2 col-form-label text-muted" for="floor">{% trans "Room__location_floor" %}</label>
+                <div class="col-sm-10">
+                  <input class="form-control"
+                         readonly
+                         type="text"
+                         name="location_floor"
+                         id="floor"
+                         value="{{ room.get_location_floor_index|default:"" }}">
+                </div>
+              </div>
+              <div class="row mb-3">
+                <label class="col-sm-2 col-form-label text-muted" for="poi">{% trans "Room__location_point" %}</label>
+                <div class="col-sm-10">
+                  <input class="form-control"
+                         readonly
+                         type="text"
+                         name="location_point"
+                         id="poi"
+                         value="{{ map_location|default:"" }}">
+                </div>
+              </div>
+              <div class="row mb-3">
+                <label class="col-sm-2 col-form-label text-muted" for="areas">{% trans "Room__location_boundaries" %}</label>
+                <div class="col-sm-10">
+                  <input class="form-control"
+                         readonly
+                         type="text"
+                         name="location_boundaries"
+                         id="areas"
+                         value="{{ map_boundaries|default:"" }}">
+                </div>
+              </div>
+            </div>
+
+            <p>{% trans "AssemblyTeam__add_comment_field" %}</p>
+            <textarea name="comment"
+                      class="form-control"
+                      placeholder="(optional)"
+                      autocomplete="off"></textarea>
+
+            <label class="col-sm-2 col-form-label text-muted" for="buttons">{% trans "AssemblyTeam__edit_position-save" %}</label>
+            <div class="btn-group" id="buttons">
+              {% include "backoffice/assemblyteam_editposition_statebuttons.html" with object=room %}
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py
index 21e729cad7740ac985d2286622ba01bb12c0b254..ea6b2f8a1d5051027259fef4fc9fa1189f4cc200 100644
--- a/src/backoffice/urls.py
+++ b/src/backoffice/urls.py
@@ -69,6 +69,7 @@ urlpatterns = [
     path('assemblyteam/<uuid:pk>/hierarchy', assemblyteam.AssemblyEditHierarchyView.as_view(), name='assemblyteam-edithierarchy'),
     path('assemblyteam/<uuid:pk>/position', assemblyteam.AssemblyEditPlacementView.as_view(), name='assemblyteam-editposition-assembly'),
     path('assemblyteam/<uuid:pk>/message', assemblyteam.AssemblyMessageView.as_view(), name='assemblyteam-message'),
+    path('assemblyteam/room/<uuid:pk>/position', assemblyteam.EditRoomPositionView.as_view(), name='assemblyteam-editposition-room'),
     path('assembly/create', assemblies.AssemblyCreateView.as_view(), name='assembly-create'),
     path('assembly/<uuid:pk>', assemblies.AssemblyDetailView.as_view(), name='assembly'),
     path('assembly/<uuid:pk>/edit', assemblies.AssemblyUpdateView.as_view(), name='assembly-edit'),
diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py
index 1d46577b0ba2c62d8a0de21318b1995d2f70550f..5695da9617ed089cc698d9ab4a0dd801adb9e879 100644
--- a/src/backoffice/views/assemblyteam.py
+++ b/src/backoffice/views/assemblyteam.py
@@ -162,7 +162,7 @@ class AssembliesListMixin(AssemblyTeamMixin, FilteredListView):
         return qs
 
 
-def build_nav_from_assembly(assembly):
+def build_nav_from_assembly(assembly, append_children: dict | None = None):
     def _build_nav_from_assembly(assmbly: Assembly, way_up: bool):
         # for cluster members, go way up to the top and trickle down
         if way_up and assmbly.parent:
@@ -171,12 +171,16 @@ def build_nav_from_assembly(assembly):
         me = {
             'caption': assmbly.slug,
             'link': reverse('backoffice:assemblyteam-detail', kwargs={'pk': assmbly.pk}),
-            'active': assembly.id == assmbly.id,
+            'active': assembly.id == assmbly.id and not append_children,
         }
 
         if assmbly.is_cluster:
             me['children'] = [_build_nav_from_assembly(a, way_up=False) for a in assmbly.children.order_by('slug').all()]
             me['expanded'] = True
+        elif append_children:
+            # we are the last child (as we're not recursing further)
+            me['children'] = append_children
+            me['expanded'] = True
 
         return [me] if way_up else me
 
@@ -691,6 +695,122 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
         return render(self.request, 'backoffice/assemblyteam_editposition_assembly.html', context)
 
 
+class EditRoomPositionView(AssemblyTeamMixin, View):
+    def dispatch(self, *args, **kwargs):
+        self.room = self.conference.rooms.get(pk=kwargs['pk'])
+        return super().dispatch(*args, **kwargs)
+
+    def get_context_data(self, *args, **kwargs):
+        ctx = super().get_context_data(*args, **kwargs)
+        ctx['room'] = self.room
+        sidebar_room = {
+            'caption': f'{self.room.get_room_type_display()} {self.room.slug}',
+            'link': reverse('backoffice:assemblyteam-editposition-room', kwargs={'pk': self.room.pk}),
+            'active': True,
+        }
+        ctx['sidebar'] = {
+            'title': _('nav_assemblies'),
+            'title_link': reverse('backoffice:assemblies'),
+            'items': build_nav_from_assembly(self.room.assembly, append_children=[sidebar_room]),
+        }
+        return ctx
+
+    def post(self, *args, **kwargs):
+        request = self.request
+        room = self.room
+
+        location_state = request.POST.get('location_state', None)
+        poi = request.POST.get('location_point', None)
+        boundaries = request.POST.get('location_boundaries', None)
+        floor = request.POST.get('location_floor', None)
+        comment = request.POST.get('comment', '').strip()
+
+        old_values = {
+            'location_state': room.location_state,
+            'location_point': str(room.get_location_point_xy() or ''),
+            'location_boundaries': str(room.get_location_boundaries_xy() or ''),
+            'location_floor': str(room.get_location_floor_index() or ''),
+        }
+        changes = {}
+        try:
+            room.location_data = room.location_data or {}
+
+            if poi and poi != '""':
+                parsed = json.loads(poi)
+                assert isinstance(parsed, list) and len(parsed) == 2
+
+                room.location_data['point'] = parsed
+                changes['location_point'] = poi
+            else:
+                room.location_data['point'] = None
+                changes['location_point'] = ''
+
+            if boundaries and boundaries != '""':
+                parsed = json.loads(boundaries)
+                assert isinstance(parsed, list)
+
+                room.location_data['boundaries'] = parsed
+                changes['location_boundaries'] = boundaries
+            else:
+                room.location_data['boundaries'] = None
+                changes['location_boundaries'] = ''
+
+            if floor and floor != '""':
+                parsed = int(floor)
+
+                room.location_floor = self.conference.map_floors.filter(index=parsed).first()
+                changes['location_floor'] = str(room.get_location_floor_index() or '')
+            else:
+                room.location_floor = None
+                changes['location_floor'] = ''
+
+        except ValueError:
+            logger.exception('Failed to update position of room %s', room.pk)
+            messages.error(request, gettext('Assembly__edit__position_error'))
+            return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk)
+
+        if location_state and location_state != room.location_state:
+            if location_state != Assembly.LocationState.NONE and not (room.location_data.get('point') or room.location_data.get('boundaries')):
+                messages.warning(request, gettext('Assembly__edit__position_missing_on_publish'))
+                return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk)
+
+            room.location_state = location_state
+            changes['location_state'] = location_state
+
+        room.save(update_fields=['location_state', 'location_floor', 'location_data'])
+
+        # log the action
+        messages.success(request, gettext('AssemblyTeam__edit_position__success'))
+        logger.info(
+            'Room "%(room_slug)s" (%(room_pk)s): set location_state to "%(location_state)s, POI to "%(poi)s" & boundaries to "%(boundaries)s" & floor to "%(floor)s" upon request by <%(user)s>.',  # noqa:E501
+            {
+                'room_slug': room.slug,
+                'room_pk': room.pk,
+                'location_state': location_state,
+                'floor': floor,
+                'poi': poi,
+                'boundaries': boundaries,
+                'user': self.request.user.username,
+            },
+        )
+        room.log_activity(
+            user=self.request.user,
+            kind=ActivityLogEntry.Kind.STAFF,
+            comment=comment if comment else None,
+            **{k: ActivityLogChange(old=old_values[k], new=v) for k, v in changes.items() if old_values[k] != v},
+        )
+
+        # we're done saving, redirect to edit view again
+        return redirect('backoffice:assemblyteam-editposition-room', pk=room.pk)
+
+    def get(self, *args, **kwargs):
+        context = self.get_context_data()
+        context['uses_map'] = True
+        context['map_location'] = self.room.get_location_point_as_json()
+        context['map_boundaries'] = self.room.get_location_boundaries_as_json()
+        return render(self.request, 'backoffice/assemblyteam_editposition_room.html', context)
+
+
 class AssemblyMessageView(SingleAssemblyTeamMixin, FormView):
     template_name = 'backoffice/assemblyteam_message.html'
     form_class = AssemblyTeamMessageForm
diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py
index a0fdf814675cb8975719fedd00b62d88bca31869..5543aaa592aaf36ed90cf211528029b306de5188 100644
--- a/src/core/models/rooms.py
+++ b/src/core/models/rooms.py
@@ -16,6 +16,7 @@ from django.utils.translation import gettext_lazy as _
 
 from core.fields import ConferenceReference
 from core.markdown import compile_translated_markdown_fields, store_relationships
+from core.models.activitylog import ActivityLogMixin
 from core.models.assemblies import Assembly
 from core.models.base_managers import ConferenceManagerMixin
 from core.models.conference import Conference, ConferenceMember
@@ -101,7 +102,7 @@ def get_banner_help_test() -> str:
     }
 
 
-class Room(BackendMixin, models.Model):
+class Room(BackendMixin, ActivityLogMixin, models.Model):
     class Meta:
         verbose_name = _('Room')
         verbose_name_plural = _('Rooms')