diff --git a/src/api/serializers.py b/src/api/serializers.py
index e0a344ae4a481f727e1d5d525ca9f6a29684a07f..bbad60a25ca8a057c2da409cb7360f97f328f9af 100644
--- a/src/api/serializers.py
+++ b/src/api/serializers.py
@@ -11,6 +11,7 @@ from core.models.assemblies import Assembly
 from core.models.badges import Badge, BadgeToken, BadgeTokenTimeConstraint
 from core.models.conference import Conference, ConferenceMember, ConferenceTrack
 from core.models.events import Event
+from core.models.links import Link
 from core.models.messages import DirectMessage
 from core.models.metanavi import MetaNavItem
 from core.models.rooms import Room
@@ -50,6 +51,56 @@ class ParameterisedHyperlinkedIdentityField(HyperlinkedIdentityField):
         return reverse(view_name, kwargs=kwargs, request=request, format=format)
 
 
+class HubHyperlinkedIdentityField(HyperlinkedIdentityField):
+    """
+    Represents the instance, or a property on the instance, using hyperlinking.
+
+    lookup_fields is a tuple of tuples of the form:
+        ('model_field', 'url_parameter')
+
+    taken from https://github.com/encode/django-rest-framework/issues/1024
+    """
+
+    lookup_fields = (('pk', 'pk'),)
+
+    def __init__(self, *args, **kwargs):
+        self.lookup_fields = kwargs.pop('lookup_fields', self.lookup_fields)
+        super().__init__(*args, **kwargs)
+
+    def get_url(self, obj, view_name, request, format):  # pylint: disable=redefined-builtin
+        """
+        Given an object, return the URL that hyperlinks to the object.
+
+        May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
+        attributes are not configured to correctly match the URL conf.
+        """
+        kwargs = {}
+        for model_field, url_param in self.lookup_fields:
+            attr = obj
+            for field in model_field.split('.'):
+                attr = getattr(attr, field)
+            kwargs[url_param] = attr
+
+        from core.templatetags.hub_absolute import hub_absolute  # pylint: disable=import-outside-toplevel
+
+        # TODO: add request=request, format=format,
+        return hub_absolute(view_name, **kwargs, i18n=False)
+
+
+class LinkRelatedField(serializers.RelatedField):
+    """
+    A read only field that represents its targets using their
+    plain string representation.
+    """
+
+    def __init__(self, **kwargs):
+        kwargs['read_only'] = True
+        super().__init__(**kwargs)
+
+    def to_representation(self, value: Link):
+        return value.to_dict()
+
+
 class ValidatingModelSerializer(serializers.ModelSerializer):
     def validate(self, data):
         instance = self.Meta.model(**{field: value for field, value in data.items() if field in self.Meta.model._meta.fields})
@@ -217,17 +268,20 @@ class BadgeTokenUpdateSerializer(BadgeTokenSerializer):
 class RoomSerializer(HubModelSerializer):
     assembly = serializers.SlugRelatedField(read_only=True, slug_field='slug')
 
