From ce39bcf7ef5d6c89590f162c9cbe2c01f89b74c1 Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Wed, 8 Apr 2020 20:43:40 +0200
Subject: [PATCH] OO-4626: add panel with statistics of BigBlueButton servers

---
 .../bigbluebutton/BigBlueButtonManager.java   |   3 +
 .../manager/BigBlueButtonManagerImpl.java     |  60 +++++-----
 .../manager/BigBlueButtonUtils.java           |  12 +-
 .../model/BigBlueButtonMeetingInfos.java      |  47 +++++++-
 .../model/BigBlueButtonServerInfos.java       | 107 ++++++++++++++++++
 .../ui/BigBlueButtonAdminController.java      |  30 +++--
 .../BigBlueButtonAdminServersController.java  |  55 ++++++++-
 .../BigBlueButtonAdminServersTableModel.java  |  71 ++++++++++--
 .../ui/BigBlueButtonServerRow.java            |  89 +++++++++++++++
 .../ui/CapacityFactorCellRenderer.java        |  51 +++++++++
 .../ui/_content/servers_admin.html            |   1 +
 .../ui/_i18n/LocalStrings_de.properties       |  56 +++++----
 .../ui/_i18n/LocalStrings_en.properties       |  50 +++++---
 13 files changed, 542 insertions(+), 90 deletions(-)
 create mode 100644 src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonServerInfos.java
 create mode 100644 src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonServerRow.java
 create mode 100644 src/main/java/org/olat/modules/bigbluebutton/ui/CapacityFactorCellRenderer.java
 create mode 100644 src/main/java/org/olat/modules/bigbluebutton/ui/_content/servers_admin.html

diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
index 8e18ffd750a..4547dcc8c36 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
@@ -26,6 +26,7 @@ import org.olat.core.id.Identity;
 import org.olat.core.id.Roles;
 import org.olat.group.BusinessGroup;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonServerInfos;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryRef;
 
@@ -45,6 +46,8 @@ public interface BigBlueButtonManager {
 	
 	public List<BigBlueButtonServer> getServers();
 	
+	public List<BigBlueButtonServerInfos> getServersInfos();
+	
 	public void deleteServer(BigBlueButtonServer server, BigBlueButtonErrors errors);
 	
 	
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
index a05e56c3412..43d08f1e58f 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
@@ -22,6 +22,7 @@ package org.olat.modules.bigbluebutton.manager;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -63,6 +64,7 @@ import org.olat.modules.bigbluebutton.model.BigBlueButtonErrorCodes;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingInfos;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonServerInfos;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryRef;
 import org.olat.repository.RepositoryManager;
@@ -225,6 +227,12 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		return bigBlueButtonServerDao.getServers();
 	}
 
+	@Override
+	public List<BigBlueButtonServerInfos> getServersInfos() {
+		List<BigBlueButtonServer> servers = getServers();
+		return getServersInfos(servers);
+	}
+
 	@Override
 	public void deleteServer(BigBlueButtonServer server, BigBlueButtonErrors errors) {
 		List<BigBlueButtonMeeting> meetings = bigBlueButtonMeetingDao.getMeetings(server);
@@ -349,6 +357,15 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 	}
 	
 	private BigBlueButtonServer getBigBlueButtonServer(List<BigBlueButtonServer> servers) {
+		List<BigBlueButtonServerInfos> serversInfos = getServersInfos(servers);
+		if(serversInfos.isEmpty()) {
+			return null;
+		}
+		Collections.sort(serversInfos, new ServerLoadComparator());
+		return serversInfos.get(0).getServer();
+	}
+	
+	private List<BigBlueButtonServerInfos> getServersInfos(List<BigBlueButtonServer> servers) {
 		CountDownLatch serverLatch = new CountDownLatch(servers.size());
 		
 		List<MeetingInfosThread> threads = new ArrayList<>();
@@ -363,21 +380,16 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		} catch (InterruptedException e) {
 			log.error("", e);
 		}
-		
-		List<ServerLoad> serversLoad = threads.stream()
-				.filter(MeetingInfosThread::isExecuted)
-				.filter(thread -> !thread.hasErrors())
-				.map(thread -> calculateLoad(thread.getServer(), thread.getMeetingsInfos()))
-				.collect(Collectors.toList());
-		
-		if(serversLoad.isEmpty()) {
-			return null;
-		}
-		Collections.sort(serversLoad);
-		return serversLoad.get(0).getServer();
+
+		return threads.stream()
+			.filter(MeetingInfosThread::isExecuted)
+			.filter(thread -> !thread.hasErrors())
+			.map(thread -> new BigBlueButtonServerInfos(thread.getServer(), thread.getMeetingsInfos(),
+					calculateLoad(thread.getServer(), thread.getMeetingsInfos())))
+			.collect(Collectors.toList());
 	}
 	
-	private ServerLoad calculateLoad(BigBlueButtonServer server, List<BigBlueButtonMeetingInfos> meetingsInfos) {
+	private double calculateLoad(BigBlueButtonServer server, List<BigBlueButtonMeetingInfos> meetingsInfos) {
 		double load = 0.0d;
 		for(BigBlueButtonMeetingInfos meetingInfos:meetingsInfos) {
 			load += meetingInfos.getListenerCount() * 1.0d;
@@ -389,7 +401,7 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 				&& server.getCapacityFactory().doubleValue() > 1.0) {
 			load = load / server.getCapacityFactory().doubleValue();
 		}
-		return new ServerLoad(server, load);
+		return load;
 	}
 	
 	private List<BigBlueButtonMeetingInfos> getMeetingInfos(BigBlueButtonServer server, BigBlueButtonErrors errors) {
@@ -750,23 +762,13 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, Initializ
 		}
 	}
 	
