diff --git a/dbrepo-auth-service/listeners/target/create-event-listener.jar b/dbrepo-auth-service/listeners/target/create-event-listener.jar
index 9fa1288598118b348a3341e591558b66152c43b3..9defdad744bc4cb391b2ea104502106d0011f6af 100644
Binary files a/dbrepo-auth-service/listeners/target/create-event-listener.jar and b/dbrepo-auth-service/listeners/target/create-event-listener.jar differ
diff --git a/dbrepo-dashboard-service/app.py b/dbrepo-dashboard-service/app.py
index 94f838e2b4ea6a8da071c2e7f18c2a2805d28a39..fe5d8218b009503afad67c45d8d7d69df7616322 100644
--- a/dbrepo-dashboard-service/app.py
+++ b/dbrepo-dashboard-service/app.py
@@ -216,7 +216,7 @@ def create_dashboard():
     logging.debug(
         f"endpoint create dashboard, is_public={is_public}, is_schema_public={is_schema_public}, owner_username={owner_username}")
     try:
-        db = dashboard.create(request.json['database_name'], request.json['owner_username'])
+        db = dashboard.create(request.json['database_name'])
         access.update_anonymous_read_access(db['uid'], is_public, is_schema_public)
         return Response(dumps(db)), 201, headers
     except GrafanaClientError as e:
diff --git a/dbrepo-dashboard-service/dashboard.py b/dbrepo-dashboard-service/dashboard.py
index a170e63dbb445dc5f7ef943fb7ed9f424c10bf1b..8ff4b47c49ba74f3ee27efb478767254f01fdba5 100644
--- a/dbrepo-dashboard-service/dashboard.py
+++ b/dbrepo-dashboard-service/dashboard.py
@@ -1,37 +1,30 @@
 import logging
 import os
 
-from dbrepo.api.dto import Database, View
+from dbrepo.api.dto import Database
+from dbrepo.api.exceptions import MalformedError
+from grafana_client.client import GrafanaClientError
 
+from api.exceptions import DashboardNotFound
 from clients.grafana_client import GrafanaClient
-
-statistics_row_title = '${view_id}'
+from panel import get_panels
 
 base_url = os.getenv('BASE_URL', 'http://localhost')
 datasource_uid = os.getenv('JSON_DATASOURCE_NAME', 'dbrepojson0')
 
 
-def map_link(title: str, url: str) -> dict:
+def map_link(title: str, url: str, icon: str = 'info') -> dict:
     return dict(targetBlank=True,
                 asDropdown=False,
                 includeVars=False,
                 keepTime=False,
                 tags=[],
                 type='link',
-                icon='info',
+                icon=icon,
                 title=title,
                 url=url)
 
 
-def map_statistics_row(dashboard: dict) -> dict | None:
-    filtered_panels = [panel for panel in dashboard['panels'] if
-                       panel['type'] == 'row' and panel['title'] == statistics_row_title]
-    if len(filtered_panels) == 0:
-        logging.warning(f"Failed to find statistics row title {statistics_row_title} in: {filtered_panels}")
-        return None
-    return filtered_panels[0]
-
-
 def map_links(database: Database) -> [dict]:
     links = []
     if len(database.identifiers) > 0:
@@ -41,275 +34,30 @@ def map_links(database: Database) -> [dict]:
     return links
 
 