-    links = serializers.StringRelatedField(
+    links = LinkRelatedField(
         many=True,
         read_only=True,
     )
 
+    public_url = HubHyperlinkedIdentityField(view_name='plainui:room', lookup_fields=(('slug', 'slug'),))
+
     class Meta:
         model = Room
         read_only_fields = ['id']
         fields = [
             'id',
             'name',
+            'slug',
             'blocked',
             'room_type',
             'capacity',
@@ -235,6 +289,7 @@ class RoomSerializer(HubModelSerializer):
             'assembly',
             'links',
             'backend_link',
+            'public_url',
         ]
         staff_only_fields = ['blocked', 'backend_link']
 
diff --git a/src/api/views/rooms.py b/src/api/views/rooms.py
index 716e8795e55ec091a9b6322e0da51deab81d6445..82c422a2d1478d65289a8e2f472bf33ad063005e 100644
--- a/src/api/views/rooms.py
+++ b/src/api/views/rooms.py
@@ -14,7 +14,7 @@ class ConferenceRoomList(ConferenceSlugMixin, generics.ListAPIView):
     serializer_class = RoomSerializer
 
     def get_queryset(self, **kwargs):
-        return Room.objects.conference_accessible(conference=self.conference).order_by('name')
+        return Room.objects.conference_accessible(conference=self.conference).order_by('official_room_order', 'name')
 
 
 class ConferenceRoomDetail(ConferenceSlugMixin, generics.RetrieveAPIView):
diff --git a/src/backoffice/templates/backoffice/schedule_source-detail.html b/src/backoffice/templates/backoffice/schedule_source-detail.html
index 2122ae2de9557aecdc34277179b406c8cf5f687d..ac55db261e5d5ae1c85e163e5d9af5eb09982be3 100644
--- a/src/backoffice/templates/backoffice/schedule_source-detail.html
+++ b/src/backoffice/templates/backoffice/schedule_source-detail.html
@@ -20,7 +20,7 @@
     </div>
     <div class="card-body">
       <p>
-        Assembly: <strong>{{ object.assembly|default:"<em>WILDCARD</em>" }}</strong>
+        Assembly: <strong><a href="{% url 'backoffice:assembly-edit' pk=object.assembly.id %}">{{ object.assembly|default:"<em>WILDCARD</em>" }}</a></strong>
       </p>
       <p>
         Type: <strong>{{ object.import_type }}</strong>
@@ -108,8 +108,10 @@
           {% for mapping in object.mappings.all %}
             <tr>
               <td>{{ mapping.get_mapping_type_display }}</td>
-              <td>{{ mapping.source_id|default:"-" }}</td>
-              <td>{{ mapping.local_id|default:"-" }}</td>
+              <td>{{ mapping.source_id|default:'-' }}</td>
+              <td>
+                <a href="{{ mapping.local_url }}">{{ mapping.local_id|default:'-' }}</a>
+              </td>
               <td>{{ mapping.skip|yesno }}</td>
               <td>&nbsp;</td>
             </tr>
diff --git a/src/backoffice/templates/backoffice/schedule_source_import-detail.html b/src/backoffice/templates/backoffice/schedule_source_import-detail.html
index 8b7e2c2597d29b28450036f892667917660171ba..ec3be03022ad3a310bf8c4ac15812b7502b0a77a 100644
--- a/src/backoffice/templates/backoffice/schedule_source_import-detail.html
+++ b/src/backoffice/templates/backoffice/schedule_source_import-detail.html
@@ -95,7 +95,7 @@
                   {{ item.source_id|default:"-/-" }}
                 </td>
                 <td class="{% if item.action == "seen" %}text-muted{% elif item.action == "error" %}text-danger{% elif item.action == "missing" or item.action == "removed" %}text-warning{% elif item.action == "added" %}text-success{% endif %}">
-                  {{ item.local_id|default:"-/-" }}
+                  <a href="{{ item.local_url }}">{{ item.local_id|default:"-/-" }}</a>
                 </td>
               </tr>
             {% endfor %}
diff --git a/src/core/admin.py b/src/core/admin.py
index 8158cdcd2d8298de242c0e11d39fd529859fa8aa..d9ee6d63023dc4ceef23e7dcd652ed3192127f44 100644
--- a/src/core/admin.py
+++ b/src/core/admin.py
@@ -750,11 +750,11 @@ class RoomShareAdmin(admin.ModelAdmin):
 
 
 class RoomAdmin(admin.ModelAdmin):
-    list_display = ['conference', 'assembly', 'name', 'room_type', 'blocked']
+    list_display = ['conference', 'assembly', 'name', 'room_type', 'blocked', 'official_room_order', 'id']
     list_display_links = ['name']
     list_filter = ['conference', 'room_type', 'backend_status', 'blocked', 'is_official', 'is_public_fahrplan']
     save_as = True
-    search_fields = ['assembly__name', 'name', 'slug']
+    search_fields = ['assembly__name', 'name', 'slug', 'id']
     inlines = [RoomLinkInline, RoomShareInline, TagsInline]
     readonly_fields = ['id', 'occupants', 'reserve_capacity']
     ordering = ('-conference__id', F('assembly__is_official').desc(nulls_last=True), 'assembly__name', F('capacity').desc(nulls_last=True), 'name')
diff --git a/src/core/models/links.py b/src/core/models/links.py
index 86764e7e409e67adef65819949d2b20d5600766f..65ded766dd94077ea2e2c23e69fe18d70a3f5a38 100644
--- a/src/core/models/links.py
+++ b/src/core/models/links.py
@@ -5,7 +5,7 @@ from django.core.validators import URLValidator
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 
-from core.utils import resolve_link
+from core.utils import resolve_internal_url, resolve_link
 
 
 class Link(models.Model):
@@ -62,6 +62,14 @@ class Link(models.Model):
             except ValidationError:
                 raise ValidationError({'link': _('Link__link__must_be_url')})
 
+    def to_dict(self):
+        return {
+            'type': self.link_type,
+            'name': self.name,
+            'uri': self.link,
+            'url': resolve_internal_url(self.link),
+        }
+
     def __str__(self) -> str:
         return self.name
 
diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py
index b54819c259eb740fe27d5810a9ede9ef16a73cac..cef75f46a77b2b89f46a675ac1a6e2c22217d303 100644
--- a/src/core/models/rooms.py
+++ b/src/core/models/rooms.py
@@ -273,6 +273,11 @@ class Room(BackendMixin, models.Model):
             return link.link
         return None
 
+    def get_absolute_url(self):
+        from core.templatetags.hub_absolute import hub_absolute  # pylint: disable=import-outside-toplevel
+
+        return hub_absolute('plainui:room', slug=self.slug, i18n=settings.ARCHIVE_MODE)
+
     def __create_slug(self, extension='', max_length: int = 50):
         """
         recursive function to generate a free room slug based on the room name
@@ -501,5 +506,13 @@ class RoomLink(models.Model):
             if not resolve_internal_url(self.link, fallback_as_is=False):
                 raise ValidationError({'link': _('RoomLink__link__must_be_url')})
 
+    def to_dict(self):
+        return {
+            'type': self.link_type,
+            'name': self.name,
+            'uri': self.link,
+            'url': resolve_internal_url(self.link),
+        }
+
     def __str__(self):
         return self.name
diff --git a/src/core/models/schedules.py b/src/core/models/schedules.py
index aed616a497c2659faa6e4b1bf292f6dd723c06bc..6ef85f09bb2e1221cc50dba818a075998acc892d 100644
--- a/src/core/models/schedules.py
+++ b/src/core/models/schedules.py
@@ -348,6 +348,9 @@ class ScheduleSource(models.Model):
                 )
                 logging.exception('Import on ScheduleSource %s encountered exception on creating mapping for %s "%s".', self.pk, item_type, item_source_id)
 
+                # ... and delete the incomplete (wrong) mapping if it was created
+                if new_mapping:
+                    mapping.delete()
                 return 'error'
 
             else:
@@ -427,7 +430,7 @@ class ScheduleSource(models.Model):
         allow_track = cfg.get('import_tracks') or False
 
         # note down all existing rooms, events and speakers so that we can call out the missing ones
-        if self.assembly:
+        if self.assembly and cfg.get('missing_rooms') != 'ignore':
             expected_rooms = list(self.assembly.rooms.values_list('id', flat=True))
         else:
             expected_rooms = list(
@@ -690,6 +693,18 @@ class ScheduleSourceMapping(models.Model):
         # we don't know about that mapping type, bail out
         raise LocalObjectAccessViolation('Unknown mapping.')
 
+    @property
+    def local_url(self):
+        if self.mapping_type == self.MappingType.ROOM:
+            return Room.admin_url(self.local_id)
+
+        if self.mapping_type == self.MappingType.EVENT:
+            return Event.admin_url(self.local_id)
+        if self.mapping_type == self.MappingType.SPEAKER:
+            return ''
+
+        return ''
+
     @property
     def local_object(self):
         if self._local_object is None:
@@ -824,18 +839,14 @@ class ScheduleSourceImport(models.Model):
                 # create list of unique errors for summary
                 msgs = list({x['message'].split('\n')[0] for x in errors})
 
-            stats = (
-                ', '.join(
-                    (t + '=' + str(sum(1 for x in activity if x['action'] == t)))
-                    for t in ['added', 'changed', 'seen', 'deleted', 'missing', 'error', 'skipped']
-                )
-                + ' \n'
-                + ' \n'.join(msgs)
-            )
-            self.summary = f"{data.get('version') or ''}\nDONE: {stats}"[:200]
+            stats = {t: sum(1 for x in activity if x['action'] == t) for t in ['added', 'changed', 'seen', 'deleted', 'missing', 'error', 'skipped']}
+            stats_str = ', '.join([f'{v}={k}' for (k, v) in stats.items() if v]) + ' \n' + ' \n'.join(msgs)
+            self.summary = f"{data.get('version') or ''}\nDONE: {stats_str}"[:200]
 
-            if len(errors) > len(activity) / 2:
-                raise Exception('Too many errors, aborting import: ' + stats)
+            # add debug option to disable abort feature, as it might hide the actual error messages
+            if len(errors) > len(activity) / 2 and self.schedule_source.import_configuration.get('debug') is not True:
+                self.save(update_fields=['summary'])
+                raise Exception('Too many errors, aborting import: ' + stats_str, errors)
 
             self.save(update_fields=['data', 'summary'])
 
diff --git a/src/core/models/shared.py b/src/core/models/shared.py
index 77d69925df55fca9553719454a82412b1a4070de..967c37ee60402d618d187cd832c8a2f91aefce67 100644
--- a/src/core/models/shared.py
+++ b/src/core/models/shared.py
@@ -3,6 +3,12 @@ from django.utils.translation import gettext_lazy as _
 
 
 class BackendMixin(models.Model):
+    @classmethod
+    def admin_url(cls, pk):
+        from django.urls import reverse
+
+        return reverse(f'admin:{cls._meta.app_label}_{cls._meta.model_name}_change', args=[pk])
+
     class Meta:
         abstract = True