diff --git a/src/core/management/commands/appversion.py b/src/core/management/commands/appversion.py
index c44b38ed9f0c4bd2b1c223887e242346db47a09d..588e704dc3412d29623c999b445b6bb8193aa58a 100644
--- a/src/core/management/commands/appversion.py
+++ b/src/core/management/commands/appversion.py
@@ -1,19 +1,19 @@
 from django.conf import settings
-from django.core.management.base import BaseCommand
+from django_rich.management import RichCommand
 
 
-class Command(BaseCommand):
+class Command(RichCommand):
     def handle(self, *args, **options):
         version_data = settings.APP_VERSION_INFO
 
         if version_data.get('tag') == 'DEV':
-            self.stdout.write('DEVELOPMENT VERSION', self.style.WARNING)
+            self.console.print('DEVELOPMENT VERSION', style='yellow bold')
             return
 
         if version_data.get('ci') is True:
-            self.stdout.write('CI BUILD', self.style.SUCCESS)
+            self.console.print('CI BUILD', style='green bold')
 
         for k, v in version_data.items():
             if v is None or v == '' or k in ['ci']:
                 continue
-            self.stdout.write(self.style.MIGRATE_HEADING(str(k)) + ': ' + str(v))
+            self.console.print(f'[cyan bold]{k:s}[/cyan bold]: {v:s}')
diff --git a/src/core/management/commands/create_conference.py b/src/core/management/commands/create_conference.py
index ad5f4069a517a105c2e88f97e11727094d23ed54..7023a6969c9f5a953e665434e46b43e52b389616 100644
--- a/src/core/management/commands/create_conference.py
+++ b/src/core/management/commands/create_conference.py
@@ -1,12 +1,14 @@
 from argparse import ArgumentTypeError, BooleanOptionalAction
 from datetime import datetime
 
+from rich.console import Console
 from zoneinfo import ZoneInfo
 
 from django.contrib.auth.models import Group
 from django.core.management import call_command
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import CommandError
 from django.utils.text import slugify
+from django_rich.management import RichCommand
 
 from core.models.assemblies import Assembly
 from core.models.badges import BadgeCategory
@@ -78,7 +80,7 @@ INTRO_PAGES = {
 }
 
 
-def seed_conference(conf: Conference, year: int = 0):
+def seed_conference(conf: Conference, console: Console, year: int = 0):
     if Assembly.objects.filter(conference=conf).exists():
         raise ValueError('Conference already has assemblies?!')
 
@@ -374,7 +376,7 @@ def _validate_date(s: str) -> datetime:
         raise ArgumentTypeError(f"Not a valid date: {s!r}. Use format 'YYYY'") from exc
 
 
