diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 6d1f4a9f56face9f67640eea16c4c24a1ff0e230..86950d726c2bc1a305d9d8462c89557154a22de6 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -725,3 +725,7 @@ TAGS_MAX_BY_OBJ = env.int("TAGS_MAX_BY_OBJ", default=30)
 FEDERATION_OBJECT_FETCH_DELAY = env.int(
     "FEDERATION_OBJECT_FETCH_DELAY", default=60 * 24 * 3
 )
+
+MODERATION_EMAIL_NOTIFICATIONS_ENABLED = env.bool(
+    "MODERATION_EMAIL_NOTIFICATIONS_ENABLED", default=True
+)
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index fa5050e34b3625e16966faf509e9d173aae3b8c2..304b94fad7eae46b3c3c92ae74d9f53d9819c2d8 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -204,6 +204,10 @@ class Actor(models.Model):
 
     class Meta:
         unique_together = ["domain", "preferred_username"]
+        verbose_name = "Account"
+
+    def get_moderation_url(self):
+        return "/manage/moderation/accounts/{}".format(self.full_username)
 
     @property
     def webfinger_subject(self):
diff --git a/api/funkwhale_api/moderation/serializers.py b/api/funkwhale_api/moderation/serializers.py
index fb87f5b9d4f647226359d1090f8d1635837bedab..81e5846bb4a9c5c3e887a1a6a0cb9a92bd36b321 100644
--- a/api/funkwhale_api/moderation/serializers.py
+++ b/api/funkwhale_api/moderation/serializers.py
@@ -14,6 +14,7 @@ from funkwhale_api.music import models as music_models
 from funkwhale_api.playlists import models as playlists_models
 
 from . import models
+from . import tasks
 
 
 class FilteredArtistSerializer(serializers.ModelSerializer):
@@ -257,4 +258,6 @@ class ReportSerializer(serializers.ModelSerializer):
             == settings.FEDERATION_HOSTNAME
         )
         validated_data["target_owner"] = get_target_owner(validated_data["target"])
-        return super().create(validated_data)
+        r = super().create(validated_data)
+        tasks.signals.report_created.send(sender=None, report=r)
+        return r
diff --git a/api/funkwhale_api/moderation/signals.py b/api/funkwhale_api/moderation/signals.py
new file mode 100644
index 0000000000000000000000000000000000000000..16be236e0c6f07d691d2072a5d4c7c2f6b8e92ac
--- /dev/null
+++ b/api/funkwhale_api/moderation/signals.py
@@ -0,0 +1,3 @@
+import django.dispatch
+
+report_created = django.dispatch.Signal(providing_args=["report"])
diff --git a/api/funkwhale_api/moderation/tasks.py b/api/funkwhale_api/moderation/tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..2942e3d03b538dc33676cf83e7c43afd32a29334
--- /dev/null
+++ b/api/funkwhale_api/moderation/tasks.py
@@ -0,0 +1,118 @@
+import logging
+from django.core import mail
+from django.dispatch import receiver
+from django.conf import settings
+
+from funkwhale_api.common import channels
+from funkwhale_api.common import utils
+from funkwhale_api.taskapp import celery
+from funkwhale_api.federation import utils as federation_utils
+from funkwhale_api.users import models as users_models
+
+from . import models
+from . import signals
+
+logger = logging.getLogger(__name__)
+
+
+@receiver(signals.report_created)
+def broadcast_report_created(report, **kwargs):
+    from . import serializers
+
+    channels.group_send(
+        "admin.moderation",
+        {
+            "type": "event.send",
+            "text": "",
+            "data": {
+                "type": "report.created",
+                "report": serializers.ReportSerializer(report).data,
+                "unresolved_count": models.Report.objects.filter(
+                    is_handled=False
+                ).count(),
+            },
+        },
+    )
+
+
+@receiver(signals.report_created)
+def trigger_moderator_email(report, **kwargs):
+    if settings.MODERATION_EMAIL_NOTIFICATIONS_ENABLED:
+        utils.on_commit(send_new_report_email_to_moderators.delay, report_id=report.pk)
+
+
+@celery.app.task(name="moderation.send_new_report_email_to_moderators")
+@celery.require_instance(
+    models.Report.objects.select_related("submitter").filter(is_handled=False), "report"
+)
+def send_new_report_email_to_moderators(report):
+    moderators = users_models.User.objects.filter(
+        is_active=True, permission_moderation=True
+    )
+    if not moderators:
+        # we fallback on superusers
+        moderators = users_models.User.objects.filter(is_superuser=True)
+    moderators = sorted(moderators, key=lambda m: m.pk)
+    subject = "[{} moderation - {}] New report from {}".format(
+        settings.FUNKWHALE_HOSTNAME,
+        report.get_type_display(),
+        report.submitter.full_username if report.submitter else report.submitter_email,
+    )
+    detail_url = federation_utils.full_url(
+        "/manage/moderation/reports/{}".format(report.uuid)
+    )
+    unresolved_reports_url = federation_utils.full_url(
+        "/manage/moderation/reports?q=resolved:no"
+    )
+    unresolved_reports = models.Report.objects.filter(is_handled=False).count()
+    body = [
+        '{} just submitted a report in the "{}" category.'.format(
+            report.submitter.full_username
+            if report.submitter
+            else report.submitter_email,
+            report.get_type_display(),
+        ),
+        "",
+        "Reported object: {} - {}".format(
+            report.target._meta.verbose_name.title(), str(report.target)
+        ),
+    ]
+    if hasattr(report.target, "get_absolute_url"):
+        body.append(
+            "Open public page: {}".format(
+                federation_utils.full_url(report.target.get_absolute_url())
+            )
+        )
+    if hasattr(report.target, "get_moderation_url"):
+        body.append(
+            "Open moderation page: {}".format(
+                federation_utils.full_url(report.target.get_moderation_url())
+            )
+        )
+    if report.summary:
+        body += ["", "Report content:", "", report.summary]
+
+    body += [
+        "",
+        "- To handle this report, please visit {}".format(detail_url),
+        "- To view all unresolved reports (currently {}), please visit {}".format(
+            unresolved_reports, unresolved_reports_url
+        ),
+        "",
+        "—",
+        "",
+        "You are receiving this email because you are a moderator for {}.".format(
+            settings.FUNKWHALE_HOSTNAME
+        ),
+    ]
+
+    for moderator in moderators:
+        if not moderator.email:
+            logger.warning("Moderator %s has no email configured", moderator.username)
+            continue
+        mail.send_mail(
+            subject,
+            message="\n".join(body),
+            recipient_list=[moderator.email],
+            from_email=settings.DEFAULT_FROM_EMAIL,
+        )
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index fc4118a98d964e73b5da5b86dd57e337d9d24560..02c4f4434b29ca509f83b2a58723e156b486b02d 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -217,6 +217,12 @@ class Artist(APIModelMixin):
     def __str__(self):
         return self.name
 