-def map_templating(database: Database) -> dict:
-    options = [dict(selected=False,
-                    text=view.name,
-                    value=str(view.id)) for view in database.views]
-    selected = dict(selected=True,
-                    text=[view.name for view in database.views],
-                    value=[str(view.id) for view in database.views])
-    datasource = dict(uid=datasource_uid,
-                      type='yesoreyeram-infinity-datasource')
-    return dict(list=[dict(description='',
-                           name='view_id',
-                           hide=0,
-                           includeAll=True,
-                           multi=True,
-                           datasource=datasource,
-                           refresh=1,
-                           regex='',
-                           sort=0,
-                           definition='dbrepo-json- (infinity) json',
-                           query=dict(queryType='infinity',
-                                      query='',
-                                      infinityQuery=dict(format='table',
-                                                         filters=[],
-                                                         parser='backend',
-                                                         refId='variable',
-                                                         root_selector='',
-                                                         source='url',
-                                                         type='json',
-                                                         url=f"/api/database/{database.id}/view",
-                                                         columns=[dict(selector='id',
-                                                                       text='value',
-                                                                       type='string'),
-                                                                  dict(
-                                                                      selector='internal_name',
-                                                                      text='name',
-                                                                      type='string')],
-                                                         url_options=dict(data='',
-                                                                          method='GET'))),
-                           label='Datasource',
-                           skipUrlSync=False,
-                           type='query',
-                           current=selected,
-                           options=options)])
-
-
-def map_timeseries_panel(database: Database, view: View) -> dict:
-    datasource = dict(uid=datasource_uid,
-                      type='yesoreyeram-infinity-datasource')
-    return dict(
-        title=view['name'],
-        type='timeseries',
-        datasource=datasource,
-        targets=[dict(datasource=datasource,
-                      format='table',
-                      global_query_id='',
-                      hide=False,
-                      refId='A',
-                      root_selector='',
-                      source='url',
-                      type='json',
-                      url=f"/api/database/{database['id']}/view/{view['id']}",
-                      url_options=dict(data='',
-                                       method='GET'))],
-        gridPos=dict(h=8,
-                     w=12,
-                     x=0,
-                     y=0),
-        options=dict(legend=dict(displayMode='list',
-                                 placement='bottom',
-                                 showLegend=True),
-                     tooltip=dict(mode='single',
-                                  sort='none')),
-        fieldConfig=dict(
-            defaults=dict(color=dict(mode='palette-classic'),
-                          custom=dict(
-                              axisBorderShow=False,
-                              axisCenteredZero=False,
-                              axisColorMode='text',
-                              axisLabel='',
-                              axisPlacement='auto',
-                              barAlignment=0,
-                              drawStyle='line',
-                              fillOpacity=0,
-                              gradientMode='none',
-                              hideFrom=dict(legend=False,
-                                            tooltip=False,
-                                            viz=False),
-                              insertNulls=False,
-                              lineInterpolation='linear',
-                              lineWidth=1,
-                              pointSize=5,
-                              scaleDistribution=dict(type='linear'),
-                              showPoints='auto',
-                              spanNulls=False,
-                              stacking=dict(group='A',
-                                            mode='none'),
-                              thresholdsStyle=dict(mode='absolute')))))
-
-
-def map_statistics_panel(database_id: str, view: View) -> dict:
-    datasource = dict(uid=datasource_uid,
-                      type='yesoreyeram-infinity-datasource')
-    return dict(
-        title=view.name,
-        type='table',
-        datasource=datasource,
-        targets=[dict(datasource=datasource,
-                      columns=[],
-                      filters=[],
-                      format='table',
-                      global_query_id='',
-                      hide=False,
-                      refId='A',
-                      root_selector='',
-                      source='url',
-                      type='json',
-                      url=f"/api/database/{database_id}/view/{view.id}/data",
-                      url_options=dict(data='',
-                                       method='GET'))],
-        options=dict(cellHeight="sm",
-                     showHeader=True,
-                     footer=dict(countRows=False,
-                                 fields="",
-                                 reducer=["sum"],
-                                 show=False)),
-        gridPos=dict(h=8,
-                     w=12,
-                     x=12,
-                     y=0),
-        transformations=dict(id="organize",
-                             options=dict(excludeByName=dict(),
-                                          includeByName=dict(),
-                                          indexByName=dict(
-                                              HEADER_AVG=3,
-                                              HEADER_COL=0,
-                                              HEADER_STDDEV=4,
-                                              HEADER_MAX=2,
-                                              HEADER_MIN=1))),
-        fieldConfig=dict(defaults=dict(custom=dict(align="auto",
-                                                   filterable="true",
-                                                   cellOptions=dict(type="auto"),
-                                                   inspect=False),
-                                       mappings=[],
-                                       thresholds=dict(mode="absolute",
-                                                       steps=[dict(color="green",
-                                                                   value=None),
-                                                              dict(color="red",
-                                                                   value=80)
-                                                              ])),
-                         overrides=[dict(matcher=dict(id="byName",
-                                                      options="HEADER_COL"),
-                                         properties=[dict(id="custom.align",
-                                                          value="center")]),
-                                    dict(matcher=dict(id="byName",
-                                                      options="HEADER_MIN"),
-                                         properties=[dict(id="custom.width",
-                                                          value=115)]),
-                                    dict(matcher=dict(id="byName",
-                                                      options="HEADER_MAX"),
-                                         properties=[dict(id="custom.width",
-                                                          value=115)]),
-                                    dict(matcher=dict(id="byName",
-                                                      options="HEADER_AVG"),
-                                         properties=[dict(id="custom.width",
-                                                          value=115)]),
-                                    dict(matcher=dict(id="byName",
-                                                      options="HEADER_STDDEV"),
-                                         properties=[dict(id="custom.width",
-                                                          value=115)])
-                                    ]))
-
-
-def map_overview_panel(database_id: str) -> dict:
-    datasource = dict(uid=datasource_uid,
-                      type='yesoreyeram-infinity-datasource')
-    return dict(title='Preview',
-                type='table',
-                fieldConfig=dict(
-                    defaults=dict(
-                        color=dict(mode='palette-classic'),
-                        custom=dict(axisBorderShow=False,
-                                    axisCenteredZero=False,
-                                    axisColorMode='text',
-                                    axisLabel='',
-                                    axisPlacement='auto',
-                                    barAlignment=0,
-                                    drawStyle='line',
-                                    fillOpacity=0,
-                                    gradientMode='none',
-                                    hideFrom=dict(
-                                        legend=False,
-                                        tooltip=False,
-                                        viz=False),
-                                    insertNulls=False,
-                                    lineInterpolation='linear',
-                                    lineWidth=1,
-                                    pointSize=5,
-                                    scaleDistribution=dict(
-                                        type='linear'),
-                                    showPoints='auto',
-                                    spanNulls=False,
-                                    stacking=dict(group='A',
-                                                  mode='none'),
-                                    thresholdsStyle=dict(
-                                        mode='off'))),
-                    overrides=[]),
-                options=dict(legend=dict(displayMode='list',
-                                         placement='bottom',
-                                         showLegend=True,
-                                         calcs=[]),
-                             tooltip=dict(mode='single',
-                                          sort='none')),
-                targets=[dict(format='json',
-                              columns=[],
-                              datasource=datasource,
-                              filters=[],
-                              global_query_id='',
-                              refId='A',
-                              root_selector='',
-                              source='url',
-                              type='json',
-                              url='/api/database/' + database_id + '/view/${view_id}/data',
-                              url_options=dict(data='',
-                                               method='GET'))],
-                datasource=datasource,
-                gridPos=dict(h=4,
-                             w=12,
-                             x=0,
-                             y=0))
-
-
-def map_row() -> dict:
-    datasource = dict(uid=datasource_uid,
-                      type='yesoreyeram-infinity-datasource')
-    return dict(collapsed=False,
-                repeat='view_id',
-                repeatDirection='h',
-                title=statistics_row_title,
-                type='row',
-                panels=[],
-                targets=[dict(refId='A',
-                              datasource=datasource)],
-                gridPos=dict(h=1,
-                             w=24,
-                             x=0,
-                             y=0))
-
-
-def map_panels(dashboard: dict, database: Database) -> [dict]:
-    if map_statistics_row(dashboard) is None:
-        dashboard['panels'].append(map_row())
-        dashboard['panels'].append(map_overview_panel(database.id))
-        for view in database.views:
-            dashboard['panels'].append(map_statistics_panel(database.id, view))
-    return dashboard['panels']
-
-
 def find(uid: str):