-	private static class ServerLoad implements Comparable<ServerLoad> {
-		
-		private final double load;
-		private final BigBlueButtonServer server;
-		
-		public ServerLoad(BigBlueButtonServer server, double load) {
-			this.server = server;
-			this.load = load;
-		}
-
-		public BigBlueButtonServer getServer() {
-			return server;
-		}
+	private static class ServerLoadComparator implements Comparator<BigBlueButtonServerInfos> {
 
 		@Override
-		public int compareTo(ServerLoad o) {
-			return Double.compare(load, o.load);
+		public int compare(BigBlueButtonServerInfos o1, BigBlueButtonServerInfos o2) {
+			double l1 = o1.getLoad();
+			double l2 = o2.getLoad();
+			return Double.compare(l1, l2);
 		}
 	}
 	
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java
index 9c5cb30fee6..e8c854043ba 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUtils.java
@@ -135,7 +135,9 @@ public class BigBlueButtonUtils {
     	for(int i=meetingList.getLength(); i-->0; ) {
     		Element meetingEl = (Element)meetingList.item(i);
     		String meetingId = getFirstElementValue(meetingEl, "meetingID");
-    		BigBlueButtonMeetingInfos meeting = new BigBlueButtonMeetingInfos(meetingId);
+    		String running = getFirstElementValue(meetingEl, "running");
+    		
+    		BigBlueButtonMeetingInfos meeting = new BigBlueButtonMeetingInfos(meetingId, "true".equals(running));
     		meetings.add(meeting);
     		
     		String videoCount = getFirstElementValue(meetingEl, "videoCount");
@@ -149,6 +151,14 @@ public class BigBlueButtonUtils {
     		meeting.setParticipantCount(toLong(participantCount));
     		String moderatorCount = getFirstElementValue(meetingEl, "moderatorCount");
     		meeting.setModeratorCount(toLong(moderatorCount));
+    		
+    		String recording = getFirstElementValue(meetingEl, "recording");
+    		meeting.setRecording("true".equals(recording));
+    		String isBreakout = getFirstElementValue(meetingEl, "isBreakout");
+    		meeting.setBreakout("true".equals(isBreakout));
+    		
+    		String maxUsers = getFirstElementValue(meetingEl, "maxUsers");
+    		meeting.setMaxUsers(toLong(maxUsers));
     	}
     	return meetings;
     }
diff --git a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingInfos.java b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingInfos.java
index 5f0fea5e1fa..7dd1ea3da07 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingInfos.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingInfos.java
@@ -28,6 +28,7 @@ package org.olat.modules.bigbluebutton.model;
 public class BigBlueButtonMeetingInfos {
 	
 	private final String meetingId;
+	private final boolean running;
 	
 	private long videoCount;
 	private long listenerCount;
@@ -36,13 +37,25 @@ public class BigBlueButtonMeetingInfos {
 	private long participantCount;
 	private long moderatorCount;
 	
-	public BigBlueButtonMeetingInfos(String meetingId) {
+	private boolean recording;
+	private boolean breakout;
+	
+	private long maxUsers;
+	
+	private double load;
+	
+	public BigBlueButtonMeetingInfos(String meetingId, boolean running) {
 		this.meetingId = meetingId;
+		this.running = running;
 	}
 	
 	public String getMeetingId() {
 		return meetingId;
 	}
+	
+	public boolean isRunning() {
+		return running;
+	}
 
 	public long getVideoCount() {
 		return videoCount;
@@ -84,6 +97,38 @@ public class BigBlueButtonMeetingInfos {
 		this.moderatorCount = moderatorCount;
 	}
 
+	public boolean isRecording() {
+		return recording;
+	}
+
+	public void setRecording(boolean recording) {
+		this.recording = recording;
+	}
+
+	public boolean isBreakout() {
+		return breakout;
+	}
+
+	public void setBreakout(boolean breakout) {
+		this.breakout = breakout;
+	}
+
+	public long getMaxUsers() {
+		return maxUsers;
+	}
+
+	public void setMaxUsers(long maxUsers) {
+		this.maxUsers = maxUsers;
+	}
+
+	public double getLoad() {
+		return load;
+	}
+
+	public void setLoad(double load) {
+		this.load = load;
+	}
+
 	@Override
 	public int hashCode() {
 		return meetingId == null ? 127846 : meetingId.hashCode();
diff --git a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonServerInfos.java b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonServerInfos.java
new file mode 100644
index 00000000000..d0162ffd385
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonServerInfos.java
@@ -0,0 +1,107 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.bigbluebutton.model;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+
+/**
+ * 
+ * Initial date: 8 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BigBlueButtonServerInfos {
+	
+	private double load;
+	private final BigBlueButtonServer server;
+	private final List<BigBlueButtonMeetingInfos> meetingsInfos;
+	
+	public BigBlueButtonServerInfos(BigBlueButtonServer server, List<BigBlueButtonMeetingInfos> meetingsInfos, double load) {
+		this.load = load;
+		this.server = server;
+		this.meetingsInfos = meetingsInfos;
+	}
+	
+	public double getLoad() {
+		return load;
+	}
+	
+	public BigBlueButtonServer getServer() {
+		return server;
+	}
+	
+	public List<BigBlueButtonMeetingInfos> getMeetingsInfos() {
+		return meetingsInfos;
+	}
+	
+	public Long getModeratorCount() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.collect(Collectors.summingLong(BigBlueButtonMeetingInfos::getModeratorCount));
+	}
+	
+	public Long getParticipantCount() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.collect(Collectors.summingLong(BigBlueButtonMeetingInfos::getParticipantCount));
+	}
+	
+	public Long getListenerCount() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.collect(Collectors.summingLong(BigBlueButtonMeetingInfos::getListenerCount));
+	}
+	
+	public Long getVoiceParticipantCount() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.collect(Collectors.summingLong(BigBlueButtonMeetingInfos::getVoiceParticipantCount));
+	}
+	
+	public Long getVideoCount() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.collect(Collectors.summingLong(BigBlueButtonMeetingInfos::getVideoCount));
+	}
+	
+	public Long getMaxUsers() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.collect(Collectors.summingLong(BigBlueButtonMeetingInfos::getMaxUsers));
+	}
+	
+	public long getRecordingMeetings() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.filter(BigBlueButtonMeetingInfos::isRecording)
+			.count();
+	}
+	
+	public long getBreakoutRecordingMeetings() {
+		return meetingsInfos.stream()
+			.filter(BigBlueButtonMeetingInfos::isRunning)
+			.filter(BigBlueButtonMeetingInfos::isRecording)
+			.filter(BigBlueButtonMeetingInfos::isBreakout)
+			.count();
+	}
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java
index c3dd16119d4..4ac6053ca0e 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminController.java
@@ -47,7 +47,7 @@ import org.olat.core.util.resource.OresHelper;
 public class BigBlueButtonAdminController extends BasicController implements Activateable2 {
 	
 	private Link configurationLink;
-	//private final Link serversLink;
+	private final Link serversLink;
 	private final Link meetingsLink;
 	private final Link templatesLink;
 	private final Link calendarLink;
@@ -55,7 +55,8 @@ public class BigBlueButtonAdminController extends BasicController implements Act
 	private final VelocityContainer mainVC;
 	
 	private final boolean configurationReadOnly;
-	
+
+	private BigBlueButtonAdminServersController serversCtrl;
 	private BigBlueButtonConfigurationController configCtrl;
 	private BigBlueButtonAdminMeetingsController meetingsCtrl;
 	private BigBlueButtonAdminTemplatesController templatesCtrl;
@@ -74,8 +75,8 @@ public class BigBlueButtonAdminController extends BasicController implements Act
 			configurationLink = LinkFactory.createLink("account.configuration", mainVC, this);
 			segmentView.addSegment(configurationLink, true);
 		}
-		//serversLink = LinkFactory.createLink("servers.title", mainVC, this);
-		//segmentView.addSegment(serversLink, false);
+		serversLink = LinkFactory.createLink("servers.title", mainVC, this);
+		segmentView.addSegment(serversLink, false);
 		templatesLink = LinkFactory.createLink("templates.title", mainVC, this);
 		segmentView.addSegment(templatesLink, false);
 		meetingsLink = LinkFactory.createLink("meetings.title", mainVC, this);
@@ -115,6 +116,9 @@ public class BigBlueButtonAdminController extends BasicController implements Act
 		} else if("Calendar".equalsIgnoreCase(type)) {
 			doOpenCalendar(ureq);
 			segmentView.select(calendarLink);
+		} else if("Servers".equalsIgnoreCase(type)) {
+			doOpenServers(ureq);
+			segmentView.select(serversLink);
 		}
 	}
 
@@ -127,12 +131,14 @@ public class BigBlueButtonAdminController extends BasicController implements Act
 				Component clickedLink = mainVC.getComponent(segmentCName);
 				if (clickedLink == configurationLink) {
 					doOpenConfiguration(ureq);
-				} else if (clickedLink == templatesLink){
+				} else if (clickedLink == templatesLink) {
 					doOpenTemplates(ureq);
-				} else if (clickedLink == meetingsLink){
+				} else if (clickedLink == meetingsLink) {
 					doOpenMeetings(ureq);
-				} else if (clickedLink == calendarLink){
+				} else if (clickedLink == calendarLink) {
 					doOpenCalendar(ureq);
+				} else if (clickedLink == serversLink) {
+					doOpenServers(ureq);
 				}
 			}
 		}
@@ -182,4 +188,14 @@ public class BigBlueButtonAdminController extends BasicController implements Act
 		mainVC.put("segmentCmp", calendarsCtrl.getInitialComponent());
 	}
 	
+	private void doOpenServers(UserRequest ureq) {
+		if(serversCtrl == null) {
+			WindowControl bwControl = addToHistory(ureq, OresHelper.createOLATResourceableInstance("Servers", 0l), null);
+			serversCtrl = new BigBlueButtonAdminServersController(ureq, bwControl);
+			listenTo(serversCtrl);
+		} else {
+			addToHistory(ureq, serversCtrl);
+		}
+		mainVC.put("segmentCmp", serversCtrl.getInitialComponent());
+	}
 }
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersController.java
index b9d9fa5ccf5..62285767ad8 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersController.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersController.java
@@ -19,11 +19,24 @@
  */
 package org.olat.modules.bigbluebutton.ui;
 
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.modules.bigbluebutton.BigBlueButtonManager;
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonServerInfos;
+import org.olat.modules.bigbluebutton.ui.BigBlueButtonAdminServersTableModel.ServersCols;
+import org.springframework.beans.factory.annotation.Autowired;
 
 /**
  * 
@@ -33,21 +46,59 @@ import org.olat.core.gui.control.WindowControl;
  */
 public class BigBlueButtonAdminServersController extends FormBasicController {
 	
+	private FlexiTableElement serversTableEl;
+	private BigBlueButtonAdminServersTableModel serversTableModel;
+	
+	@Autowired
+	private BigBlueButtonManager bigBlueButtonManager;
+	
 	public BigBlueButtonAdminServersController(UserRequest ureq, WindowControl wControl) {
-		super(ureq, wControl);
+		super(ureq, wControl, "servers_admin");
 		
 		initForm(ureq);
+		loadModel();
 	}
 
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
-		//
+		
+		FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.enabled));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.url));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.capacityFactor, new CapacityFactorCellRenderer()));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.moderatorCount));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.participantCount));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.listenerCount));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.voiceParticipantCount));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.videoCount));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.maxUsers));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.recordingMeetings));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.breakoutRecordingMeetings));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(ServersCols.load, new CapacityFactorCellRenderer()));
+		
+		serversTableModel = new BigBlueButtonAdminServersTableModel(columnsModel, getLocale());
+		
+		serversTableEl = uifactory.addTableElement(getWindowControl(), "servers", serversTableModel, 10, false, getTranslator(), formLayout);
+		serversTableEl.setCustomizeColumns(true);
+		serversTableEl.setEmtpyTableMessageKey("bigbluebutton.servers.empty");
 	}
 
 	@Override
 	protected void doDispose() {
 		//
 	}