+    def get_absolute_url(self):
+        return "/library/artists/{}".format(self.pk)
+
+    def get_moderation_url(self):
+        return "/manage/library/artists/{}".format(self.pk)
+
     @classmethod
     def get_or_create_from_name(cls, name, **kwargs):
         kwargs.update({"name": name})
@@ -356,6 +362,12 @@ class Album(APIModelMixin):
     def __str__(self):
         return self.title
 
+    def get_absolute_url(self):
+        return "/library/albums/{}".format(self.pk)
+
+    def get_moderation_url(self):
+        return "/manage/library/albums/{}".format(self.pk)
+
     @property
     def cover_path(self):
         if not self.cover:
@@ -488,6 +500,12 @@ class Track(APIModelMixin):
     def __str__(self):
         return self.title
 
+    def get_absolute_url(self):
+        return "/library/tracks/{}".format(self.pk)
+
+    def get_moderation_url(self):
+        return "/manage/library/tracks/{}".format(self.pk)
+
     def save(self, **kwargs):
         try:
             self.artist
@@ -1051,6 +1069,12 @@ class Library(federation_models.FederationMixin):
     uploads_count = models.PositiveIntegerField(default=0)
     objects = LibraryQuerySet.as_manager()
 
+    def __str__(self):
+        return self.name
+
+    def get_moderation_url(self):
+        return "/manage/library/libraries/{}".format(self.uuid)
+
     def get_federation_id(self):
         return federation_utils.full_url(
             reverse("federation:music:libraries-detail", kwargs={"uuid": self.uuid})
diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py
index 15332c75a2d8ec01d0d8bac3bafa41361bd83c24..37d498c4f0a452ec7aa0b092ddf95c010935a0be 100644
--- a/api/funkwhale_api/playlists/models.py
+++ b/api/funkwhale_api/playlists/models.py
@@ -69,6 +69,9 @@ class Playlist(models.Model):
     def __str__(self):
         return self.name
 
+    def get_absolute_url(self):
+        return "/library/playlists/{}".format(self.pk)
+
     @transaction.atomic
     def insert(self, plt, index=None, allow_duplicates=True):
         """
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index e0958bc823878fe6cec68ebe23b568de6ee2b31d..dd181f7bfe55b82ee08d7e54aa32a8d1a11573ec 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -232,8 +232,13 @@ class User(AbstractUser):
 
     def get_channels_groups(self):
         groups = ["imports", "inbox"]
+        groups = ["user.{}.{}".format(self.pk, g) for g in groups]
 
-        return ["user.{}.{}".format(self.pk, g) for g in groups]
+        for permission, value in self.all_permissions.items():
+            if value:
+                groups.append("admin.{}".format(permission))
+
+        return groups
 
     def full_username(self):
         return "{}@{}".format(self.username, settings.FEDERATION_HOSTNAME)
diff --git a/api/tests/common/test_models.py b/api/tests/common/test_models.py
index 25c9befda809de508864b39f7bdc551f6600b14b..a2ea89ef2abe416c563493bdb8d142f20e20cc8e 100644
--- a/api/tests/common/test_models.py
+++ b/api/tests/common/test_models.py
@@ -15,3 +15,34 @@ def test_mutation_fid_is_populated(factories, model, factory_args, namespace):
     assert instance.fid == federation_utils.full_url(
         reverse(namespace, kwargs={"uuid": instance.uuid})
     )
+
+
+@pytest.mark.parametrize(
+    "factory_name, expected",
+    [
+        ("music.Artist", "/library/artists/{obj.pk}"),
+        ("music.Album", "/library/albums/{obj.pk}"),
+        ("music.Track", "/library/tracks/{obj.pk}"),
+        ("playlists.Playlist", "/library/playlists/{obj.pk}"),
+    ],
+)
+def test_get_absolute_url(factory_name, factories, expected):
+    obj = factories[factory_name]()
+
+    assert obj.get_absolute_url() == expected.format(obj=obj)
+
+
+@pytest.mark.parametrize(
+    "factory_name, expected",
+    [
+        ("music.Artist", "/manage/library/artists/{obj.pk}"),
+        ("music.Album", "/manage/library/albums/{obj.pk}"),
+        ("music.Track", "/manage/library/tracks/{obj.pk}"),
+        ("music.Library", "/manage/library/libraries/{obj.uuid}"),
+        ("federation.Actor", "/manage/moderation/accounts/{obj.full_username}"),
+    ],
+)
+def test_get_moderation_url(factory_name, factories, expected):
+    obj = factories[factory_name]()
+
+    assert obj.get_moderation_url() == expected.format(obj=obj)
diff --git a/api/tests/moderation/test_serializers.py b/api/tests/moderation/test_serializers.py
index 041a8f27432164925ed3e238377a52866cc8c2ca..01cb323eee220050e5afdcd3868972d94d14dea2 100644
--- a/api/tests/moderation/test_serializers.py
+++ b/api/tests/moderation/test_serializers.py
@@ -7,6 +7,7 @@ from django.core.serializers.json import DjangoJSONEncoder
 from funkwhale_api.common import utils as common_utils
 from funkwhale_api.federation import models as federation_models
 from funkwhale_api.moderation import serializers
+from funkwhale_api.moderation import signals
 
 
 def test_user_filter_serializer_repr(factories):
@@ -225,3 +226,32 @@ def test_report_serializer_save_unauthenticated_validation(
     payload["target"] = target_data
     serializer = serializers.ReportSerializer(data=payload, context=context)
     assert serializer.is_valid() is is_valid
+
+
+def test_report_create_send_websocket_event(factories, mocker):
+    target = factories["music.Artist"]()
+    group_send = mocker.patch("funkwhale_api.common.channels.group_send")
+    report_created = mocker.spy(signals.report_created, "send")
+    payload = {
+        "summary": "Report content",
+        "type": "illegal_content",
+        "target": {"type": "artist", "id": target.pk},
+        "submitter_email": "test@submitter.example",
+    }
+    serializer = serializers.ReportSerializer(data=payload)
+
+    assert serializer.is_valid(raise_exception=True) is True
+    report = serializer.save()
+    report_created.assert_called_once_with(sender=None, report=report)
+    group_send.assert_called_with(
+        "admin.moderation",
+        {
+            "type": "event.send",
+            "text": "",
+            "data": {
+                "type": "report.created",
+                "report": serializer.data,
+                "unresolved_count": 1,
+            },
+        },
+    )
diff --git a/api/tests/moderation/test_tasks.py b/api/tests/moderation/test_tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..18e031fd8ac1c853c566261c8418b36cffe05ef7
--- /dev/null
+++ b/api/tests/moderation/test_tasks.py
@@ -0,0 +1,45 @@
+from funkwhale_api.federation import utils as federation_utils
+
+from funkwhale_api.moderation import tasks
+
+
+def test_report_created_signal_calls_send_new_report_mail(factories, mocker):
+    report = factories["moderation.Report"]()
+    on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
+    tasks.signals.report_created.send(sender=None, report=report)
+    on_commit.assert_called_once_with(
+        tasks.send_new_report_email_to_moderators.delay, report_id=report.pk
+    )
+
+
+def test_report_created_signal_sends_email_to_mods(factories, mailoutbox, settings):
+    mod1 = factories["users.User"](permission_moderation=True)
+    mod2 = factories["users.User"](permission_moderation=True)
+    # inactive, so no email
+    factories["users.User"](permission_moderation=True, is_active=False)
+
+    report = factories["moderation.Report"]()
+
+    tasks.send_new_report_email_to_moderators(report_id=report.pk)
+
+    detail_url = federation_utils.full_url(
+        "/manage/moderation/reports/{}".format(report.uuid)
+    )
+    unresolved_reports_url = federation_utils.full_url(
+        "/manage/moderation/reports?q=resolved:no"
+    )
+    for i, mod in enumerate([mod1, mod2]):
+        m = mailoutbox[i]
+        assert m.subject == "[{} moderation - {}] New report from {}".format(
+            settings.FUNKWHALE_HOSTNAME,
+            report.get_type_display(),
+            report.submitter.full_username,
+        )
+        assert report.summary in m.body
+        assert report.target._meta.verbose_name.title() in m.body
+        assert str(report.target) in m.body
+        assert report.target.get_absolute_url() in m.body
+        assert report.target.get_moderation_url() in m.body
+        assert detail_url in m.body
+        assert unresolved_reports_url in m.body
+        assert list(m.to) == [mod.email]
diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py
index 1b185e55f88844baa9400a82c49dff3d706a24f7..c98472a27838cad0357ce8f50b5928d1dba1c835 100644
--- a/api/tests/users/test_models.py
+++ b/api/tests/users/test_models.py
@@ -176,11 +176,12 @@ def test_creating_actor_from_user(factories, settings):
 
 
 def test_get_channels_groups(factories):
-    user = factories["users.User"]()
+    user = factories["users.User"](permission_library=True)
 
     assert user.get_channels_groups() == [
         "user.{}.imports".format(user.pk),
         "user.{}.inbox".format(user.pk),
+        "admin.library",
     ]
 
 
diff --git a/front/src/App.vue b/front/src/App.vue
index 0895962994978d27e5e518beb1b24f822c6c576d..9881554f9f102a6b5507de2c40e2f9f8279d0de3 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -107,6 +107,11 @@ export default {
       id: 'sidebarReviewEditCount',
       handler: this.incrementReviewEditCountInSidebar
     })
+    this.$store.commit('ui/addWebsocketEventHandler', {
+      eventName: 'report.created',
+      id: 'sidebarPendingReviewReportCount',
+      handler: this.incrementPendingReviewReportsCountInSidebar
+    })
   },
   mounted () {
     let self = this
@@ -133,6 +138,10 @@ export default {
       eventName: 'mutation.updated',
       id: 'sidebarReviewEditCount',
     })
+    this.$store.commit('ui/removeWebsocketEventHandler', {
+      eventName: 'mutation.updated',
+      id: 'sidebarPendingReviewReportCount',
+    })
     this.disconnect()
   },
   methods: {
@@ -142,6 +151,10 @@ export default {
     incrementReviewEditCountInSidebar (event) {
       this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewEdits', value: event.pending_review_count})
     },
+    incrementPendingReviewReportsCountInSidebar (event) {
+      console.log('HELLO', event)
+      this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewReports', value: event.unresolved_count})
+    },
     fetchNodeInfo () {
       let self = this
       axios.get('instance/nodeinfo/2.0/').then(response => {
diff --git a/front/src/store/ui.js b/front/src/store/ui.js
index fccbf934864886002303ff11161c096fcc424480..989e8b2adfdde78dbd6e27950a80256f55177129 100644
--- a/front/src/store/ui.js
+++ b/front/src/store/ui.js
@@ -22,6 +22,7 @@ export default {
       'import.status_updated': {},
       'mutation.created': {},
       'mutation.updated': {},
+      'report.created': {},
     },
     pageTitle: null
   },
diff --git a/front/src/views/admin/moderation/ReportsList.vue b/front/src/views/admin/moderation/ReportsList.vue
index 7313828f217a58ae496b4450b7a2124eda85e640..9cb4af7f74aed5bbb3441f698bca9cd8390ccb45 100644
--- a/front/src/views/admin/moderation/ReportsList.vue
+++ b/front/src/views/admin/moderation/ReportsList.vue
@@ -135,7 +135,10 @@ export default {
       axios.get('manage/moderation/reports/', {params: params}).then((response) => {
         self.result = response.data
         self.isLoading = false
-        // self.fetchTargets()
+        if (self.search.query === 'resolved:no') {
+          console.log('Refreshing sidebar notifications')
+          self.$store.commit('ui/incrementNotifications', {type: 'pendingReviewReports', value: response.data.count})
+        }
       }, error => {
         self.isLoading = false
         self.errors = error.backendErrors