+    """
+    Finds a dashboard with the given uid.
+
+    @return The dashboard, if successful. Otherwise, `None`.
+    """
+    if uid is None:
+        return None
     grafana = GrafanaClient().connect()
-    return grafana.dashboard.get_dashboard(uid)
+    try:
+        return grafana.dashboard.get_dashboard(uid)
+    except GrafanaClientError:
+        logging.warning(f"Failed to find dashboard with uid: {uid}")
+        return None
 
 
 def create(database_name: str, uid: str = '') -> dict:
     grafana = GrafanaClient().connect()
     dashboard = dict(uid=uid,
                      title=f'{database_name} Overview',
-                     tags=['dbrepo'],
+                     tags=['managed'],
                      timezone='browser',
-                     fiscalYearStartMonth=1,
+                     refresh='30m',
+                     preload=False,
                      panels=[])
     dashboard['panels'] = []
     payload = dict(folderUid='',
@@ -327,16 +75,20 @@ def delete(uid: str) -> None:
 
 def update(database: Database) -> None:
     grafana = GrafanaClient().connect()
-    dashboard = find(database.dashboard_uid)['dashboard']
+    dashboard = find(database.dashboard_uid)
+    if dashboard is None:
+        raise DashboardNotFound(f'Dashboard {database.dashboard_uid} not found')
+    dashboard = dashboard['dashboard']
     # update metadata
+    if not database.is_dashboard_enabled and 'managed' in dashboard['tags']:
+        dashboard['tags'].remove('managed')
     if len(database.identifiers) > 0 and len(database.identifiers[0].titles) > 0:
         dashboard['title'] = database.identifiers[0].titles[0].title
     if len(database.identifiers) > 0 and len(database.identifiers[0].descriptions) > 0:
         dashboard['description'] = database.identifiers[0].descriptions[0].description
     dashboard['links'] = map_links(database)
-    dashboard['templating'] = map_templating(database)
     # update panels
-    dashboard['panels'] = map_panels(dashboard, database)
+    dashboard['panels'] = get_panels(dashboard, database)
     payload = dict(folderUid='',
                    overwrite=True,
                    dashboard=dashboard)
diff --git a/dbrepo-dashboard-service/init/dashboard.py b/dbrepo-dashboard-service/init/dashboard.py
index a7c03894e4a2194f23800d4284323254c233a93e..8ff4b47c49ba74f3ee27efb478767254f01fdba5 100644
--- a/dbrepo-dashboard-service/init/dashboard.py
+++ b/dbrepo-dashboard-service/init/dashboard.py
@@ -50,7 +50,7 @@ def find(uid: str):
         return None
 
 
-def create(database_name: str, uid: str = '') -> str:
+def create(database_name: str, uid: str = '') -> dict:
     grafana = GrafanaClient().connect()
     dashboard = dict(uid=uid,
                      title=f'{database_name} Overview',
@@ -65,7 +65,7 @@ def create(database_name: str, uid: str = '') -> str:
                    dashboard=dashboard)
     dashboard = grafana.dashboard.update_dashboard(payload)
     logging.info(f"Created dashboard with uid: {dashboard['uid']}")
-    return dashboard['uid']
+    return dashboard
 
 
 def delete(uid: str) -> None:
diff --git a/dbrepo-dashboard-service/init/tests/test_integration_dashboard.py b/dbrepo-dashboard-service/init/tests/test_integration_dashboard.py
index 1ae27faae084df1f8ad9463d410ff244022fd8d9..da6071f13bf6a056c4cd910a3ff63a4444521446 100644
--- a/dbrepo-dashboard-service/init/tests/test_integration_dashboard.py
+++ b/dbrepo-dashboard-service/init/tests/test_integration_dashboard.py
@@ -108,7 +108,7 @@ class DashboardIntegrationTest(unittest.TestCase):
 
     def test_create_succeeds(self):
         # test
-        dashboard.create('some_database', 'foobar')
+        dashboard.create('some_database')
 
     def test_create_with_uid_succeeds(self):
         # test
diff --git a/dbrepo-data-service/Dockerfile b/dbrepo-data-service/Dockerfile
index 9edf1375fb6c47f63dbd45f26bba0b3a6fe15255..233e1f43c49d648c0bc562b6be406fe860871e41 100644
--- a/dbrepo-data-service/Dockerfile
+++ b/dbrepo-data-service/Dockerfile
@@ -1,5 +1,5 @@
 ###### FIRST STAGE ######
-FROM dbrepo-metadata-service:build AS dependency
+FROM dbrepo-core:build AS dependency
 LABEL org.opencontainers.image.authors="martin.weise@tuwien.ac.at"
 
 ###### SECOND STAGE ######
@@ -10,7 +10,7 @@ COPY ./pom.xml ./
 
 RUN mvn -fn dependency:go-offline
 
-COPY --from=dependency /root/.m2/repository/at/tuwien /root/.m2/repository/at/tuwien
+COPY --from=dependency /root/.m2/repository/at/ac/tuwien/ifs/dbrepo /root/.m2/repository/at/ac/tuwien/ifs/dbrepo
 
 COPY ./querystore ./querystore
 COPY ./report ./report
@@ -28,7 +28,7 @@ RUN apk add --no-cache curl bash jq
 
 WORKDIR /app
 
-RUN adduser -S -u 1001 data-service
+RUN adduser -S -u 1001 dbrepo
 
 USER 1001
 
diff --git a/dbrepo-metadata-service/Dockerfile b/dbrepo-metadata-service/Dockerfile
index fa92b799eeaac75f9daea7b5a1eec11560b04647..9bcf0d75c75ec6cfba7d49a05b08a48448e0064d 100644
--- a/dbrepo-metadata-service/Dockerfile
+++ b/dbrepo-metadata-service/Dockerfile
@@ -1,32 +1,27 @@
 ###### FIRST STAGE ######
+FROM dbrepo-core:build AS dependency
+LABEL org.opencontainers.image.authors="martin.weise@tuwien.ac.at"
+
+###### SECOND STAGE ######
 FROM maven:3-amazoncorretto-17 AS build
 LABEL org.opencontainers.image.authors="martin.weise@tuwien.ac.at"
 
 COPY ./pom.xml ./
-COPY ./api/pom.xml ./api/
-COPY ./entities/pom.xml ./entities/
-COPY ./oai/pom.xml ./oai/
-COPY ./report/pom.xml ./report/
-COPY ./repositories/pom.xml ./repositories/
-COPY ./rest-service/pom.xml ./rest-service/
-COPY ./services/pom.xml ./services/
-COPY ./test/pom.xml ./test/
-
-RUN mvn dependency:go-offline
-
-COPY ./api ./api
-COPY ./entities ./entities
+
+RUN mvn -fn dependency:go-offline
+
+COPY --from=dependency /root/.m2/repository/at/ac/tuwien/ifs/dbrepo /root/.m2/repository/at/ac/tuwien/ifs/dbrepo
+
 COPY ./oai ./oai
 COPY ./report ./report
 COPY ./repositories ./repositories
 COPY ./rest-service ./rest-service
 COPY ./services ./services
-COPY ./test ./test
 
 # Make sure it compiles
-RUN mvn clean install -DskipTests
+RUN mvn -fn clean package -DskipTests
 
-###### SECOND STAGE ######
+###### THIRD STAGE ######
 FROM amazoncorretto:17-alpine3.19 AS runtime
 LABEL org.opencontainers.image.authors="martin.weise@tuwien.ac.at"
 
@@ -34,6 +29,8 @@ RUN apk add --no-cache curl bash jq
 
 WORKDIR /app
 
+RUN adduser -S -u 1001 dbrepo
+
 USER 1001
 
 COPY --from=build --chown=1001 ./rest-service/target/dbrepo-metadata-service-rest-service-*.jar ./metadata-service.jar
@@ -41,4 +38,6 @@ COPY --from=build --chown=1001 ./rest-service/target/dbrepo-metadata-service-res
 # non-root port
 EXPOSE 8080
 
-ENTRYPOINT ["java", "-Dlog4j2.formatMsgNoLookups=true",  "-jar", "./metadata-service.jar"]
+ENV JAVA_OPTS="-Dlog4j2.formatMsgNoLookups=true"
+
+ENTRYPOINT exec java $JAVA_OPTS -jar ./metadata-service.jar
\ No newline at end of file
diff --git a/helm/dbrepo/files/create-event-listener.jar b/helm/dbrepo/files/create-event-listener.jar
index 9fa1288598118b348a3341e591558b66152c43b3..9defdad744bc4cb391b2ea104502106d0011f6af 100644
Binary files a/helm/dbrepo/files/create-event-listener.jar and b/helm/dbrepo/files/create-event-listener.jar differ
diff --git a/lib/java/dbrepo-core/Dockerfile b/lib/java/dbrepo-core/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..8a6348bbcef25836c318e41ae226295f90549a66
--- /dev/null
+++ b/lib/java/dbrepo-core/Dockerfile
@@ -0,0 +1,12 @@
+###### FIRST STAGE ######
+FROM maven:3-amazoncorretto-17 AS build
+LABEL org.opencontainers.image.authors="martin.weise@tuwien.ac.at"
+
+COPY ./pom.xml ./
+
+RUN mvn dependency:go-offline
+
+COPY ./src/ ./src/
+
+# Make sure it compiles
+RUN mvn clean install -DskipTests
\ No newline at end of file
diff --git a/make/build.mk b/make/build.mk
index 96cd6392d504c519061e5eb9d6f43b8e0ff57921..48e9212a6ac44f9d64c93fb981fa197bc407cd4e 100644
--- a/make/build.mk
+++ b/make/build.mk
@@ -2,8 +2,9 @@
 
 .PHONY: build-images
 build-images: ## Build Docker images.
-	docker build --network=host -t dbrepo-metadata-service:build --target build dbrepo-metadata-service
+	docker build --network=host -t dbrepo-core:build --target build ./lib/java/dbrepo-core
 	docker build --network=host -t dbrepo-data-service:build --target build dbrepo-data-service
+	docker build --network=host -t dbrepo-metadata-service:build --target build dbrepo-metadata-service
 	docker compose build --parallel
 
 .PHONY: build-data-service