+	
+	private void loadModel() {
+		List<BigBlueButtonServer> servers = bigBlueButtonManager.getServers();
+		List<BigBlueButtonServerInfos> serversInfos = bigBlueButtonManager.getServersInfos();
+		Map<BigBlueButtonServer, BigBlueButtonServerInfos> serversToInfos = serversInfos.stream()
+				.collect(Collectors.toMap(BigBlueButtonServerInfos::getServer, infos -> infos, (u, v) -> u));
+		List<BigBlueButtonServerRow> rows = servers.stream()
+				.map(server -> new BigBlueButtonServerRow(server, serversToInfos.get(server)))
+				.collect(Collectors.toList());
+		serversTableModel.setObjects(rows);
+		serversTableEl.reset(true, true, true);
+	}
 
 	@Override
 	protected void formOK(UserRequest ureq) {
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersTableModel.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersTableModel.java
index 39c4538ad4e..3abcef753e2 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersTableModel.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonAdminServersTableModel.java
@@ -23,9 +23,9 @@ import java.util.Locale;
 
 import org.olat.core.commons.persistence.SortKey;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiSortableColumnDef;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
 import org.olat.core.gui.components.form.flexible.impl.elements.table.SortableFlexiTableDataModel;
-import org.olat.modules.bigbluebutton.BigBlueButtonServer;
 
 /**
  * 
@@ -33,8 +33,10 @@ import org.olat.modules.bigbluebutton.BigBlueButtonServer;
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-public class BigBlueButtonAdminServersTableModel extends DefaultFlexiTableDataModel<BigBlueButtonServer>
-implements SortableFlexiTableDataModel<BigBlueButtonServer> {
+public class BigBlueButtonAdminServersTableModel extends DefaultFlexiTableDataModel<BigBlueButtonServerRow>
+implements SortableFlexiTableDataModel<BigBlueButtonServerRow> {
+	
+	private static final ServersCols[] COLS = ServersCols.values();
 
 	private final Locale locale;
 	
@@ -50,20 +52,69 @@ implements SortableFlexiTableDataModel<BigBlueButtonServer> {
 
 	@Override
 	public Object getValueAt(int row, int col) {
-		// TODO Auto-generated method stub
-		return null;
+		BigBlueButtonServerRow server = getObject(row);
+		return getValueAt(server, col);
 	}
 
 	@Override
-	public Object getValueAt(BigBlueButtonServer row, int col) {
-		return null;
+	public Object getValueAt(BigBlueButtonServerRow row, int col) {
+		switch(COLS[col]) {
+			case url: return row.getUrl();
+			case enabled: return row.isEnabled();
+			case capacityFactor: return row.getCapacityFactor();
+			case moderatorCount: return row.getModeratorCount();
+			case participantCount: return row.getParticipantCount();
+			case listenerCount: return row.getListenerCount();
+			case voiceParticipantCount: return row.getVoiceParticipantCount();
+			case videoCount: return row.getVideoCount();
+			case maxUsers: return row.getMaxUsers();
+			case recordingMeetings: return row.getRecordingMeetings();
+			case breakoutRecordingMeetings: return row.getBreakoutRecordingMeetings();
+			case load: return row.getLoad();
+			default: return "ERROR";
+		}
 	}
 
 	@Override
-	public DefaultFlexiTableDataModel<BigBlueButtonServer> createCopyWithEmptyList() {
-		return null;
+	public BigBlueButtonAdminServersTableModel createCopyWithEmptyList() {
+		return new BigBlueButtonAdminServersTableModel(getTableColumnModel(), locale);
 	}
 	
-	
+	public enum ServersCols implements FlexiSortableColumnDef {
+		
+		url("table.header.server.url"),
+		enabled("table.header.server.enabled"),
+		capacityFactor("table.header.capacity.factor"),
+		moderatorCount("table.header.moderator.count"),
+		participantCount("table.header.participant.count"),
+		listenerCount("table.header.listener.count"),
+		voiceParticipantCount("table.header.voice.participant.count"),
+		videoCount("table.header.video.count"),
+		maxUsers("table.header.max.users"),
+		recordingMeetings("table.header.recording.meetings"),
+		breakoutRecordingMeetings("table.header.breakout.recording.meetings"),
+		load("table.header.load");
+		
+		private final String i18nHeaderKey;
+		
+		private ServersCols(String i18nHeaderKey) {
+			this.i18nHeaderKey = i18nHeaderKey;
+		}
+
+		@Override
+		public boolean sortable() {
+			return true;
+		}
+
+		@Override
+		public String sortKey() {
+			return name();
+		}
+
+		@Override
+		public String i18nHeaderKey() {
+			return i18nHeaderKey;
+		}
+	}
 
 }
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonServerRow.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonServerRow.java
new file mode 100644
index 00000000000..e2f3cf0517e
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonServerRow.java
@@ -0,0 +1,89 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.bigbluebutton.ui;
+
+import org.olat.modules.bigbluebutton.BigBlueButtonServer;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonServerInfos;
+
+/**
+ * 
+ * Initial date: 8 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BigBlueButtonServerRow {
+	
+	private final BigBlueButtonServer server;
+	private final BigBlueButtonServerInfos serverInfos;
+	
+	public BigBlueButtonServerRow(BigBlueButtonServer server, BigBlueButtonServerInfos serverInfos) {
+		this.server = server;
+		this.serverInfos = serverInfos;
+	}
+	
+	public String getUrl() {
+		return server.getUrl();
+	}
+	
+	public boolean isEnabled() {
+		return server.isEnabled();
+	}
+	
+	public Double getCapacityFactor() {
+		return server.getCapacityFactory();
+	}
+	
+	public double getLoad() {
+		return serverInfos.getLoad();
+	}
+	
+	public Long getModeratorCount() {
+		return serverInfos.getModeratorCount();
+	}
+	
+	public Long getParticipantCount() {
+		return serverInfos.getParticipantCount();
+	}
+
+	public Long getListenerCount() {
+		return serverInfos.getListenerCount();
+	}
+	
+	public Long getVoiceParticipantCount() {
+		return serverInfos.getVoiceParticipantCount();
+	}
+	
+	public Long getVideoCount() {
+		return serverInfos.getVideoCount();
+	}
+	
+	public Long getMaxUsers() {
+		return serverInfos.getMaxUsers();
+	}
+	
+	public Long getRecordingMeetings() {
+		return serverInfos.getRecordingMeetings();
+	}
+	
+	public Long getBreakoutRecordingMeetings() {
+		return serverInfos.getBreakoutRecordingMeetings();
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/CapacityFactorCellRenderer.java b/src/main/java/org/olat/modules/bigbluebutton/ui/CapacityFactorCellRenderer.java
new file mode 100644
index 00000000000..ac392ff2caf
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/CapacityFactorCellRenderer.java
@@ -0,0 +1,51 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.bigbluebutton.ui;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Locale;
+
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiCellRenderer;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableComponent;
+import org.olat.core.gui.render.Renderer;
+import org.olat.core.gui.render.StringOutput;
+import org.olat.core.gui.render.URLBuilder;
+import org.olat.core.gui.translator.Translator;
+
+/**
+ * 
+ * Initial date: 8 avr. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CapacityFactorCellRenderer implements FlexiCellRenderer {
+	
+	private final DecimalFormat doubleFormat = new DecimalFormat("#0.#", new DecimalFormatSymbols(Locale.ENGLISH));
+
+	@Override
+	public void render(Renderer renderer, StringOutput target, Object cellValue, int row, FlexiTableComponent source,
+			URLBuilder ubu, Translator translator) {
+		if(cellValue instanceof Double) {
+			Double val = (Double)cellValue;
+			target.append(doubleFormat.format(val.doubleValue()));
+		}
+	}
+}
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_content/servers_admin.html b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/servers_admin.html
new file mode 100644
index 00000000000..abaade57c0a
--- /dev/null
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/servers_admin.html
@@ -0,0 +1 @@
+$r.render("servers")
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties
index 6e2fa664a0c..7049cc84d66 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties
@@ -1,4 +1,4 @@
-#Thu Mar 19 21:29:32 CET 2020
+#Wed Apr 08 20:40:23 CEST 2020
 account.configuration=Konfiguration
 add.daily.meeting=T\u00E4glich wiederkehrende Online-Termine hinzuf\u00FCgen
 add.meeting=Online-Termin hinzuf\u00FCgen
@@ -9,7 +9,7 @@ add.template=Raumvorlage erstellen
 add.weekly.meeting=W\u00F6chentlich wiederkehrende Online-Termine hinzuf\u00FCgen
 admin.menu.title=BigBlueButton
 admin.menu.title.alt=BigBlueButton Web Conferencing
-bigbluebutton.intro=BigBlueButton ist ein Open Source Web Conferencing System speziell f\u00fcr eLearning. Weitere Informationen zu BigBlueButton findet sich auf der <a href='https://bigbluebutton.org' target='_blank'>BigBlueButton Webseite</a> und dem <a href='https://github.com/bigbluebutton' target='_blank'>BigBlueButton GitHub Repository</a>
+bigbluebutton.intro=BigBlueButton ist ein Open Source Web Conferencing System speziell f\u00FCr eLearning. Weitere Informationen zu BigBlueButton findet sich auf der <a href\='https\://bigbluebutton.org' target\='_blank'>BigBlueButton Webseite</a> und dem <a href\='https\://github.com/bigbluebutton' target\='_blank'>BigBlueButton GitHub Repository</a>
 bigbluebutton.module.enabled=Modul "BigBlueButton"
 bigbluebutton.module.enabled.for=Aktivieren f\u00FCr
 bigbluebutton.module.enabled.for.courses=Kurse
@@ -32,12 +32,12 @@ connection.failed=BigBlueButton API Login fehlgeschlagen.
 connection.successful=BigBlueButton API Login erfolgreich\!
 edit.server=Server "{0}" bearbeiten
 edit.template=Raumvorlage "{0}" bearbeiten
-enable.permanent.meeting=Online-Termine ohne Datum
 enable.adhoc.meeting=Adhoc Online-Termin
+enable.permanent.meeting=Online-Termine ohne Datum
 error.capacity.factory=Ein Nummer zwischen 1.0 und 100.0
-error.connectionValidationFailed=Die Verbindungspr\u00fcfung ist fehlgeschlagen: <pre>{0}</pre>
+error.connectionValidationFailed=Die Verbindungspr\u00FCfung ist fehlgeschlagen\: <pre>{0}</pre>
 error.date.in.past=Der Termin kann sich nicht in der Vergangenheit befinden.
-error.duration=Termindauer \u00fcberschritten. Maximal Dauer: {0} Minuten.
+error.duration=Termindauer \u00FCberschritten. Maximal Dauer\: {0} Minuten.
 error.end.past=Der Online-Termin kann nicht in Vergangenheit geplant werden.
 error.first.date.in.past=Der erste Termin kann sich nicht in der Vergangenheit befinden.
 error.prefix=Ein Fehler ist aufgetreten\:
@@ -46,11 +46,11 @@ error.server.exists=Ein Server mit diesem URL existiert schon.
 error.server.raw={1} <small>Schl\u00FCssel\: {0}</small>
 error.start.after.end=Das Enddatum darf nicht vor dem Beginndatum sein.
 error.too.long.time=Zeit ist zu lang. Es sind maximal {0} Minuten erlaubt.
-error.url.invalid=Ung\u00fcltige Serveradresse
-meeting.create.intro=Der Online-Termin wurde vom Betreuer noch nicht er\u00F6ffnet. Teilnehmer k\u00F6nnen den Raum f\u00fcr noch nicht betreten.
+error.url.invalid=Ung\u00FCltige Serveradresse
+meeting.create.intro=Der Online-Termin wurde vom Betreuer noch nicht er\u00F6ffnet. Teilnehmer k\u00F6nnen den Raum f\u00FCr noch nicht betreten.
 meeting.day=Datum des Meetings
-meeting.description=Beschreibung
 meeting.deleted=Das Meeting wurde erfolgreich gel\u00F6scht.
+meeting.description=Beschreibung
 meeting.end=Ende
 meeting.ended=Der Online-Termin wurde bereits beendet.
 meeting.followupTime=Nachlaufzeit (Min.)
@@ -73,45 +73,57 @@ meetings.title=Online-Termine
 meetings.upcoming=Aktuelle und zuk\u00FCnftige Online-Termine
 minutes={0} Min.
 no.meeting.configured=Zur Zeit sind keine Online-Termine vorhanden.
-no.template.configured=Es sind keine aktiven Raumvorlagen vorhanden.
 no.recordings=Es ist zur Zeit noch keine Aufzeichnung f\u00FCr diesen Online-Termin vorhanden.
+no.template.configured=Es sind keine aktiven Raumvorlagen vorhanden.
 no.upcoming.meetings=Zur Zeit sind keine aktuellen oder zuk\u00FCnftigen Online-Termine vorhanden.
 option.baseurl=BigBlueButton API URL 
 option.baseurl.example=https\://bigbluebutton.openolat.com/bigbluebutton/
 option.bigbluebutton.secret=Secret
 option.bigbluebutton.shared.secret=Shared secret
-option.capacity.factory=Capacity factor
 option.capacity.factor.example=Ein Nummer zwischen 1 und 100 (zum Beispiel 1.23)
+option.capacity.factory=Capacity factor
 option.enabled.server=Server aktivieren
 option.recordingurl=Aufzeichnung URL
-recordings=Aufzeichnungen
 recording.browser.infos=Aufzeichnungen k\u00F6nnen nur mit Google Chrome und Firefox gesehen werden.
 recording.type.podcast=Podcast
 recording.type.presentation=Pr\u00E4sentation
+recordings=Aufzeichnungen
 role.administrator=Administrator
 role.author=Autor
 role.coach=Betreuer
-role.owner=Kursbesitzer
 role.group=Gruppenmitglied
+role.owner=Kursbesitzer
 server.overloaded=F\u00FCr das gew\u00E4hlten Datum/Uhrzeit ist kein Raum verf\u00FCgbar. W\u00E4hlen Sie ein anderes Datum/Uhrzeit oder eine andere Raumvorlage.
 servers.title=Server
-table.header.day.week=Tag
 table.header.available=Verf\u00FCgbarkeit
+table.header.breakout.meetings=\# Breakout
+table.header.breakout.recording.meetings=\# Breakout Recording
+table.header.capacity.factor=Kapazit\u00E4t
+table.header.day.week=Tag
 table.header.enabled=Aktiv
-table.header.permanent=Ohne Datum
-table.header.system=System
+table.header.listener.count=\# Listeners
+table.header.load=Load
 table.header.max.concurrent.meetings=R\u00E4ume
 table.header.max.duration=Dauer
 table.header.max.participants=Teilnehmer
+table.header.max.users=Max. Benutzer
+table.header.moderator.count=\# Moderatoren
+table.header.number.meetings=\# Meetings
+table.header.participant.count=\# Teilnehmer
+table.header.permanent=Ohne Datum
+table.header.recording.end=Ende
+table.header.recording.meetings=\# Recordings
 table.header.recording.name=Name
-table.header.recording.type=Typ
 table.header.recording.open=\u00D6ffnen
 table.header.recording.start=Beginn
-table.header.recording.end=Ende
+table.header.recording.type=Typ
 table.header.server.enabled=Eingeschaltet
 table.header.server.recording=Aufzeichnungen URL
 table.header.server.url=URL
+table.header.system=System
 table.header.template=Raumvorlage
+table.header.video.count=\# Video
+table.header.voice.participant.count=\# Voices
 table.header.webcams.only.moderator=Nur Moderatorenkamera
 template.allowModsToUnmuteUsers=Moderatoren d\u00FCrfen Teilnehmer-Mikrofon aktiveren
 template.allowStartStopRecording=Starten und Stoppen von Aufzeichnungen zulassen
@@ -119,21 +131,21 @@ template.autoStartRecording=Aufzeichnung automatisch starten
 template.breakout=Breakout-R\u00E4me zulassen
 template.description=Beschreibung
 template.enabled=Raumvorlage aktivieren
-template.explain.max.participants=Max. Anzahl Teilnehmer: {0} ({1} R\u00E4ume verf\u00FCgbar)
-template.explain.max.participants.with.webcams.mod=Max. Anzahl Teilnehmer: {0}, nur Moderatorenkamera ({1} R\u00E4ume verf\u00FCgbar)
+template.explain.max.participants=Max. Anzahl Teilnehmer\: {0} ({1} R\u00E4ume verf\u00FCgbar)
+template.explain.max.participants.with.webcams.mod=Max. Anzahl Teilnehmer\: {0}, nur Moderatorenkamera ({1} R\u00E4ume verf\u00FCgbar)
 template.lock=F\u00FCr gesperrte Teilnehmer... 
 template.lockSettingsDisableCam=Kamera ausschalten
 template.lockSettingsDisableMic=Mikrofon ausschalten
 template.lockSettingsDisableNote=Gmeinsame Notizen ausschalten
 template.lockSettingsDisablePrivateChat=Privater Chat ausschalten
-template.lockSettingsDisablePublicChat=Öffentlicher Chat ausschalten
-template.lockSettingsLockedLayout=Layoutanpassung ausschalten
+template.lockSettingsDisablePublicChat=\u00D6ffentlicher Chat ausschalten
 template.lockSettingsHideUserList=Teilnehmerliste ausschalten
 template.lockSettingsLockOnJoin=Teilnehmer bei Eintritt automatisch sperren
 template.lockSettingsLockOnJoinConfigurable=Ausschalten der automatischen Sperre durch Moderator verhindern
+template.lockSettingsLockedLayout=Layoutanpassung ausschalten
+template.max.concurrent.meetings=Anzahl R\u00E4ume
 template.maxDuration=Dauer (Minuten)
 template.maxParticipants=Anzahl Teilnehmer
-template.max.concurrent.meetings=Anzahl R\u00E4ume
 template.muteOnStart=Teilnehmer-Mikrofon beim Eintritt deaktivieren
 template.name=Raumname
 template.record=Aufzeichnungen des Online-Termins zulassen
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties
index a7ff2b03d5e..0a56e87a859 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties
@@ -1,14 +1,15 @@
-#Thu Mar 19 21:29:07 CET 2020
+#Wed Apr 08 20:38:54 CEST 2020
 account.configuration=Configuration
 add.daily.meeting=Add daily recurring meeting
 add.meeting=Add online-meeting
 add.permanent.meeting=Add permanent meeting room
+add.server=Add server
 add.single.meeting=Add single meeting
 add.template=New room template
 add.weekly.meeting=Add weekly recurring meeting
 admin.menu.title=BigBlueButton
 admin.menu.title.alt=BigBlueButton Web Conferencing
-bigbluebutton.intro=BigBlueButton is an Open Source Web Conferencing System designed for learning. More information about BigBlueButton can be found on the <a href='https://bigbluebutton.org' target='_blank'>BigBlueButton web page</a> and the <a href='https://github.com/bigbluebutton' target='_blank'>BigBlueButton GitHub repository</a>
+bigbluebutton.intro=BigBlueButton is an Open Source Web Conferencing System designed for learning. More information about BigBlueButton can be found on the <a href\='https\://bigbluebutton.org' target\='_blank'>BigBlueButton web page</a> and the <a href\='https\://github.com/bigbluebutton' target\='_blank'>BigBlueButton GitHub repository</a>
 bigbluebutton.module.enabled=Module "BigBlueButton"
 bigbluebutton.module.enabled.for=Activate for
 bigbluebutton.module.enabled.for.courses=Courses
@@ -31,12 +32,12 @@ connection.failed=BigBlueButton API login failed.
 connection.successful=BigBlueButton API login successful\!
 edit.server=Edit server "{0}"
 edit.template=Edit room-template "{0}"
-enable.permanent.meeting=Online-Meetings without date
 enable.adhoc.meeting=Adhoc online-meeting
+enable.permanent.meeting=Online-Meetings without date
 error.capacity.factory=A number between 1.0 and 100.0
-error.connectionValidationFailed=The connection validation failed: <pre>{0}</pre>
+error.connectionValidationFailed=The connection validation failed\: <pre>{0}</pre>
 error.date.in.past=The meeting date can not be in the past.
-error.duration=Meeting duration is too long. Maximal duration: {0} minutes.
+error.duration=Meeting duration is too long. Maximal duration\: {0} minutes.
 error.end.past=Online-meeting cannot be planned in the past.
 error.first.date.in.past=The first meeting date can not be in the past.
 error.prefix=An error happened\:
@@ -48,8 +49,8 @@ error.too.long.time=Time is too long. It is limited to {0} minutes.
 error.url.invalid=Invalid server URL
 meeting.create.intro=The meeting has not yet been started by the coach. Participants are not able to enter the classroom.
 meeting.day=Date of the meeting
-meeting.description=Description
 meeting.deleted=The meeting was successfully deleted.
+meeting.description=Description
 meeting.end=End date
 meeting.ended=The online-meeting has already ended.
 meeting.followupTime=Follow-up (min.)
@@ -79,37 +80,50 @@ option.baseurl=BigBlueButton API URL
 option.baseurl.example=https\://bigbluebutton.openolat.com/bigbluebutton/
 option.bigbluebutton.secret=Secret
 option.bigbluebutton.shared.secret=Shared secret
+option.capacity.factor.example=A number between 1 and 100 (for example\: 1.23)
 option.capacity.factory=Capacity factor
-option.capacity.factor.example=A number between 1 and 100 (for example: 1.23)
 option.enabled.server=Activate server
 option.recordingurl=Recording URL
-recordings=Recordings
 recording.browser.infos=Recordings can only be viewed with Google Chrome or Firefox.
 recording.type.podcast=Podcast
 recording.type.presentation=Presentation
+recordings=Recordings
 role.administrator=Administrator
 role.author=Author
 role.coach=Coach
-role.owner=Course owner
 role.group=Group user
+role.owner=Course owner
 server.overloaded=There is no room available for the choosen date/time. Choose another date/time or another room template.
 servers.title=Servers
+table.header.available=Availability
+table.header.breakout.meetings=\# Breakout
+table.header.breakout.recording.meetings=\# Breakout Recording
+table.header.capacity.factor=Capacity
 table.header.day.week=Day
 table.header.enabled=Enabled
-table.header.permanent=Withour date
-table.header.system=System
+table.header.listener.count=\# Listeners
+table.header.load=Load
 table.header.max.concurrent.meetings=Rooms
 table.header.max.duration=Duration
 table.header.max.participants=Participants
+table.header.max.users=Max. users
+table.header.moderator.count=\# Moderator
+table.header.number.meetings=\# Meetings
+table.header.participant.count=\# Participants
+table.header.permanent=Withour date
+table.header.recording.end=End
+table.header.recording.meetings=\# Recordings
 table.header.recording.name=Name
-table.header.recording.type=Type
 table.header.recording.open=Open
 table.header.recording.start=Start
-table.header.recording.end=End
+table.header.recording.type=Type
 table.header.server.enabled=Enabled
 table.header.server.recording=Recording URL
 table.header.server.url=URL
+table.header.system=System
 table.header.template=Room-template
+table.header.video.count=\# Video
+table.header.voice.participant.count=\# Voices
 table.header.webcams.only.moderator=Webcams only for moderators
 template.allowModsToUnmuteUsers=Allow moderators to activate the participants microphone
 template.allowStartStopRecording=Allow to start / stop of recordings
@@ -117,21 +131,21 @@ template.autoStartRecording=Automatically start recording
 template.breakout=Allow breakout rooms
 template.description=Description
 template.enabled=Enable room-template
-template.explain.max.participants=Max. number of participants: {0} ({1} rooms available)
-template.explain.max.participants.with.webcams.mod=Max. number of participants: {0}, only moderator webcam ({1} rooms available)
+template.explain.max.participants=Max. number of participants\: {0} ({1} rooms available)
+template.explain.max.participants.with.webcams.mod=Max. number of participants\: {0}, only moderator webcam ({1} rooms available)
 template.lock=For locked participants... 
 template.lockSettingsDisableCam=disable webcam
 template.lockSettingsDisableMic=disable microphone
 template.lockSettingsDisableNote=disable shared-notes
 template.lockSettingsDisablePrivateChat=disable private chat
 template.lockSettingsDisablePublicChat=disable public chat
-template.lockSettingsLockedLayout=disable layout changes
 template.lockSettingsHideUserList=disable participant list
 template.lockSettingsLockOnJoin=Put participants into lock mode on join
 template.lockSettingsLockOnJoinConfigurable=Prevent disabling of the automatic locking by the moderator
+template.lockSettingsLockedLayout=disable layout changes
+template.max.concurrent.meetings=Number of rooms
 template.maxDuration=Duration (minutes)
 template.maxParticipants=Number of participants
-template.max.concurrent.meetings=Number of rooms
 template.muteOnStart=Disable participant microphone on join
 template.name=Room name
 template.record=Allow meeting recording
@@ -141,7 +155,7 @@ templates.title=Room-templates
 undelete=Reactivate
 view=View
 view.template=Room-template "{0}"
+warning.at.least.one.meeting=You must select at least one meeting.
 warning.template.in.use=The room-template cannot be deleted because it is used by online-meetings. Delete the corresponding online-meeting or de-activate the room-template.
 wizard.dates.title=Dates
 wizard.meeting.title=Configuration
-
-- 
GitLab