diff --git a/README.md b/README.md
index de540f579429cb6f13e42c8b56a2789bf1a65478..a2061afdcc2b2bfe737679d8f216a00174b54b78 100644
--- a/README.md
+++ b/README.md
@@ -32,17 +32,47 @@ export TAGS_TO_MIRROR="IO_EVT, IO_SUM, IO_TSL, IO_TSL0, IO_TSL1, IO_TSL2, IO_TSS
 ```
     
 
-After that, use the following command to start the ``supervisor``:
+After that, use the following command to start the ``supervisor``, which
+you only need to do once:
 
     source setenv.sh
     make start
 
-To see the status of the processes, use ``supervisorctl status``
+From now on ``supervisorctl`` is the tool to communicate with the monitoring
+system. To see the status of the processes, use ``supervisorctl status``,
+which will show each process one by one (make sure you call it in the
+folder where you launched it):
 
+```
+$ supervisorctl status
+ligiers:ligiermirror                  RUNNING   pid 611, uptime 1 day, 7:55:09
+ligiers:monitoring_ligier             RUNNING   pid 610, uptime 1 day, 7:55:09
+logging:msg_dumper                    RUNNING   pid 7466, uptime 1 day, 7:28:00
+logging:weblog                        RUNNING   pid 7465, uptime 1 day, 7:28:00
+monitoring_process:ahrs_calibration   RUNNING   pid 19612, uptime 1 day, 1:20:32
+monitoring_process:dom_activity       RUNNING   pid 626, uptime 1 day, 7:55:09
+monitoring_process:dom_rates          RUNNING   pid 631, uptime 1 day, 7:55:09
+monitoring_process:pmt_hrv            RUNNING   pid 633, uptime 1 day, 7:55:09
+monitoring_process:pmt_rates          RUNNING   pid 632, uptime 1 day, 7:55:09
+monitoring_process:rttc               RUNNING   pid 9717, uptime 10:55:53
+monitoring_process:trigger_rates      RUNNING   pid 637, uptime 1 day, 7:55:09
+monitoring_process:triggermap         RUNNING   pid 638, uptime 1 day, 7:55:09
+monitoring_process:ztplot             RUNNING   pid 7802, uptime 1 day, 7:26:13
+webserver                             RUNNING   pid 29494, uptime 1 day, 0:34:23
+```
+
+The processes are grouped accordingly (ligier, monitoring_process etc.) and
+automaticallly started in the right order.
 
 You can stop and start individual services using ``supervisorctl stop
 group:process_name`` and ``supervisorctl start group:process_name``
 
+Since the system knows the order, you can safely ``restart all`` or just
+a group of processes. Use the ``supervisorctl help`` to find out more and
+``supervisorctl help COMMAND`` to get a detailed description of the
+corresponding command.
+
+
 ## Configuration file
 
 A file called `pipeline.toml` can be placed into the root folder of the
diff --git a/app/routes.py b/app/routes.py
index 11234379ae61ddbadc2c396ddf256aa85dab444e..03ead0b0d36ea3d297bb5693a09ca788a3b0d4ec 100644
--- a/app/routes.py
+++ b/app/routes.py
@@ -1,4 +1,5 @@
-from os.path import join, exists
+from glob import glob
+from os.path import basename, join, exists, splitext
 from functools import wraps
 import toml
 from flask import render_template, send_from_directory, request, Response
@@ -13,7 +14,7 @@ app.config['FREEZER_DESTINATION'] = '../km3web'
 PLOTS = [['dom_activity', 'dom_rates'], ['pmt_rates', 'pmt_hrv'],
          ['trigger_rates'], ['ztplot', 'triggermap']]
 
-AHRS_PLOTS = [['yaw_calib'], ['pitch_calib'], ['roll_calib']]
+AHRS_PLOTS = ['yaw_calib_du*', 'pitch_calib_du*', 'roll_calib_du*']
 TRIGGER_PLOTS = [['trigger_rates'], ['trigger_rates_lin']]
 K40_PLOTS = [['intradom'], ['angular_k40rate_distribution']]
 RTTC_PLOTS = [['rttc']]
@@ -21,7 +22,7 @@ RECO_PLOTS = [['track_reco', 'ztplot_roy'], ['time_residuals']]
 COMPACT_PLOTS = [['dom_activity', 'dom_rates', 'pmt_rates', 'pmt_hrv'],
                  ['trigger_rates', 'trigger_rates_lin'],
                  ['ztplot', 'ztplot_roy', 'triggermap']]
-SN_PLOTS = [['sn_bg_distribution']]
+SN_PLOTS = [['sn_bg_histogram', 'sn_pk_history']]
 
 if exists(CONFIG_PATH):
     config = toml.load(CONFIG_PATH)
@@ -31,6 +32,21 @@ if exists(CONFIG_PATH):
         PASSWORD = config["WebServer"]["password"]
 
 
+def expand_wildcards(plot_layout):
+    """Replace wildcard entries with list of files"""
+    plots = []
+    for row in plot_layout:
+        if not isinstance(row, list) and '*' in row:
+            plots.append(
+                sorted([
+                    splitext(basename(p))[0]
+                    for p in glob(join(app.root_path, PLOTS_PATH, row))
+                ]))
+        else:
+            plots.append(row)
+    return plots
+
+
 def check_auth(username, password):
     """This function is called to check if a username /
     password combination is valid.
@@ -76,29 +92,31 @@ def add_header(r):
 @app.route('/index.html')
 @requires_auth
 def index():
-    return render_template('plots.html', plots=PLOTS)
+    return render_template('plots.html', plots=expand_wildcards(PLOTS))
 
 
 @app.route('/ahrs.html')
 @requires_auth
 def ahrs():
-    return render_template('plots.html', plots=AHRS_PLOTS)
+    return render_template('plots.html', plots=expand_wildcards(AHRS_PLOTS))
 
 
 @app.route('/reco.html')
 @requires_auth
 def reco():
-    return render_template('plots.html', plots=RECO_PLOTS)
+    return render_template('plots.html', plots=expand_wildcards(RECO_PLOTS))
+
 
 @app.route('/sn.html')
 @requires_auth
 def supernova():
-    return render_template('plots.html', plots=SN_PLOTS)
+    return render_template('plots.html', plots=expand_wildcards(SN_PLOTS))
+
 
 @app.route('/compact.html')
 @requires_auth
 def compact():
-    return render_template('plots.html', plots=COMPACT_PLOTS)
+    return render_template('plots.html', plots=expand_wildcards(COMPACT_PLOTS))
 
 
 @app.route('/rttc.html')
@@ -106,7 +124,7 @@ def compact():
 def rttc():
     return render_template(
         'plots.html',
-        plots=RTTC_PLOTS,
+        plots=expand_wildcards(RTTC_PLOTS),
         info=
         "Cable Round Trip Time calculated from realtime data provided by the "
         "Detector Manager. The red lines shows the median and the STD "
@@ -119,7 +137,7 @@ def rttc():
 def k40():
     return render_template(
         'plots.html',
-        plots=K40_PLOTS,
+        plots=expand_wildcards(K40_PLOTS),
         info="The first plot shows the intra-DOM calibration. "
         "y-axis: delta_t [ns], x-axis: cosine of angles. "
         "The second plot the angular distribution of K40 rates. "
@@ -130,7 +148,7 @@ def k40():
 @app.route('/trigger.html')
 @requires_auth
 def trigger():
-    return render_template('plots.html', plots=TRIGGER_PLOTS)
+    return render_template('plots.html', plots=expand_wildcards(TRIGGER_PLOTS))
 
 
 @app.route('/plots/<path:filename>')
diff --git a/scripts/ahrs_calibration.py b/scripts/ahrs_calibration.py
index d5db82c16da907c693fec202e1369f8a0b99058d..61d3a09c8d7de8ec88898968c92ae45cbbf1a194 100755
--- a/scripts/ahrs_calibration.py
+++ b/scripts/ahrs_calibration.py
@@ -44,21 +44,29 @@ class CalibrateAHRS(kp.Module):
         self.plots_path = self.require('plots_path')
         det_id = self.require('det_id')
         self.detector = kp.hardware.Detector(det_id=det_id)
-        self.du = self.get('du', default=1)
+        self.dus = set()
 
         self.clbmap = kp.db.CLBMap(det_oid=det_id)
 
         self.cuckoo = kp.time.Cuckoo(60, self.create_plot)
         self.cuckoo_log = kp.time.Cuckoo(10, print)
+
         self.data = {}
-        queue_size = 50000
-        for ahrs_param in ('yaw', 'pitch', 'roll'):
-            self.data[ahrs_param] = defaultdict(
-                partial(deque, maxlen=queue_size))
-        self.times = defaultdict(partial(deque, maxlen=queue_size))
+        self.queue_size = 50000
+
         self.lock = threading.Lock()
         self.index = 0
 
+    def _register_du(self, du):
+        """Create data cache for DU"""
+        self.data[du] = {}
+        for ahrs_param in ('yaw', 'pitch', 'roll'):
+            self.data[du][ahrs_param] = defaultdict(
+                partial(deque, maxlen=self.queue_size))
+        self.data[du]['times'] = defaultdict(
+            partial(deque, maxlen=self.queue_size))
+        self.dus.add(du)
+
     def process(self, blob):
         self.index += 1
         if self.index % 29 != 0:
@@ -71,23 +79,24 @@ class CalibrateAHRS(kp.Module):
             self.log.info("Skipping base CLB")
             return blob
 
-        if clb.du != self.du:
-            return blob
-
         yaw = tmch_data.yaw
         calib = get_latest_ahrs_calibration(clb.upi, max_version=4)
 
         if calib is None:
+            self.log.warning("No calibration found for CLB UPI '%s'", clb.upi)
             return blob
 
+        du = clb.du
+        if du not in self.dus:
+            self._register_du(du)
         cyaw, cpitch, croll = fit_ahrs(tmch_data.A, tmch_data.H, *calib)
         self.cuckoo_log("DU{}-DOM{} (random pick): calibrated yaw={}".format(
             clb.du, clb.floor, cyaw))
         with self.lock:
-            self.data['yaw'][clb.floor].append(cyaw)
-            self.data['pitch'][clb.floor].append(cpitch)
-            self.data['roll'][clb.floor].append(croll)
-            self.times[clb.floor].append(now)
+            self.data[du]['yaw'][clb.floor].append(cyaw)
+            self.data[du]['pitch'][clb.floor].append(cpitch)
+            self.data[du]['roll'][clb.floor].append(croll)
+            self.data[du]['times'][clb.floor].append(now)
 
         self.cuckoo.msg()
         return blob
@@ -96,30 +105,33 @@ class CalibrateAHRS(kp.Module):
         print(self.__class__.__name__ + ": updating plot.")
         # xfmt = md.DateFormatter('%Y-%m-%d %H:%M')
         xfmt = md.DateFormatter('%H:%M')
-        for ahrs_param in self.data.keys():
-            fig, ax = plt.subplots(figsize=(16, 6))
-            sns.set_palette("husl", 18)
-            ax.set_title("AHRS {} Calibration on DU{}\n{}".format(
-                ahrs_param, self.du, datetime.utcnow()))
-            ax.set_xlabel("UTC time")
-            ax.xaxis.set_major_formatter(xfmt)
-            ax.set_ylabel(ahrs_param)
-            with self.lock:
-                for floor in sorted(self.data[ahrs_param].keys()):
-                    ax.plot(
-                        self.times[floor],
-                        self.data[ahrs_param][floor],
-                        marker='.',
-                        linestyle='none',
-                        label="Floor {}".format(floor))
-            lgd = plt.legend(
-                bbox_to_anchor=(1.005, 1), loc=2, borderaxespad=0.)
-            fig.tight_layout()
-            plt.savefig(
-                os.path.join(self.plots_path, ahrs_param + '_calib.png'),
-                bbox_extra_artists=(lgd, ),
-                bbox_inches='tight')
-            plt.close('all')
+        for du in self.dus:
+            data = self.data[du]
+            for ahrs_param in data.keys():
+                fig, ax = plt.subplots(figsize=(16, 6))
+                sns.set_palette("husl", 18)
+                ax.set_title("AHRS {} Calibration on DU{}\n{}".format(
+                    ahrs_param, du, datetime.utcnow()))
+                ax.set_xlabel("UTC time")
+                ax.xaxis.set_major_formatter(xfmt)
+                ax.set_ylabel(ahrs_param)
+                with self.lock:
+                    for floor in sorted(data[ahrs_param].keys()):
+                        ax.plot(data['times'][floor],
+                                data[ahrs_param][floor],
+                                marker='.',
+                                linestyle='none',
+                                label="Floor {}".format(floor))
+                lgd = plt.legend(bbox_to_anchor=(1.005, 1),
+                                 loc=2,
+                                 borderaxespad=0.)
+                fig.tight_layout()
+                plt.savefig(os.path.join(
+                    self.plots_path,
+                    ahrs_param + '_calib_du{}.png'.format(du)),
+                            bbox_extra_artists=(lgd, ),
+                            bbox_inches='tight')
+                plt.close('all')
 
 
 def main():
@@ -132,13 +144,12 @@ def main():
     ligier_port = int(args['-p'])
 
     pipe = kp.Pipeline()
-    pipe.attach(
-        kp.io.ch.CHPump,
-        host=ligier_ip,
-        port=ligier_port,
-        tags='IO_MONIT',
-        timeout=60 * 60 * 24 * 7,
-        max_queue=2000)
+    pipe.attach(kp.io.ch.CHPump,
+                host=ligier_ip,
+                port=ligier_port,
+                tags='IO_MONIT',
+                timeout=60 * 60 * 24 * 7,
+                max_queue=2000)
     pipe.attach(kp.io.daq.DAQProcessor)
     pipe.attach(CalibrateAHRS, det_id=det_id, plots_path=plots_path)
     pipe.drain()