-class Command(BaseCommand):
+class Command(RichCommand):
     def add_arguments(self, parser):
         parser.add_argument(
             'name',
@@ -430,7 +432,10 @@ class Command(BaseCommand):
         elif not (year := options.get('year')):
             year = datetime.now()  # noqa: DTZ005
         year = year.year
-        print(f'Creating conference {slug} ({name}) for year {year}')
+
+        self.console.print(
+            f'Creating conference [bold blue]{slug}[/bold blue] (name: [bold blue]{name}[/bold blue]){ f' for year {year}' if year != 0 else ""}'
+        )
 
         # check if the conference already exists
         if Conference.objects.filter(slug=slug).exists():
@@ -443,35 +448,39 @@ class Command(BaseCommand):
 
             bootstrap = answer.lower() == 'y'
             if bootstrap:
-                self.stdout.write(self.style.WARNING('Groups will be loaded and may be duplicated!'))
+                self.console.print('Groups will be loaded and may be duplicated!', style='yellow bold')
             else:
-                self.stdout.write(self.style.WARNING('Bootstrap will not be applied.'))
+                self.console.print('Bootstrap will not be applied.', style='yellow bold')
 
         # load our bootstrap fixtures
         if bootstrap or (bootstrap is None and not Group.objects.exists()):
-            self.stdout.write('Loading bootstrap data...')
-            call_command('loaddata', 'bootstrap_auth_groups.json')
-            self.stdout.write(self.style.SUCCESS('Bootstrap data loaded.'))
+            with self.console.status('Bootstrapping groups..') as status:
+                call_command('loaddata', 'bootstrap_auth_groups.json')
+                self.console.print('Creating conference... [green]done[/green]', style='green bold')
         elif bootstrap is None and Group.objects.exists():
-            self.stdout.write(self.style.SUCCESS('Groups are already present in the database.'))
+            self.console.print('Groups are already present in the database.', style='green bold')
 
-        # create the new conference
-        conf = Conference.objects.create(
-            slug=slug,
-            name=name,
-            is_public=True,
-        )
+        with self.console.status('Creating conference...') as status:
+            # create the new conference
+            conf = Conference.objects.create(
+                slug=slug,
+                name=name,
+                is_public=True,
+            )
 
-        # for CCC Jahresendveranstaltung we can guess some timestamps from the current year alone
-        if year > 1970:
-            conf.start = datetime(year, 12, 27, 10, 0, 0, tzinfo=ZoneInfo('Europe/Berlin'))
-            conf.end = datetime(year, 12, 30, 18, 0, 0, tzinfo=ZoneInfo('Europe/Berlin'))
-            conf.registration_deadline = datetime(year, 12, 1, 0, 0, 0, tzinfo=ZoneInfo('Europe/Berlin'))
+            # for CCC Jahresendveranstaltung we can guess some timestamps from the current year alone
+            if year > 1970:
+                status.update('Setting deadline...')
+                conf.start = datetime(year, 12, 27, 10, 0, 0, tzinfo=ZoneInfo('Europe/Berlin'))
+                conf.end = datetime(year, 12, 30, 18, 0, 0, tzinfo=ZoneInfo('Europe/Berlin'))
+                conf.registration_deadline = datetime(year, 12, 1, 0, 0, 0, tzinfo=ZoneInfo('Europe/Berlin'))
+                self.console.print('Setting deadline... [green]done[/green]', style='bold')
 
-        # save everything
-        conf.save()
-        self.stdout.write(self.style.SUCCESS(f'Created conference {conf.pk}'))
+            # save everything
+            conf.save()
+        self.console.print('Creating conference... [green]done[/green]', style='bold')
 
         # fill in initial data
-        seed_conference(conf, year=year)
-        self.stdout.write(self.style.SUCCESS(f'Initial data set up for "{conf.slug}"'))
+        with self.console.status('Setting up initial data for [blue bold]{conf.slug}[/blue bold]...') as status:
+            seed_conference(conf, console=self.console, year=year)
+        self.console.print(f'Setting up initial data for [blue bold]{conf.slug}[/blue bold]... [green]done[/green]', style='bold')
diff --git a/src/core/management/commands/housekeeping.py b/src/core/management/commands/housekeeping.py
index a81d4bf19ab3f08fb83fd20a2218902be2e03670..80ce7091fcf2fdd537a88711095b42b8805ce8d0 100644
--- a/src/core/management/commands/housekeeping.py
+++ b/src/core/management/commands/housekeeping.py
@@ -1,8 +1,10 @@
+import sys
 import time
 
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import CommandError
 from django.db.models import Max
 from django.utils import timezone
+from django_rich.management import RichCommand
 
 from core.models.conference import Conference, ConferenceExportCache
 from core.models.messages import DirectMessage
@@ -11,7 +13,11 @@ from core.models.schedules import ScheduleSource, ScheduleSourceImport
 from core.models.voucher import Voucher
 
 
-class Command(BaseCommand):
+def _no_traceback_exception_hook(exc_type, exc_val, traceback):
+    """Hook to prevent traceback on KeyboardInterrupt."""
+
+
+class Command(RichCommand):
     def add_arguments(self, parser):
         parser.add_argument('--forever', action='store_true', help='repeat the housekeeping forever (until Ctrl+C is pressed)')
         parser.add_argument('--forever-delay', type=int, default=300, help='seconds to wait between housekeeping runs')
@@ -20,19 +26,19 @@ class Command(BaseCommand):
 
     def _housekeeping_directmessages(self):
         # clear all direct messages which are after their expiry date
-        print('Deleting messages ... ', end='', flush=True)
+        self.console.print('Deleting messages ... ', end='', style='red')
         deleted_msgs_count, _ = DirectMessage.objects.filter(autodelete_after__isnull=False, autodelete_after__lte=timezone.now()).delete()
-        print(deleted_msgs_count)
+        self.console.print(deleted_msgs_count)
 
     def _housekeeping_vouchers(self):
         # do auto-assignments
-        print('Auto-assigning vouchers ... ', end='', flush=True)
+        self.console.print('Auto-assigning vouchers ... ', end='', style='green')
         vouchers_assigned = Voucher.do_auto_assignments()
-        print(vouchers_assigned)
+        self.console.print(vouchers_assigned)
 
     def _housekeeping_scheduleimports(self):
         # schedules
-        print('Schedule imports ... ', end='', flush=True)
+        self.console.print('Schedule imports ... ', end='', style='blue')
         schedule_results = {}
         schedule_failure, schedule_success, schedule_skipped = 0, 0, 0
         for schedule in ScheduleSource.objects.all():
@@ -63,7 +69,7 @@ class Command(BaseCommand):
                 schedule_failure += 1
 
         # print schedule import results
-        print(schedule_success, '/', len(schedule_results), ' (', schedule_skipped, ' skipped)', sep='')
+        self.console.print(f'{schedule_success}/{len(schedule_results)} ({schedule_skipped} skipped)', style='cyan bold')
         for k, v in schedule_results.items():
             print('  ', k, ' => ', v, sep='')
 
@@ -94,12 +100,12 @@ class Command(BaseCommand):
             entry.save(update_fields=['needs_regeneration'])
             changed += 1
 
-        print('Flagged', changed, 'out of', total, 'cached exports for regeneration.')
+        self.console.print(f'Flagged {changed} out of {total} cached exports for regeneration.', style='yellow')
 
     def _housekeeping_wikimports(self):
         for c in Conference.objects.all():
             for ns in StaticPageNamespace.objects.filter(conference=c).exclude(upstream_url__isnull=True).all():
-                print(f'Importing "{c.slug}" wiki namespace "{ns.prefix}" ...')
+                self.console.print(f'Importing "{c.slug}" wiki namespace "{ns.prefix}" ...', style='blue')
                 ns.fetch_upstream()
 
     def handle(self, *args, **options):
@@ -109,27 +115,29 @@ class Command(BaseCommand):
         if forever:
             if forever_delay <= 0:
                 raise CommandError('The --forever-delay value must a positive value (in seconds).')
-            print(f'Running housekeeping forever each {forever_delay}s:', end='\n\n')
-
-        while True:
-            if forever:
-                print(timezone.now().isoformat())
-
-            # call the individual housekeeping methods
-            self._housekeeping_directmessages()
-            self._housekeeping_vouchers()
-            if not options.get('skip_schedule_imports'):
-                self._housekeeping_scheduleimports()
-            self._housekeeping_exportcache()
-            if not options.get('skip_wiki_imports'):
-                self._housekeeping_wikimports()
-
-            if forever:
-                print()  # empty line
-                try:
+            self.console.print(f'Running housekeeping forever each {forever_delay}s:', end='\n\n')
+
+        try:
+            while True:
+                if forever:
+                    self.console.print(timezone.now().isoformat(), style='orange1', highlight=False)
+
+                # call the individual housekeeping methods
+                self._housekeeping_directmessages()
+                self._housekeeping_vouchers()
+                if not options.get('skip_schedule_imports'):
+                    self._housekeeping_scheduleimports()
+                self._housekeeping_exportcache()
+                if not options.get('skip_wiki_imports'):
+                    self._housekeeping_wikimports()
+
+                if forever:
+                    print()  # empty line
                     time.sleep(forever_delay)
-                except KeyboardInterrupt:
-                    print('Aborted.')
+                else:
                     break
-            else:
-                break
+        except KeyboardInterrupt:
+            if sys.excepthook is sys.__excepthook__:
+                sys.excepthook = _no_traceback_exception_hook
+            print('Aborted.')
+            raise
diff --git a/src/core/management/commands/import_mapservice_resultfile.py b/src/core/management/commands/import_mapservice_resultfile.py
index 241054f0e1a95a9ea7bd1a9530f97de983d18ca2..41d1953b27f3fd18c5fdf7e57b2920879f075218 100644
--- a/src/core/management/commands/import_mapservice_resultfile.py
+++ b/src/core/management/commands/import_mapservice_resultfile.py
@@ -20,5 +20,5 @@ class Command(BaseCommand):
         data = json.load(options.get('filename'))
 
         status_code, response = MapServiceView.handle_mapservice_push_request(conference, data)
-        print('RESPONSE (', status_code, '):', sep='')
+        print(f'RESPONSE ({status_code}):')
         print(response)
diff --git a/src/core/management/commands/rerender_markdown.py b/src/core/management/commands/rerender_markdown.py
index 9dce899decaceb6e9c5f28835259587daedcb3a0..996f3cb5bb716ac2203fba1ff528280e0835429e 100644
--- a/src/core/management/commands/rerender_markdown.py
+++ b/src/core/management/commands/rerender_markdown.py
@@ -1,33 +1,38 @@
-from django.core.management.base import BaseCommand
+from django_rich.management import RichCommand
 
 from core.markdown import render_markdown_ex, store_relationships
 from core.models import Assembly, ConferenceMember, Event, Room, StaticPage
 
 
-class Command(BaseCommand):
+class Command(RichCommand):
     def handle(self, *args, **options):
         # markdown is usually rerendered in the save() method, so calling save() is sufficient here for most models
-        for event in Event.objects.all():
-            event.save(update_fields=['description'])
+        with self.console.status('Rerendering markdown for all events') as status:
+            for event in Event.objects.all():
+                event.save(update_fields=['description'])
 
-        for room in Room.objects.all():
-            room.save(update_fields=['description'])
+            status.update('Rerendering markdown for all rooms')
+            for room in Room.objects.all():
+                room.save(update_fields=['description'])
 
-        for assembly in Assembly.objects.all():
-            assembly.save(update_fields=['description'])
+            status.update('Rerendering markdown for all assemblies')
+            for assembly in Assembly.objects.all():
+                assembly.save(update_fields=['description'])
 
-        for user in ConferenceMember.objects.all():
-            user.save(update_fields=['description'])
+            status.update('Rerendering markdown for all conference members')
+            for user in ConferenceMember.objects.all():
+                user.save(update_fields=['description'])
 
-        for page in StaticPage.objects.all():
-            if page.public_revision > 0:
-                public_page = page.revisions.filter(revision=page.public_revision).first()
-                if public_page is None:
-                    page.body_html = ''
+            status.update('Rerendering markdown for all static pages')
+            for page in StaticPage.objects.all():
+                if page.public_revision > 0:
+                    public_page = page.revisions.filter(revision=page.public_revision).first()
+                    if public_page is None:
+                        page.body_html = ''
+                    else:
+                        render_result = render_markdown_ex(page.conference, public_page.body)
+                        page.body_html = render_result.document
+                        store_relationships(page.conference, page, render_result)
                 else:
-                    render_result = render_markdown_ex(page.conference, public_page.body)
-                    page.body_html = render_result.document
-                    store_relationships(page.conference, page, render_result)
-            else:
-                page.body_html = ''
-            page.save(update_fields=['body_html'])
+                    page.body_html = ''
+                page.save(update_fields=['body_html'])
diff --git a/src/core/management/commands/stats.py b/src/core/management/commands/stats.py
deleted file mode 100644
index 7eb4aca693fed9c2bbffbcb6c32cf9c2538cd79e..0000000000000000000000000000000000000000
--- a/src/core/management/commands/stats.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from django.core.management.base import BaseCommand
-
-from core.models import Assembly, Badge, Conference, Event, Room, UserBadge
-
-
-class Command(BaseCommand):
-    def handle(self, *args, **options):
-        for conf in Conference.objects.all():
-            print('#', conf.slug, '(' + conf.name + ')')
-
-            print(
-                '- assemblies:',
-                Assembly.objects.filter(conference=conf).count(),
-                'total,',
-                Assembly.objects.filter(conference=conf, state__in=Assembly.PUBLIC_STATES).count(),
-                'accepted',
-            )
-
-            print(
-                '- events:',
-                Event.objects.filter(conference=conf).count(),
-                'total,',
-                Event.objects.filter(conference=conf, is_public=True).count(),
-                'visible',
-            )
-
-            print('- rooms:', Room.objects.filter(conference=conf).count(), 'total')
-            for rt in Room.BACKEND_ROOMTYPES:
-                print('  -', Room.objects.filter(conference=conf, room_type=rt).count(), rt.label)
-
-            print('- badges (created):', Badge.objects.filter(conference=conf).count())
-
-            badges_redeemed = {
-                'achievement': UserBadge.objects.filter(badge__is_achievement=True, badge__conference=conf).count(),
-                'sticker': UserBadge.objects.filter(badge__is_achievement=False, badge__conference=conf).count(),
-            }
-            print(f'- badges (redeemed): {badges_redeemed["achievement"]} achievements, {badges_redeemed["sticker"]} stickers')
diff --git a/src/core/management/commands/test_schedule_import.py b/src/core/management/commands/test_schedule_import.py
index 8108cf4abe05fda1bbe57061241932e14541c953..5c8be0207b1749b04ca954cb4b11f8dcc3ec0d89 100644
--- a/src/core/management/commands/test_schedule_import.py
+++ b/src/core/management/commands/test_schedule_import.py
@@ -3,13 +3,13 @@ import json
 
 from django.conf import settings
 from django.core.exceptions import SuspiciousOperation
-from django.core.management.base import BaseCommand
 from django.db import transaction
+from django_rich.management import RichCommand
 
 from core.models.conference import Conference
 
 
-class Command(BaseCommand):
+class Command(RichCommand):
     def add_arguments(self, parser):
         parser.add_argument('--data-file', '-f', type=argparse.FileType('r'), help='the data file to load')
 
@@ -27,4 +27,4 @@ class Command(BaseCommand):
         except SuspiciousOperation:
             pass
 
-        print(json.dumps(activity, indent=2))
+        self.console.print_json(activity)