diff --git a/LICENSE.txt b/LICENSE.txt
index 1aeea9182e5a9e154f59375feafe22065e15f843..70887f54eb6c3f76d406273298ae06a77a510956 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -40,6 +40,7 @@ This software includes third party software components:
 - https://github.com/h2database/h2database
 - https://github.com/ben-manes/caffeine
 - https://github.com/MarcGiffing/bucket4j-spring-boot-starter
+- https://github.com/commonmark/commonmark-java
 
 - https://github.com/angular/angular
 - https://github.com/angular-slider/ngx-slider
@@ -67,6 +68,6 @@ This software includes third party software components:
 - https://github.com/mgechev/codelyzer
 - https://github.com/jasmine/jasmine
 - https://github.com/karma-runner/karma
-- https://github.com/commonmark/commonmark-java
+- https://github.com/angular-split/angular-split
 
 The licenses are included in the licenses folder. 
diff --git a/licenses/angular-split.txt b/licenses/angular-split.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b80e9973d4e8ff2f4cb7c0d4a07db0d30635d80e
--- /dev/null
+++ b/licenses/angular-split.txt
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2017 Bertrand Gaillard
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/subato-web/package-lock.json b/subato-web/package-lock.json
index 2b5b3fa737e92b39d0e3fd48efe16e52351bed10..b72a0072435047601b28f9f6f02dd3d69d3386b0 100644
--- a/subato-web/package-lock.json
+++ b/subato-web/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "subato-web",
-  "version": "2.0.0-beta1",
+  "version": "2.0.0-beta2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "subato-web",
-      "version": "2.0.0-beta1",
+      "version": "2.0.0-beta2",
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
@@ -29,6 +29,7 @@
         "@ngneat/cashew": "^3.0.0",
         "@tinymce/tinymce-angular": "^7.0.0",
         "angular-pipes": "^10.0.0",
+        "angular-split": "^14.1.0",
         "bootstrap": "4.6.1",
         "codemirror": "^5.65.2",
         "core-js": "2.5.1",
@@ -4584,6 +4585,19 @@
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
     },
+    "node_modules/angular-split": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/angular-split/-/angular-split-14.1.0.tgz",
+      "integrity": "sha512-WJ7LUROpvuEy9r7/EHBO2TwrJXrHeB8PUeAWmM5gU1lCmfttAXcCGSCMMIuiCs/QNSsq8mR8rTMCvNs4Xeihfg==",
+      "dependencies": {
+        "tslib": "^2.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": ">=9.0.0",
+        "@angular/core": ">=9.0.0",
+        "rxjs": ">=6.0.0"
+      }
+    },
     "node_modules/ansi-colors": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -24123,6 +24137,14 @@
         }
       }
     },
+    "angular-split": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/angular-split/-/angular-split-14.1.0.tgz",
+      "integrity": "sha512-WJ7LUROpvuEy9r7/EHBO2TwrJXrHeB8PUeAWmM5gU1lCmfttAXcCGSCMMIuiCs/QNSsq8mR8rTMCvNs4Xeihfg==",
+      "requires": {
+        "tslib": "^2.0.0"
+      }
+    },
     "ansi-colors": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
diff --git a/subato-web/package.json b/subato-web/package.json
index 88c2e81f49d3d1e515812932b1047db0fc9c5172..64f75b73f0f2a6d6d627e9abf5a756c557ae83e8 100755
--- a/subato-web/package.json
+++ b/subato-web/package.json
@@ -1,6 +1,6 @@
 {
   "name": "subato-web",
-  "version": "2.0.0-beta2",
+  "version": "2.0.0-beta3",
   "license": "MIT",
   "scripts": {
     "ng": "ng",
@@ -43,6 +43,7 @@
     "@ngneat/cashew": "^3.0.0",
     "@tinymce/tinymce-angular": "^7.0.0",
     "angular-pipes": "^10.0.0",
+    "angular-split": "^14.1.0",
     "bootstrap": "4.6.1",
     "codemirror": "^5.65.2",
     "core-js": "2.5.1",
diff --git a/subato-web/src/app/app.component.ts b/subato-web/src/app/app.component.ts
index 41b6ffdf5203307c08e918f8ab1077fda07bc8aa..8f75420564e34c3f820cd8cf35aaa2cbcf853684 100755
--- a/subato-web/src/app/app.component.ts
+++ b/subato-web/src/app/app.component.ts
@@ -66,9 +66,9 @@ export class AppComponent extends BaseComponent {
   }
 
   initApp(url: string): void {
-    combineLatest(this.authService.loadPrincipal(), this.appStateService.loadTerm())
+    combineLatest(this.authService.loadPrincipal(), this.appStateService.loadTerm(), this.appStateService.loadConfig())
       .pipe(takeUntil(this.unsubscribe))
-      .subscribe(([principal, term]) => {
+      .subscribe(([principal, term, config]) => {
         this.appStateService.setInit(true);
         if (principal != null && this.appStateService.currentUrl == '/') {
           this.router.navigate(["/dashboard"]);
diff --git a/subato-web/src/app/core/models/public-config.ts b/subato-web/src/app/core/models/public-config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..269a89b63da3ea2413593ad55acf63521576fa73
--- /dev/null
+++ b/subato-web/src/app/core/models/public-config.ts
@@ -0,0 +1,7 @@
+export class PublicConfig {
+    maxUploadSizeInMb: number;
+
+    constructor(dto: any) {
+        this.maxUploadSizeInMb = dto.maxUploadSizeInMb;
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/core/services/app.state.service.ts b/subato-web/src/app/core/services/app.state.service.ts
index c5d4cb2ad88c2cf0b47cd0cf7946f383d2dc3ec6..0b9cc870f9fdb4006e37c253a2453c1b6ad06a96 100755
--- a/subato-web/src/app/core/services/app.state.service.ts
+++ b/subato-web/src/app/core/services/app.state.service.ts
@@ -10,7 +10,9 @@ import { Observable } from "rxjs/Observable";
 import { filter, flatMap, map, take } from "rxjs/operators";
 import { SUBATO_THEME } from "../core.module";
 import { SubatoPrincipal } from "../models/principal/subato-principal";
+import { PublicConfig } from "../models/public-config";
 import { AuthService } from "./auth.service";
+import { ConfigService } from "./config.service";
 
 
 @Injectable({
@@ -40,11 +42,14 @@ export class AppStateService {
     courseId$: Observable<number>
     courseId: number
 
+    config: PublicConfig;
+
     constructor(
         private router: Router,
         private termService: TermService,
         private authService: AuthService,
         private themeService: NbThemeService,
+        private configService: ConfigService,
         private notificationService: NotificationService
     ) {
         this._init = new BehaviorSubject<boolean>(false);
@@ -115,6 +120,16 @@ export class AppStateService {
         return this.currentTerm;
     }
 
+    public loadConfig(): Observable<PublicConfig> {
+        return this.configService.getConfig()
+            .pipe(
+                map(config => {
+                    this.config = config;
+                    return config;
+                })
+            );
+    }
+
     public loadTerm(): Observable<Term> {
         return this.termService.getAll()
             .pipe(
diff --git a/subato-web/src/app/core/services/config.service.ts b/subato-web/src/app/core/services/config.service.ts
new file mode 100755
index 0000000000000000000000000000000000000000..02581395146382d0fccb3acb0820e8e10f668b80
--- /dev/null
+++ b/subato-web/src/app/core/services/config.service.ts
@@ -0,0 +1,31 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { NbToastrService } from '@nebular/theme';
+import { HttpCacheManager, withCache } from '@ngneat/cashew';
+import { BackendError } from 'app/shared/models/backend-error';
+import { BaseService } from 'app/shared/services/base.service';
+import { environment } from 'environments/environment';
+import { Observable } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+import { PublicConfig } from '../models/public-config';
+
+@Injectable({
+    providedIn: 'root'
+})
+export class ConfigService extends BaseService {
+
+    constructor(private http: HttpClient, private cacheManager: HttpCacheManager) {
+        super();
+    }
+
+    getConfig(): Observable<PublicConfig> {
+        return this.http.get<PublicConfig>(`${environment.backendUrl}/config`, {
+            context: withCache()
+        }).pipe(
+            catchError(e => {
+                throw BackendError.parse(e);
+            }),
+            map(conf => new PublicConfig(conf))
+        );
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/core/styles/styles.scss b/subato-web/src/app/core/styles/styles.scss
index 06588fc4415d301b95d4163c6d625b41c1830c7a..600b0ddf3c4ce25aa9e00e4ad46c2eae1b6032ef 100755
--- a/subato-web/src/app/core/styles/styles.scss
+++ b/subato-web/src/app/core/styles/styles.scss
@@ -111,6 +111,10 @@
     }
   }
 
+  nb-accordion-item-body.no-padding .item-body {
+    padding: 0px !important;
+  }
+
   table {
     tr.hover {
       &:hover {
@@ -227,7 +231,7 @@
       @supports (position: sticky) {
         position: sticky;
         top: 0;
-        z-index: 1020;
+        z-index: 991;
       }
 
       max-height: calc(100vh - 105px) !important;
@@ -240,7 +244,7 @@
       @supports (position: sticky) {
         position: sticky;
         top: 0;
-        z-index: 1020;
+        z-index: 991;
       }
 
       max-height: calc(100vh - 105px) !important;
@@ -251,6 +255,7 @@
   .sticky-top {
     max-height: calc(100vh - 105px) !important;
     top: 90px !important;
+    z-index: 991 !important;
   }
 
   .title {
@@ -372,4 +377,57 @@
     padding-left: 100px !important;
     padding-right: 100px !important;
   }
+
+  nb-spinner {
+    z-index: 990 !important;
+  }
+
+  .nb-tree-grid-row:nth-child(2n):not(:hover), .nb-tree-grid-row {
+    background: unset;
+  }
+
+  .nb-tree-grid-row:hover {
+    background-color: var(--option-hover-background-color) !important;
+    color: var(--option-hover-text-color) !important;
+  }
+
+  .nb-tree-grid-row:nth-child(2n):not(:hover).selected, .nb-tree-grid-row.selected {
+    //background-color: var(--button-filled-basic-background-color) !important;
+    background-color: var(--option-active-background-color);
+    color: var(--option-active-text-color);
+  }
+
+  nb-tree-grid-row-toggle nb-icon {
+    color: var(--option-text-color) !important;
+  }
+
+  as-split .as-split-gutter {
+    background-color: var(--background-basic-color-3) !important;
+  }
+
+  .fullscreen {
+    width: 100vw;
+    height: 100vh !important;
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 999999;
+  }
+
+  body.noscroll{
+      position:fixed;
+      overflow:hidden;
+  }
+
+  .badge {
+    font-size: 100%
+  }
+
+  .badge-primary {
+    background-color: nb-theme(color-primary-500);
+  }
+
+  solution-ide as-split-area {
+    overflow: hidden !important;
+  }
 }
\ No newline at end of file
diff --git a/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html b/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html
index 32753125353d9640fb97788788c09e9046a63935..9f0322f3fae2c06a33c8a7344d84e1db11cb34ac 100644
--- a/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html
+++ b/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html
@@ -23,11 +23,11 @@
             <span *ngIf="file.description" class="subtitle text-muted">{{ file.description }} </span>
           </div>
           <div class="interaction">
-            <a nbButton *ngIf="principal != null && principal.isAdmin(course.id)" [nbSpinner]="deletingFile == file.id"
+            <button nbButton *ngIf="principal != null && principal.isAdmin(course.id)" [disabled]="deletingFile == file.id" [nbSpinner]="deletingFile == file.id"
               (click)="$event.stopPropagation(); deleteFile(file)" [status]="'danger'">
               <nb-icon icon="trash-2-outline" pack="eva"></nb-icon>
               <span class="d-none d-md-block d-lg-block d-xl-block">Löschen</span>
-            </a>
+            </button>
           </div>
         </a>
       </nb-list-item>
diff --git a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html
index 4a52137cac484611bd86dc579172f40ca441f9d9..2f5cc5baf0ead3216c328869c3ef3635691ac3ed 100644
--- a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html
+++ b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html
@@ -8,6 +8,7 @@
                 <div class="form-group">
                     <label for="file" class="label">Datei</label>
                     <dropzone id="file" [file]="file" (fileChanged)="handleFile($event)"></dropzone>
+                    <small>Die maximale Dateigröße beträgt {{ config.maxUploadSizeInMb }} MB.</small>
                 </div>
                 <div class="form-group">
                     <label for="description" class="label">Beschreibung</label>
diff --git a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts
index 97eea4d7a0fff11c3afed752fcaa7c65f6a4a52f..d0f0d68095d94af4e090a1418dcb7008276c29f4 100644
--- a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts
+++ b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts
@@ -1,6 +1,8 @@
 import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
 import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
 import { Router, ActivatedRoute } from '@angular/router';
+import { PublicConfig } from 'app/core/models/public-config';
+import { AppStateService } from 'app/core/services/app.state.service';
 import { BaseComponent } from 'app/shared/components/base/base.component';
 import { BackendError } from 'app/shared/models/backend-error';
 import { NotificationService } from 'app/shared/services/notification.service';
@@ -23,13 +25,17 @@ export class CourseFileUploadComponent extends BaseComponent implements OnInit {
   form: UntypedFormGroup;
   file: File;
 
+  config: PublicConfig;
+
   constructor(
     private courseFileService: CourseFileService,
+    private appStateService: AppStateService,
     private router: Router,
     private route: ActivatedRoute,
     private notificationService: NotificationService,
     private fb: UntypedFormBuilder) {
     super();
+    this.config = appStateService.config;
   }
 
   ngOnInit(): void {
diff --git a/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html b/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html
index 23a6648612ae4361ba7d3a74bfd43df258ea3d01..d1636c4aaf1ecf4b93012d4b5fa0a97d44a2ee32 100644
--- a/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html
+++ b/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html
@@ -14,9 +14,9 @@
                             <span class="subtitle text-muted">{{ module.program.name }}</span>
                         </div>
                         <div class="interaction">
-                            <a nbButton (click)="remove(module)" [status]="'danger'">
+                            <button nbButton (click)="remove(module)" [status]="'danger'">
                                 <nb-icon icon="close-outline" pack="eva"></nb-icon>
-                            </a>
+                            </button>
                         </div>
                     </nb-list-item>
                 </nb-list>
@@ -26,10 +26,10 @@
     <div class="col-12 col-lg-6">
         <div class="row mb-3">
             <div class="col-12 d-flex" style="align-items: flex-start">
-                <a nbButton class="mr-3" [disabled]="selectableModules.length == 0" (click)="addAll()"
+                <button nbButton class="mr-3" [disabled]="selectableModules.length == 0" (click)="addAll()"
                     [status]="'primary'">
                     <nb-icon icon="plus-outline" pack="eva"></nb-icon> Alle
-                </a>
+                </button>
                 <nb-form-field style="flex-grow: 1">
                     <nb-icon nbPrefix icon="search-outline" pack="eva"></nb-icon>
                     <input type="text" (ngModelChange)="filterModules($event)" [ngModel]="query"
@@ -47,9 +47,9 @@
                             </nb-list-item>
                             <nb-list-item *ngFor="let module of selectableModules">
                                 <div class="interaction mr-3">
-                                    <a nbButton (click)="add(module)" [status]="'primary'">
+                                    <button nbButton (click)="add(module)" [status]="'primary'">
                                         <nb-icon icon="plus-outline" pack="eva"></nb-icon>
-                                    </a>
+                                    </button>
                                 </div>
                                 <div class="content" style="flex-grow: 1">
                                     <div class="title">
diff --git a/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html b/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html
index cd099effc452d463d8a0c8026ff9c8276f339fba..5162cb69fd0e61bada08a7b8a1e72c5c5801a2de 100644
--- a/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html
+++ b/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html
@@ -11,16 +11,16 @@
                             </div>
                         </div>
                         <div style="align-self: center; gap: 15px" class="d-flex">
-                            <a *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'basic'"
+                            <button *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'basic'"
                                 [nbSpinner]="reimportingCourse" [disabled]="reimportingCourse" (click)="reimport()">
                                 <nb-icon icon="refresh-outline" pack="eva"></nb-icon>
                                 <span class="d-none d-md-block d-lg-block d-xl-block">AoR Reimport</span>
-                            </a>
-                            <a *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'danger'"
+                            </button>
+                            <button *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'danger'"
                                 [nbSpinner]="deletingCourse" [disabled]="deletingCourse" (click)="deleteCourse()">
                                 <nb-icon icon="trash-2-outline" pack="eva"></nb-icon>
                                 <span class="d-none d-md-block d-lg-block d-xl-block">Kurs löschen</span>
-                            </a>
+                            </button>
                         </div>
                     </nb-card-header>
                 </nb-card>
diff --git a/subato-web/src/app/features/courses/pages/new-course/new-course.component.html b/subato-web/src/app/features/courses/pages/new-course/new-course.component.html
index c90a2228aa7bf40cfa315aea29c783c0e2bccb68..bcc8c107bbbcb5e6551750e684cae1f6129653f0 100644
--- a/subato-web/src/app/features/courses/pages/new-course/new-course.component.html
+++ b/subato-web/src/app/features/courses/pages/new-course/new-course.component.html
@@ -23,21 +23,12 @@
                         <nb-stepper orientation="horizontal" [linear]="true">
                             <nb-step label="Vorlage" [stepControl]="templateForm">
                                 <div class="step-container">
-                                    <div class="buttons mb-4">
-                                        <button nbButton disabled nbStepperPrevious>
-                                            <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon>
-                                            Zurück
-                                        </button>
-                                        <button nbButton nbStepperNext [disabled]="!templateForm.valid">
-                                            Weiter
-                                            <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon>
-                                        </button>
-                                    </div>
                                     <nb-alert class="d-block">
                                         Es kann ein bestehender Kurs als Vorlage angegeben werden. Aus diesem Kurs
-                                        werden dann bestimmte Daten kopiert. 
+                                        werden dann bestimmte Daten kopiert.
                                         <br>
-                                        Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere Informationen dazu. 
+                                        Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere
+                                        Informationen dazu.
                                     </nb-alert>
                                     <div class="form-group mb-3">
                                         <nb-toggle [ngModel]="useTemplate" (checkedChange)="useTemplateChanged($event)">
@@ -50,21 +41,21 @@
                                             message="Kurs muss ausgewählt werden">
                                         </validation-hint>
                                     </div>
-                                </div>
-                            </nb-step>
-
-                            <nb-step label="Allgemein" [stepControl]="generalForm">
-                                <form class="step-container" [formGroup]="generalForm" ngNativeValidate>
-                                    <div class="buttons mb-4">
-                                        <button nbButton nbStepperPrevious>
+                                    <div class="buttons mt-4">
+                                        <button nbButton disabled nbStepperPrevious>
                                             <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon>
                                             Zurück
                                         </button>
-                                        <button nbButton nbStepperNext [disabled]="!generalForm.valid">
+                                        <button nbButton nbStepperNext [disabled]="!templateForm.valid">
                                             Weiter
                                             <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon>
                                         </button>
                                     </div>
+                                </div>
+                            </nb-step>
+
+                            <nb-step label="Allgemein" [stepControl]="generalForm">
+                                <form class="step-container" [formGroup]="generalForm" ngNativeValidate>
                                     <div class="row">
                                         <div class="col-12 col-lg-6">
                                             <div class="form-group">
@@ -91,25 +82,29 @@
                                             </div>
                                         </div>
                                     </div>
-                                </form>
-                            </nb-step>
-
-                            <nb-step label="AoR">
-                                <div class="step-container">
-                                    <div class="buttons mb-4">
+                                    <div class="buttons mt-4">
                                         <button nbButton nbStepperPrevious>
                                             <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon>
                                             Zurück
                                         </button>
-                                        <button nbButton (click)="save()" status="success" nbStepperNext>
-                                            <nb-icon icon="save-outline" pack="eva"></nb-icon> Erstellen
+                                        <button nbButton nbStepperNext [disabled]="!generalForm.valid">
+                                            Weiter
+                                            <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon>
                                         </button>
                                     </div>
+                                </form>
+                            </nb-step>
+
+                            <nb-step label="AoR">
+                                <div class="step-container">
+
                                     <nb-alert class="d-block">
                                         Kurse können durch die AoR-Integration mit Modulen aus den
-                                        Informatikstudiengängen verknüpft werden. Dies ermöglicht den Import von Veranstaltungen und deren Teilnehmer. 
+                                        Informatikstudiengängen verknüpft werden. Dies ermöglicht den Import von
+                                        Veranstaltungen und deren Teilnehmer.
                                         <br>
-                                        Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere Informationen dazu. 
+                                        Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere
+                                        Informationen dazu.
                                     </nb-alert>
 
                                     <nb-alert *ngIf="permanent" class="d-block" accent="warning">Du hast ausgewählt,
@@ -117,11 +112,23 @@
                                         der
                                         Kurs permanent ist. Teilnehmer, Veranstaltungen und Berechtigungen werden
                                         bei
-                                        permanenten Kursen nicht importiert. Eine Verknüpfung kann aber aufgrund der Kategorisierung in der Kursliste dennoch sinnvoll
+                                        permanenten Kursen nicht importiert. Eine Verknüpfung kann aber aufgrund der
+                                        Kategorisierung in der Kursliste dennoch sinnvoll
                                         sein.</nb-alert>
 
                                     <ngx-module-multiselect [selectedModules]="selectedModules"
                                         (modulesChanged)="modulesChanged($event)"></ngx-module-multiselect>
+
+                                    <div class="buttons mt-4">
+                                        <button nbButton nbStepperPrevious>
+                                            <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon>
+                                            Zurück
+                                        </button>
+                                        <button nbButton [nbSpinner]="saving" (click)="save()" [disabled]="saving"
+                                            status="success" nbStepperNext>
+                                            <nb-icon icon="save-outline" pack="eva"></nb-icon> Erstellen
+                                        </button>
+                                    </div>
                                 </div>
                             </nb-step>
                         </nb-stepper>
diff --git a/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts b/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts
index c6d20b0b12c08c0381b04675fdc15388c8ebe2d3..16ad19c18f68e38cc61716a9a98ea629df4c22f1 100644
--- a/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts
+++ b/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts
@@ -37,6 +37,8 @@ export class NewCourseComponent extends FormComponent implements OnInit {
   templateCourse: Course | null;
   selectedModules: ProgramModule[] = [];
 
+  saving: boolean = false;
+
   constructor(
     private route: ActivatedRoute,
     private router: Router,
@@ -107,6 +109,8 @@ export class NewCourseComponent extends FormComponent implements OnInit {
   }
 
   save() {
+    this.saving = true;
+
     let dto: CreateCourseDto = {
       name: this.generalForm.get('name').value,
       shortName: this.generalForm.get('shortName').value,
@@ -121,6 +125,17 @@ export class NewCourseComponent extends FormComponent implements OnInit {
       .subscribe(course => {
         this.notificationService.showSuccess("Kurs erfolgreich erstellt");
         this.router.navigate(['course', course.id]);
+        this.saving = false;
+      }, error => {
+        this.saving = false;
+
+        if (BackendError.isBackendError(error)) {
+          this.notificationService.showError(error.format(
+            "Fehler bei Erstellung des Kurses",
+          ));
+        } else {
+          throw error;
+        }
       });
   }
 }
diff --git a/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts b/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts
index 9e84f0e34b5c9b4c6fc9c8a66911f6ed6a5b0716..c4946e41ef1b3424d8a75727541a727c7291e4d9 100644
--- a/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts
+++ b/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts
@@ -23,6 +23,6 @@ export class EclipseImportComponent implements OnInit {
   }
 
   generateImportLink() {
-    this.importLink = `eclipse+command://de.hsrm.sls.subato.eclipse.core.commands.createTask?de.hsrm.sls.subato.eclipse.core.commands.createTask.parameter.exerciseId=${this.taskInstance.exercise.id}&de.hsrm.sls.subato.eclipse.core.commands.createTask.parameter.taskId=${this.taskInstance.task.id}`;
+    this.importLink = `eclipse+command://de.hsrm.sls.subato.eclipse.core.commands.importTask?de.hsrm.sls.subato.eclipse.core.commands.importTask.parameter.exerciseId=${this.taskInstance.exercise.id}&de.hsrm.sls.subato.eclipse.core.commands.importTask.parameter.taskId=${this.taskInstance.task.id}`;
   }
 }
diff --git a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html
index cc72c19d852090a75a2006a6992f70e7c4448742..fa06dd75ec9af4ee5a33682f670990a24c9fcb1c 100644
--- a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html
+++ b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html
@@ -1,9 +1,14 @@
 <div *ngIf="showFilter" class="row mt-3">
-  <div class="col-12">
-    <nb-toggle [ngModel]="hideInvisible" (ngModelChange)="applyFilter($event)">Unsichtbare Übungen
+  <div class="col-12 d-flex" style="justify-content: space-between; flex-wrap: wrap;gap: 15px;">
+    <nb-form-field style="flex-grow: 1; max-width: 600px">
+      <nb-icon nbPrefix icon="search-outline" pack="eva"></nb-icon>
+      <input type="text" (ngModelChange)="onQueryChange($event)" [ngModel]="query" nbInput fullWidth>
+    </nb-form-field>
+    <nb-toggle [ngModel]="hideInvisible" (ngModelChange)="onHideInvisibleChanged($event)" style="min-width: 300px;">Unsichtbare Übungen
       ausblenden</nb-toggle>
   </div>
 </div>
+
 <div class="row mt-3">
   <div class="col-12">
     <div>
@@ -37,7 +42,9 @@
                 <nb-icon icon="question-mark-circle-outline"></nb-icon>
               </span>
               <span>
-                <a *ngIf="exerciseProgress.submission" [routerLink]="['/', 'submissions', exerciseProgress.submission.id]">{{ exerciseProgress.exercise.name }}</a>
+                <a *ngIf="exerciseProgress.submission"
+                  [routerLink]="['/', 'submissions', exerciseProgress.submission.id]">{{ exerciseProgress.exercise.name
+                  }}</a>
                 <span *ngIf="!exerciseProgress.submission">{{ exerciseProgress.exercise.name }}</span>
               </span>
             </th>
@@ -66,10 +73,11 @@
             [class.success]="exerciseProgress.submission && exerciseProgress.submission.passed == true"
             [class.danger]="exerciseProgress.submission && exerciseProgress.submission.passed == false"
             [class.basic]="exerciseProgress.submission && exerciseProgress.submission.passed == null"
-            *ngFor="let taskInstance of exerciseProgress.taskInstances"
-            >
+            *ngFor="let taskInstance of exerciseProgress.taskInstances">
             <td>
-              <a *ngIf="hasSolution(exerciseProgress, taskInstance)" [routerLink]="['/', 'submissions', exerciseProgress.submission.id]" [queryParams]="{ taskId: taskInstance.task.id }">{{ taskInstance.task.name }}</a>
+              <a *ngIf="hasSolution(exerciseProgress, taskInstance)"
+                [routerLink]="['/', 'submissions', exerciseProgress.submission.id]"
+                [queryParams]="{ taskId: taskInstance.task.id }">{{ taskInstance.task.name }}</a>
               <span *ngIf="!hasSolution(exerciseProgress, taskInstance)">{{ taskInstance.task.name }}</span>
             </td>
             <td class="fit center">
diff --git a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts
index 04e92265a402318c93dad1c94620ad452893e60a..aea71a6449db6749cd8d65c89c12f6e0ac9446ec 100644
--- a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts
+++ b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts
@@ -2,31 +2,47 @@ import { Component, Input, OnInit } from '@angular/core';
 import { Router } from '@angular/router';
 import { TaskInstance } from '../../models/task-instance';
 import { ExerciseProgress } from '../../models/exercise-progress';
+import { Subject } from 'rxjs';
+import { BaseComponent } from 'app/shared/components/base/base.component';
+import { takeUntil, debounceTime } from 'rxjs/operators';
 
 @Component({
   selector: 'ngx-exercise-progress',
   templateUrl: './exercise-progress.component.html',
   styleUrls: ['./exercise-progress.component.scss']
 })
-export class ExerciseProgressComponent implements OnInit {
+export class ExerciseProgressComponent extends BaseComponent implements OnInit {
   @Input() _exerciseProgresses: ExerciseProgress[] = [];
   filteredExerciseProgresses: ExerciseProgress[] = [];
   @Input() showFilter: boolean;
   hideInvisible = true;
 
+  query: string;
+  private queryChanged: Subject<string> = new Subject<string>();
+  
   @Input() set exerciseProgresses(val: ExerciseProgress[]) {
     this._exerciseProgresses = val;
-    console.log(this.hideInvisible);
-    this.applyFilter(this.hideInvisible);
+    this.applyFilter();
   }
 
   get exerciseProgresses(): ExerciseProgress[] {
     return this._exerciseProgresses;
   }
 
-  constructor(private router: Router) { }
+  constructor(private router: Router) {
+    super();
+  }
 
   ngOnInit(): void {
+    this.queryChanged
+      .pipe(
+        takeUntil(this.unsubscribe),
+        debounceTime(500),
+      )
+      .subscribe(query => {
+        this.query = query;
+        this.applyFilter();
+      });
   }
 
   navigateToSubmission(exerciseProgress: ExerciseProgress) {
@@ -50,10 +66,25 @@ export class ExerciseProgressComponent implements OnInit {
     return exerciseResult.submission?.pointRatings.find(r => r.taskInstance.task.id == taskInstance.task.id);
   }
 
-  applyFilter(val: boolean) {
+  onHideInvisibleChanged(val: boolean) {
     this.hideInvisible = val;
+    this.applyFilter();
+  }
+
+  onQueryChange(query) {
+    this.queryChanged.next(query);
+  }
+
+  applyFilter() {
     this.filteredExerciseProgresses = this.hideInvisible 
       ? this.exerciseProgresses.filter(r => !this.hideInvisible ||  r.exercise.visible)
       : this.exerciseProgresses;
+    
+    this.filteredExerciseProgresses = this.query 
+      ? this.filteredExerciseProgresses.filter(
+          r => r.exercise.name.toLowerCase().indexOf(this.query.toLowerCase()) !== -1 ||
+          r.taskInstances.some(ti => ti.task.name.toLocaleLowerCase().indexOf(this.query.toLowerCase()) !== -1)
+      )
+      : this.filteredExerciseProgresses;
   }
 }
diff --git a/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html b/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html
index 21d3120a63a36f5a28f1d6e0bca0f868e84af5d4..bb7493e1f150ed5f16dfee763bc0a38408deeb1d 100644
--- a/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html
+++ b/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html
@@ -23,9 +23,9 @@
         <nb-list *ngIf="!error">
           <nb-list-item *ngFor="let task of selectableTasks">
             <div class="mr-3">
-              <a nbButton [status]="'primary'" (click)="selectTask(task)">
+              <button nbButton [status]="'primary'" (click)="selectTask(task)">
                 <nb-icon icon="plus-outline" pack="eva"></nb-icon>
-              </a>
+              </button>
             </div>
             <div style="flex-grow: 1">
               <div class="title">
diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.html b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..2a85b84e95d34a5765b5afbf16c253925a2bb0ac
--- /dev/null
+++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.html
@@ -0,0 +1,14 @@
+<nb-alert class="h-100 mb-0" style="flex-direction: row;
+    align-items: center;justify-content: space-between;" [status]="numerator != 0 ? status : 'basic'">
+    <div style="display: flex; align-items: baseline; flex-direction: column; gap: 5px;">
+        <div style="display: flex; flex-direction:row; flex-wrap: wrap; align-items: baseline; gap: 5px;">
+            <div style="font-size: 34px;">
+                {{ numerator }}
+            </div>
+            <div>/ {{ denominator }} Übungen</div>
+        </div>
+        <div class="details">
+            <nb-icon class="mr-1" style="flex-shrink: 0" [icon]="icon"></nb-icon> <strong>{{ label }}</strong>
+        </div>
+    </div>
+</nb-alert>
\ No newline at end of file
diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.scss b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e242b3a76526ab0af4e42c7cddbca2ebf5c69a02
--- /dev/null
+++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.scss
@@ -0,0 +1,7 @@
+.details {
+    flex: 1 1 auto;
+    flex-direction: column;
+    justify-content: center;
+    height: 100%;
+    border-left: 1px solid transparent;
+  }
\ No newline at end of file
diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.spec.ts b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4672d3e23b62be3046d6e3933fcc95ca5167c91a
--- /dev/null
+++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SubmissionInfoBannerComponent } from './submission-info-banner.component';
+
+describe('SubmissionInfoBannerComponent', () => {
+  let component: SubmissionInfoBannerComponent;
+  let fixture: ComponentFixture<SubmissionInfoBannerComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ SubmissionInfoBannerComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(SubmissionInfoBannerComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.ts b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f18864bec577847672aaa7fdd4f8a4b3998c4aa8
--- /dev/null
+++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.ts
@@ -0,0 +1,20 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'ngx-submission-info-banner',
+  templateUrl: './submission-info-banner.component.html',
+  styleUrls: ['./submission-info-banner.component.scss']
+})
+export class SubmissionInfoBannerComponent implements OnInit {
+  @Input() status: string;
+  @Input() numerator: number;
+  @Input() denominator: number;
+  @Input() label: string;
+  @Input() icon: string;
+
+  constructor() { }
+
+  ngOnInit(): void {
+  }
+
+}
diff --git a/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.html b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..8b288b9a2c1039f54fa0ff8d996f9e3d3c832334
--- /dev/null
+++ b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.html
@@ -0,0 +1,17 @@
+<div class="row">
+    <div class="col-12 col-xl-4 mb-3">
+      <ngx-submission-info-banner status="success" icon="checkmark-circle-2-outline" 
+        [numerator]="exerciseProgressesResult.submissionsProgress.passedTotal" [denominator]="exerciseProgressesResult.exerciseProgresses.length"
+        label="bestanden"></ngx-submission-info-banner>
+    </div>
+    <div class="col-12 col-xl-4 mb-3">
+      <ngx-submission-info-banner status="danger" icon="close-circle-outline" 
+        [numerator]="exerciseProgressesResult.submissionsProgress.notPassedTotal" [denominator]="exerciseProgressesResult.exerciseProgresses.length"
+        label="nicht bestanden"></ngx-submission-info-banner>
+    </div>
+    <div class="col-12 col-xl-4 mb-3">
+      <ngx-submission-info-banner status="primary" icon="question-mark-circle-outline" 
+        [numerator]="exerciseProgressesResult.submissionsProgress.undecidedTotal" [denominator]="exerciseProgressesResult.exerciseProgresses.length"
+        label="ausstehend"></ngx-submission-info-banner>
+    </div>
+  </div>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.scss b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.scss
similarity index 100%
rename from subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.scss
rename to subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.scss
diff --git a/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.spec.ts b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e73783dffdebc24aa4ecfec8904e7cb299b9317
--- /dev/null
+++ b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SubmissionProgressContainerComponent } from './submission-progress-container.component';
+
+describe('SubmissionProgressContainerComponent', () => {
+  let component: SubmissionProgressContainerComponent;
+  let fixture: ComponentFixture<SubmissionProgressContainerComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ SubmissionProgressContainerComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(SubmissionProgressContainerComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.ts b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15e6040ad140634b4d4f219c788ba90cbd17da71
--- /dev/null
+++ b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.ts
@@ -0,0 +1,17 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { ExerciseProgressResult } from '../../models/exercise-progress-result';
+
+@Component({
+  selector: 'ngx-submission-progress-container',
+  templateUrl: './submission-progress-container.component.html',
+  styleUrls: ['./submission-progress-container.component.scss']
+})
+export class SubmissionProgressContainerComponent implements OnInit {
+  @Input() exerciseProgressesResult: ExerciseProgressResult;
+  
+  constructor() { }
+
+  ngOnInit(): void {
+  }
+
+}
diff --git a/subato-web/src/app/features/exercises/exercises.module.ts b/subato-web/src/app/features/exercises/exercises.module.ts
index 6a4c558ed1c040b011ad4af5c49fe60a98bd22c9..9a7fe274bd47c14afa055fbf056b63c807eeb145 100644
--- a/subato-web/src/app/features/exercises/exercises.module.ts
+++ b/subato-web/src/app/features/exercises/exercises.module.ts
@@ -25,6 +25,8 @@ import { PointsPipe } from './pipes/points.pipe';
 import { TaskInstanceSummaryComponent } from './components/task-instance-summary/task-instance-summary.component';
 import { UserAttemptsPipe } from './pipes/user-attempts.pipe';
 import { SolutionsModule } from '../solutions/solutions.module';
+import { SubmissionInfoBannerComponent } from './components/submission-info-banner/submission-info-banner.component';
+import { SubmissionProgressContainerComponent } from './components/submission-progress-container/submission-progress-container.component';
 
 @NgModule({
   declarations: [
@@ -51,6 +53,8 @@ import { SolutionsModule } from '../solutions/solutions.module';
     SubmissionSelectionComponent,
     SubmissionsFinishedScreenComponent,
     TaskInstanceSummaryComponent,
+    SubmissionInfoBannerComponent,
+    SubmissionProgressContainerComponent,
   ],
   imports: [
     CommonModule,
@@ -61,7 +65,8 @@ import { SolutionsModule } from '../solutions/solutions.module';
     ExercisesRoutingModule
   ],
   exports: [
-    ExerciseProgressComponent
+    ExerciseProgressComponent,
+    SubmissionProgressContainerComponent
   ]
 })
 export class ExercisesModule { }
diff --git a/subato-web/src/app/features/exercises/models/exercise-progress-result.ts b/subato-web/src/app/features/exercises/models/exercise-progress-result.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7da8dcea44307277e562d301680258a730bf24f5
--- /dev/null
+++ b/subato-web/src/app/features/exercises/models/exercise-progress-result.ts
@@ -0,0 +1,12 @@
+import { ExerciseProgress } from "./exercise-progress";
+import { SubmissionsProgress } from "./submissions-progress";
+
+export class ExerciseProgressResult {
+    exerciseProgresses: ExerciseProgress[];
+    submissionsProgress: SubmissionsProgress;
+
+    constructor(result: any) {
+        this.exerciseProgresses = result.exerciseProgresses.map(p => new ExerciseProgress(p));
+        this.submissionsProgress = new SubmissionsProgress(result.submissionsProgress);
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/exercises/pages/submission/submission.component.html b/subato-web/src/app/features/exercises/pages/submission/submission.component.html
index 3f3dd4291e6bd475fa56f89ce4ce8b4e3fea3851..ee5397da64a4dbb6f88d840cef7fbdd429319ac4 100644
--- a/subato-web/src/app/features/exercises/pages/submission/submission.component.html
+++ b/subato-web/src/app/features/exercises/pages/submission/submission.component.html
@@ -149,7 +149,7 @@
                 <nb-card-body>
                   <nb-tabset #solutionTabset class="no-side-padding d-flex flex-column">
                     <nb-tab tabId="solution" tabTitle="Lösung">
-                      <solution-viewer *ngIf="currentSolution" [solution]="currentSolution"></solution-viewer>
+                      <solution-ide *ngIf="currentSolution" withBorder="true" [solution]="currentSolution"></solution-ide>
                       <div class="p-3" *ngIf="!currentSolution">Keine Lösung ausgewählt</div>
                     </nb-tab>
                     <nb-tab tabTitle="Historie" style="max-height: 400px"
diff --git a/subato-web/src/app/features/exercises/services/exercise.service.ts b/subato-web/src/app/features/exercises/services/exercise.service.ts
index 718ca781b93ca52eb78d9c7be9baf0b42a7de4b3..415d6c2ae8e669c38c0ed3fe2a34a3dc344e06c4 100755
--- a/subato-web/src/app/features/exercises/services/exercise.service.ts
+++ b/subato-web/src/app/features/exercises/services/exercise.service.ts
@@ -16,6 +16,7 @@ import { ExerciseProgress } from "../models/exercise-progress";
 import { TaskInstance } from "../models/task-instance";
 import { FileRef } from "app/features/tasks/models/file-ref";
 import { DownloadService } from "app/shared/services/download.service";
+import { ExerciseProgressResult } from "../models/exercise-progress-result";
 
 @Injectable({
     providedIn: 'root'
@@ -90,18 +91,18 @@ export class ExerciseService extends BaseService {
     }
 
 
-    getProgressForUser(courseId: number, userId: number): Observable<ExerciseProgress[]> {
+    getProgressForUser(courseId: number, userId: number): Observable<ExerciseProgressResult> {
         let params = new HttpParams();
         params = params.set('courseId', courseId);
         params = params.set('userId', userId);
         
-        return this.http.get<ExerciseProgress[]>(`${environment.backendUrl}/exercise/progress`, {
+        return this.http.get<ExerciseProgressResult>(`${environment.backendUrl}/exercise/progress`, {
             params: params    
         }).pipe(
             catchError(e => {
                 throw BackendError.parse(e);
             }),
-            map(exerciseProgresses => exerciseProgresses.map(exerciseProgress => new ExerciseProgress(exerciseProgress)))
+            map(result => new ExerciseProgressResult(result))
         )
     }
 
diff --git a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html
index 615aa167968e781a37faee55e1cf74e0db57dc46..bbe77ac50d56f7a5bfd55102bc98a7130f623203 100644
--- a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html
+++ b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html
@@ -45,7 +45,8 @@
               <div class="col-12">
                 <nb-tabset class="no-side-padding">
                   <nb-tab tabTitle="Ergebnisse">
-                    <ngx-exercise-progress *ngIf="exerciseProgresses" [showFilter]="true" [exerciseProgresses]="exerciseProgresses">
+                    <ngx-submission-progress-container *ngIf="exerciseProgressResult" [exerciseProgressesResult]="exerciseProgressResult"></ngx-submission-progress-container>
+                    <ngx-exercise-progress *ngIf="exerciseProgressResult" [showFilter]="true" [exerciseProgresses]="exerciseProgressResult.exerciseProgresses">
                     </ngx-exercise-progress>
                   </nb-tab>
                   <nb-tab>
diff --git a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts
index cb8678b92ef82afa4adedeb352d14d0e1e544d0c..3a531c0daa22acea277dd50468f9612bd0238084 100644
--- a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts
+++ b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts
@@ -17,6 +17,7 @@ import { ErrorContainerConfig } from 'app/shared/components/error-container/erro
 import { AppStateService } from 'app/core/services/app.state.service';
 import { Breadcrumb } from 'app/shared/components/breadcrumbs/breadcrumbs.component';
 import { SubatoPrincipal } from 'app/core/models/principal/subato-principal';
+import { ExerciseProgressResult } from 'app/features/exercises/models/exercise-progress-result';
 
 @Component({
   selector: 'ngx-course-unit-progress',
@@ -30,7 +31,7 @@ export class CourseUnitProgressComponent extends BaseComponent implements OnInit
   loadingProgress: boolean = false;
   courseUnit: CourseUnit;
   participantAttendance: ParticipantAttendance;
-  exerciseProgresses: ExerciseProgress[] = [];
+  exerciseProgressResult: ExerciseProgressResult;
 
   loadingProgressError: BackendError;
   error: BackendError;
@@ -118,9 +119,9 @@ export class CourseUnitProgressComponent extends BaseComponent implements OnInit
     .pipe(
       takeUntil(this.unsubscribe),
     )
-    .subscribe(([participantAttendance, courseResult]) => {
+    .subscribe(([participantAttendance, exerciseProgressResult]) => {
       this.participantAttendance = participantAttendance;
-      this.exerciseProgresses = courseResult;
+      this.exerciseProgressResult = exerciseProgressResult;
       this.loadingProgress = false;
     }, (error) => {
       this.loadingProgress = false;
diff --git a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html
index b1a4549b5a9de9594ad9176db78ceca0f4e824c4..7ccf4b3809441d4885eded97ddadcf078077ac98 100644
--- a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html
+++ b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html
@@ -20,9 +20,10 @@
                 <nb-card>
                     <nb-card-body>
                         <nb-tabset class="no-side-padding">
-                            <nb-tab tabTitle="Ergebnisse">
+                            <nb-tab *ngIf="exerciseProgressResult" tabTitle="Ergebnisse">
+                                <ngx-submission-progress-container [exerciseProgressesResult]="exerciseProgressResult"></ngx-submission-progress-container>
                                 <ngx-exercise-progress [showFilter]="canSeeFilter"
-                                    [exerciseProgresses]="exerciseProgresses">
+                                    [exerciseProgresses]="exerciseProgressResult.exerciseProgresses">
                                 </ngx-exercise-progress>
                             </nb-tab>
                             <nb-tab *ngFor="let participantAttendance of participantAttendances">
diff --git a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts
index e8f016cdc0d33c777a8132d0e4fada1e51d924e4..f383413c96a0df81d2c7c7cc19a4eb66d367d8d4 100644
--- a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts
+++ b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts
@@ -19,6 +19,7 @@ import { UserService } from 'app/core/services/user.service';
 import { ErrorContainerConfig } from 'app/shared/components/error-container/error-container.component';
 import { AppStateService } from 'app/core/services/app.state.service';
 import { SubatoPrincipal } from 'app/core/models/principal/subato-principal';
+import { ExerciseProgressResult } from 'app/features/exercises/models/exercise-progress-result';
 
 @Component({
   selector: 'ngx-participant-progress',
@@ -33,7 +34,7 @@ export class ParticipantProgressComponent extends BaseComponent implements OnIni
   course: Course;
   participantAttendances: ParticipantAttendance[];
   events: CourseUnitEvent[];
-  exerciseProgresses: ExerciseProgress[] = [];
+  exerciseProgressResult: ExerciseProgressResult;
   hideInvisible;
 
   error: BackendError;
@@ -98,10 +99,10 @@ export class ParticipantProgressComponent extends BaseComponent implements OnIni
       this.userService.getById(userId)
     )
     .pipe(
-      map(([attendances, exerciseProgresses, user]) => {
+      map(([attendances, exerciseProgressResult, user]) => {
         this.user = user;
         this.participantAttendances = attendances;
-        this.exerciseProgresses = exerciseProgresses;
+        this.exerciseProgressResult = exerciseProgressResult;
       })
     );
   }
diff --git a/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html b/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html
index 23710fd8414b10e635cda86593b6a3e376773b70..2c91b642ee60eed8ee438487ccaf384ad9c412fe 100644
--- a/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html
+++ b/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html
@@ -3,14 +3,14 @@
     <div class="text-muted">Für diese Aufgabe sind Abnahmetests hinterlegt.
     </div>
     <div class="mt-3">
-        <a *ngIf="!acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'primary'">
+        <button *ngIf="!acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'primary'">
             <nb-icon icon="play-circle-outline" pack="eva"></nb-icon> <span
                 class="d-none d-lg-block d-xl-block">Ausführen</span>
-        </a>
-        <a *ngIf="acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'basic'" class="ml-2">
+        </button>
+        <button *ngIf="acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'basic'" class="ml-2">
             <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-lg-block d-xl-block">Erneut
                 ausführen</span>
-        </a>
+        </button>
     </div>
 </div>
 <div class="mt-4">
diff --git a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html
index e8ee829bc763138e3755daf1ae8bbeb61c394167..a8c89b4527e76a6d19ab441fc4e508f05f2f10ae 100644
--- a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html
+++ b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html
@@ -9,7 +9,7 @@
             <a *ngIf="feedback.actions?.retry?.enabled" nbButton
                 [routerLink]="['/', 'tasks', solution.task.id]"
                 [status]="feedback.actions.retry.status" class="ml-2">
-                <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-lg-block d-xl-block">Nochmal
+                <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-sm-block d-md-block d-lg-block d-xl-block">Nochmal
                     versuchen</span>
             </a>
         </div>
@@ -17,13 +17,13 @@
             <a *ngIf="feedback.actions?.retry?.enabled" nbButton
                 [routerLink]="['/', 'exercises', solution.exercise.id, 'tasks', solution.task.id]"
                 [status]="feedback.actions.retry.status" class="ml-2">
-                <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-lg-block d-xl-block">Nochmal
+                <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-sm-block d-md-block d-lg-block d-xl-block">Nochmal
                     versuchen</span>
             </a>
             <a *ngIf="feedback.actions?.next?.enabled && nextTaskInstance" nbButton
                 [routerLink]="['/', 'exercises', solution.exercise.id, 'tasks', nextTaskInstance.task.id]"
                 [status]="feedback.actions.next.status" class="ml-2">
-                <span class="d-none d-lg-block d-xl-block">Nächste Aufgabe</span>
+                <span class="d-none d-sm-block d-md-block d-lg-block d-xl-block">Nächste Aufgabe</span>
                 <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon>
             </a>
         </div>
diff --git a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss
index 7dd154e2cce3e5ed59d39d13fb35e19bee107596..2fc0fd2c6467475c201c9e81bbd450dbacb10d59 100644
--- a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss
+++ b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss
@@ -5,7 +5,7 @@
     align-items: center;
 
     .content {
-        width: 800px;
+        max-width: 800px;
     }
 
     .emoji {
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.html b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.html
new file mode 100755
index 0000000000000000000000000000000000000000..e192c078dab6ddf84be07f35cecfcbfeefca0e5c
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.html
@@ -0,0 +1,29 @@
+<div class="browser-container h-100">
+  <nb-form-field class="p-2" style="flex-grow: 1">
+    <nb-icon nbPrefix icon="search-outline" pack="eva"></nb-icon>
+    <input type="text" (ngModelChange)="filterTree($event)" [ngModel]="query" nbInput fullWidth>
+  </nb-form-field>
+  <div class="table-wrapper scroll">
+    <table [nbTreeGrid]="data" equalColumnsWidth>
+
+      <tr nbTreeGridRow *nbTreeGridRowDef="let row; columns: allColumns" [class.selected]="row.data == selectedTree">
+      </tr>
+
+      <ng-container [nbTreeGridColumnDef]="customColumn">
+        <td nbTreeGridCell *nbTreeGridCellDef="let row" (click)="onTreeClicked(row.data)">
+
+          <nb-tree-grid-row-toggle [expanded]="row.expanded" *ngIf="row.data.children.length !== 0">
+          </nb-tree-grid-row-toggle>
+
+          {{row.data.name}}
+
+        </td>
+      </ng-container>
+
+      <ng-container *ngFor="let column of defaultColumns" [nbTreeGridColumnDef]="column">
+        <td nbTreeGridCell *nbTreeGridCellDef="let row">{{row.data[column]}}</td>
+      </ng-container>
+
+    </table>
+  </div>
+</div>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.scss
new file mode 100755
index 0000000000000000000000000000000000000000..6a3bcd4eb90d817a998526647f0a0ed6adcf9829
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.scss
@@ -0,0 +1,17 @@
+@use 'themes' as *;
+
+td {
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    cursor: pointer;
+}
+
+.browser-container {
+    background-color: nb-theme(background-basic-color-1)
+}
+
+.table-wrapper {
+    overflow-y: auto;
+    height: 100%;
+}
\ No newline at end of file
diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.spec.ts
similarity index 100%
rename from subato-web/src/app/shared/components/file-browser/file-browser.component.spec.ts
rename to subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.spec.ts
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.ts
new file mode 100755
index 0000000000000000000000000000000000000000..3424a1e6e84caf72e3ecd56b3a12e79baa099363
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.ts
@@ -0,0 +1,76 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { BaseComponent } from 'app/shared/components/base/base.component';
+import { FileTree } from 'app/shared/models/file-tree';
+import { Subject } from 'rxjs';
+import { debounceTime, takeUntil } from 'rxjs/operators';
+
+interface TreeNode<T> {
+  data: T;
+  children?: TreeNode<T>[];
+  expanded?: boolean;
+}
+
+@Component({
+  selector: 'ngx-file-browser',
+  templateUrl: './file-browser.component.html',
+  styleUrls: ['./file-browser.component.scss']
+})
+export class FileBrowserComponent extends BaseComponent implements OnInit {
+  @Output() treeSelectedEvent = new EventEmitter<FileTree>();
+  @Input() tree: FileTree;
+  filteredTree: FileTree;
+
+  data: TreeNode<FileTree>[] = [];
+
+  customColumn = 'filename';
+  defaultColumns = [];
+  allColumns = [this.customColumn, ...this.defaultColumns];
+
+  selectedTree: FileTree | null = null;
+
+  query: string;
+  private queryChanged: Subject<string> = new Subject<string>();
+
+  constructor() {
+    super();
+   }
+
+  private treeNodeFor(tree: FileTree, expanded: boolean = false): TreeNode<FileTree> {
+    return {
+      data: tree,
+      children: tree.children.map(child => this.treeNodeFor(child)),
+      expanded: expanded
+    };
+  }
+
+  setFilteredTree(val: FileTree, expand: boolean = false) {
+    this.filteredTree = val;
+    this.data = this.filteredTree?.children?.map(tree => this.treeNodeFor(tree, expand)) ?? [];
+  }
+
+  ngOnInit(): void {
+    this.setFilteredTree(this.tree);
+
+    this.queryChanged
+      .pipe(
+        takeUntil(this.unsubscribe),
+        debounceTime(300),
+      )
+      .subscribe(query => {
+        this.setFilteredTree(this.tree.filterByName(query), true);
+      });
+  }
+
+  onTreeClicked(tree: FileTree) {
+    if (tree.children.length === 0) {
+      this.selectedTree = tree;
+
+      this.treeSelectedEvent.emit(tree);
+    }
+  }
+
+  filterTree(query) {
+    this.query = query;
+    this.queryChanged.next(query);
+  }
+}
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.html b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..5c574dd23488bec67be8d707364b5aad08f764f3
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.html
@@ -0,0 +1,6 @@
+<nb-alert class="mb-0 nothing-here">
+    <div class="content">
+        <nb-icon icon="alert-circle-outline" class="text-danger mr-1"></nb-icon>
+        Datei konnte nicht geladen werden
+    </div>
+</nb-alert>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..61d60c3eecdd12ba497c5edd15431fa04d1d1e21
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.scss
@@ -0,0 +1,11 @@
+.nothing-here {
+    text-align: center;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    
+    .content {
+        max-width: 800px;
+    }
+
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e5aca3956807230eae6562c8132c003d32c3e655
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { IdeErrorComponent } from './ide-error.component';
+
+describe('IdeErrorComponent', () => {
+  let component: IdeErrorComponent;
+  let fixture: ComponentFixture<IdeErrorComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ IdeErrorComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(IdeErrorComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4422a494d5e5523d2d034233874b47dbf48e5184
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'ngx-ide-error',
+  templateUrl: './ide-error.component.html',
+  styleUrls: ['./ide-error.component.scss']
+})
+export class IdeErrorComponent implements OnInit {
+
+  constructor() { }
+
+  ngOnInit(): void {
+  }
+
+}
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide.module.ts b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a9207fa6070271c6c4fff41d3e4e58344a10b200
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide.module.ts
@@ -0,0 +1,34 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SolutionIdeComponent } from './solution-ide/solution-ide.component';
+import { ViewerHostDirective } from './viewer/viewer-host.directive';
+import { PdfViewerComponent } from './viewer/pdf-viewer/pdf-viewer.component';
+import { SourceCodeViewerComponent } from './viewer/source-code-viewer/source-code-viewer.component';
+import { FileBrowserComponent } from './file-browser/file-browser.component';
+import { NothingViewerComponent } from './viewer/nothing-viewer/nothing-viewer.component';
+import { SharedModule } from 'app/shared/shared.module';
+import { AngularSplitModule } from 'angular-split';
+import { DefaultViewerComponent } from './viewer/default-viewer/default-viewer.component';
+import { IdeErrorComponent } from './ide-error/ide-error.component';
+
+@NgModule({
+  declarations: [
+    SolutionIdeComponent,
+    ViewerHostDirective,
+    PdfViewerComponent,
+    SourceCodeViewerComponent,
+    FileBrowserComponent,
+    NothingViewerComponent,
+    DefaultViewerComponent,
+    IdeErrorComponent,
+  ],
+  imports: [
+    CommonModule,
+    SharedModule,
+    AngularSplitModule
+  ],
+  exports: [
+    SolutionIdeComponent
+  ]
+})
+export class SolutionIdeModule { }
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.html b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..1ed5f5243207ff2838176389ae058e312c6d8ca5
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.html
@@ -0,0 +1,42 @@
+<div class="w-100" [class.with-border]="withBorder" maximize #maximize="maximize">
+    <div class="top-nav w-100">
+        <a *ngIf="fileBrowserEnabled" (click)="toggleMenu()" nbButton [status]="'basic'" class="mr-3">
+            <nb-icon icon="menu-outline" pack="eva"></nb-icon>
+        </a>
+        <div class="ml-1">
+            <span class="title" *ngIf="currentTree">
+                {{ currentTree.name }}
+            </span>
+        </div>
+        <div class="actions ml-auto">
+            <button (click)="downloadSolution()" nbButton [status]="'basic'">
+                <nb-icon icon="download-outline" pack="eva"></nb-icon>
+                <span class="d-none d-md-block d-lg-block d-xl-block">Lösung herunterladen</span>
+            </button>
+            <button *ngIf="!(maximize.isMaximized$ | async)" (click)="maximize.maximize()" nbButton
+                [status]="'primary'">
+                <nb-icon icon="expand-outline" pack="eva"></nb-icon>
+                <span class="d-none d-md-block d-lg-block d-xl-block">Maximieren</span>
+            </button>
+            <button *ngIf="(maximize.isMaximized$ | async)" (click)="maximize.minimize()" nbButton [status]="'primary'">
+                <nb-icon icon="collapse-outline" pack="eva"></nb-icon>
+                <span class="d-none d-md-block d-lg-block d-xl-block">Minimieren</span>
+            </button>
+        </div>
+    </div>
+    <div class="split-container">
+        <as-split [gutterSize]="compactMode ? 0 : 11" direction="horizontal">
+            <as-split-area [size]="browserWidth">
+                <div class="h-100">
+                    <ngx-file-browser [tree]="solution.tree" (treeSelectedEvent)="onTreeSelected($event)">
+                    </ngx-file-browser>
+                </div>
+            </as-split-area>
+            <as-split-area [size]="100-browserWidth" [class.overflow-hidden]="currentViewer == Viewer.pdf">
+                <ngx-ide-error *ngIf="!loadingViewer && viewerError"></ngx-ide-error>
+                <ngx-loading-screen [loading]="loadingViewer"></ngx-loading-screen>
+                <ng-template viewerHost></ng-template>
+            </as-split-area>
+        </as-split>
+    </div>
+</div>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9815b9ae48e9a4b852f44c5b70810c234de8a055
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.scss
@@ -0,0 +1,32 @@
+@use 'themes' as *;
+
+.fullscreen .split-container {
+    height: 100vh;
+}
+
+.split-container {
+    height: 600px;
+}
+
+.top-nav {
+    display: flex;
+    align-items: center;
+    padding: 15px 25px;
+    background-color: nb-theme(background-basic-color-1);
+    z-index: 10;
+    box-shadow: nb-theme(header-shadow);
+    position: relative;
+
+    .actions {
+        gap: 15px; 
+        display:flex
+    }
+}
+
+.split-container {
+    background-color: nb-theme(background-basic-color-1);
+}
+
+.with-border {
+    border: nb-theme(card-border-width) solid nb-theme(card-border-color);
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7af74a4ab6c7fe9da989ac742c9323e196fe6bcb
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SolutionIdeComponent } from './solution-ide.component';
+
+describe('SolutionIdeComponent', () => {
+  let component: SolutionIdeComponent;
+  let fixture: ComponentFixture<SolutionIdeComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ SolutionIdeComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(SolutionIdeComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc1bb4850a6a712ecaf4edafa99c9f7f9eeca295
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.ts
@@ -0,0 +1,119 @@
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { BaseComponent } from 'app/shared/components/base/base.component';
+import { FileTree } from 'app/shared/models/file-tree';
+import { take, takeUntil } from 'rxjs/operators';
+import { NotificationService } from 'app/shared/services/notification.service';
+import { BackendError } from 'app/shared/models/backend-error';
+import { Solution } from 'app/features/solutions/models/solution';
+import { SolutionService } from 'app/features/solutions/services/solution.service';
+import { ViewerHostDirective } from '../viewer/viewer-host.directive';
+import { ViewerFactory } from '../viewer/viewer.factory';
+import { Viewer } from '../viewer/viewer';
+
+@Component({
+  selector: 'solution-ide',
+  templateUrl: './solution-ide.component.html',
+  styleUrls: ['./solution-ide.component.scss']
+})
+export class SolutionIdeComponent extends BaseComponent implements OnInit {
+  @Input() withBorder: boolean = false;
+
+  loading: boolean;
+  currentTree: FileTree;
+  _solution: Solution;
+  _compactMode: boolean = false;
+  fileBrowserEnabled: boolean = false;
+
+  Viewer = Viewer;
+  currentViewer: Viewer = Viewer.none;
+
+  defaultBrowserWidth: number = 15;
+  browserWidth: number = this.defaultBrowserWidth;
+  loadingViewer: boolean = false;
+  viewerError: any;
+
+  @ViewChild(ViewerHostDirective, { static: true }) viewerHost!: ViewerHostDirective;
+
+  constructor(
+    private solutionService: SolutionService,
+    private notificationService: NotificationService,
+    private viewerFactory: ViewerFactory
+  ) {
+    super();
+  }
+
+  ngOnInit(): void {
+  }
+
+  @Input() set solution(val: Solution) {
+    this._solution = val;
+    if (val.tree.children.length != 0) {
+      let first = val.tree.children[0]
+      this.compactMode = val.tree.children.length == 1 && first.children.length == 0;
+      this.currentTree = this.compactMode ? first : null;
+      this.fileBrowserEnabled = !this.compactMode;
+    }
+
+    this.createViewer();
+  }
+
+  get solution() {
+    return this._solution;
+  }
+
+  get compactMode(): boolean {
+    return this._compactMode;
+  }
+
+  set compactMode(val: boolean) {
+    this._compactMode = val;
+    this.browserWidth = this.compactMode ? 0 : this.defaultBrowserWidth;
+  }
+
+  createViewer(): void {
+    const viewContainerRef = this.viewerHost.viewContainerRef;
+    viewContainerRef.clear();
+    this.loadingViewer = true;
+
+    this.viewerFactory.createViewer(this.solution, this.currentTree, viewContainerRef)
+      .pipe(
+        take(1)
+      )
+      .subscribe(viewer => {
+        this.currentViewer = viewer;
+        this.loadingViewer = false;
+      }, error => {
+        this.viewerError = error;
+        this.loadingViewer = false;
+      });
+  }
+
+  onTreeSelected(tree: FileTree) {
+    if (tree.children.length != 0) {
+      return;
+    }
+
+    this.currentTree = tree;
+    this.createViewer();
+  }
+
+  downloadSolution() {
+    this.solutionService.downloadSolution(this.solution.id)
+      .pipe(takeUntil(this.unsubscribe))
+      .subscribe(() => { }, (error: any) => {
+        if (BackendError.isBackendError(error)) {
+          this.notificationService.showError(
+            error.format(
+              "Fehler beim Herunterladen der Lösung",
+            )
+          );
+        } else {
+          throw error;
+        }
+      });
+  }
+
+  toggleMenu() {
+    this.compactMode = !this.compactMode;
+  }
+}
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..6e05b942b470dc8f618352a04d5ea22906f9f011
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.html
@@ -0,0 +1,15 @@
+<nb-alert class="mb-0 nothing-here">
+    <div class="content">
+        <div class="msg">Anzeige nicht möglich</div>
+        <div class="text-muted mt-1">
+            Diese Datei können wir hier nicht anzeigen.
+            Du kannst diese Datei aber herunterladen.
+        </div>
+        <div class="mt-3">
+            <button nbButton (click)="download()" [status]="'basic'">
+                <nb-icon icon="download-outline" pack="eva"></nb-icon>
+                <span class="d-none d-md-block d-lg-block d-xl-block">{{ tree.name }}</span>
+            </button>
+        </div>
+    </div>
+</nb-alert>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d90a5c702e4e30a5c967792ae7a44bf8b18459a7
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.scss
@@ -0,0 +1,14 @@
+.nothing-here {
+    text-align: center;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    
+    .content {
+        max-width: 800px;
+    }
+
+    .msg {
+        font-size: 20px;
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..883e7ac817ddfd9fcd8b13ab421bfb00b8d5ff2a
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DefaultViewerComponent } from './default-viewer.component';
+
+describe('DefaultViewerComponent', () => {
+  let component: DefaultViewerComponent;
+  let fixture: ComponentFixture<DefaultViewerComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ DefaultViewerComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(DefaultViewerComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e7d6ccee5b646e99886cde5f142c36f4fc2bb835
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.ts
@@ -0,0 +1,46 @@
+import { Component, OnInit } from '@angular/core';
+import { Solution } from 'app/features/solutions/models/solution';
+import { SolutionService } from 'app/features/solutions/services/solution.service';
+import { BaseComponent } from 'app/shared/components/base/base.component';
+import { BackendError } from 'app/shared/models/backend-error';
+import { FileTree } from 'app/shared/models/file-tree';
+import { NotificationService } from 'app/shared/services/notification.service';
+import { takeUntil } from 'rxjs/operators';
+
+@Component({
+  selector: 'ngx-default-viewer',
+  templateUrl: './default-viewer.component.html',
+  styleUrls: ['./default-viewer.component.scss']
+})
+export class DefaultViewerComponent extends BaseComponent implements OnInit {
+  tree: FileTree;
+  solution: Solution;
+
+  constructor(
+    private solutionService: SolutionService,
+    private notificationService: NotificationService
+    ) {
+    super();
+  }
+
+  ngOnInit(): void {
+  }
+
+  download() {
+    this.solutionService.downloadFile(this.solution.id, this.tree)
+    .pipe(takeUntil(this.unsubscribe))
+    .subscribe(() => {}, (error: any) => {
+      if (BackendError.isBackendError(error)) {
+        this.notificationService.showError(
+          error.format(
+            "Fehler beim Herunterladen der Lösung",
+          )
+        );
+      } else {
+        throw error;
+      }
+    });
+
+  }
+
+}
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.factory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d700333082a50e941ec9b7536348c6738674294d
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.factory.ts
@@ -0,0 +1,29 @@
+import { Injectable, ViewContainerRef } from "@angular/core";
+import { Solution } from "app/features/solutions/models/solution";
+import { SolutionService } from "app/features/solutions/services/solution.service";
+import { BaseComponent } from "app/shared/components/base/base.component";
+import { BackendError } from "app/shared/models/backend-error";
+import { FileTree } from "app/shared/models/file-tree";
+import { NotificationService } from "app/shared/services/notification.service";
+import { Observable, of } from "rxjs";
+import { takeUntil } from "rxjs/operators";
+import { DefaultViewerComponent } from "./default-viewer.component";
+
+@Injectable({
+    providedIn: 'root'
+})
+export class DefaultViewerFactory extends BaseComponent {
+    constructor(
+        private solutionService: SolutionService,
+        private notificationService: NotificationService
+    ) {
+        super();
+    }
+
+    createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<void> {
+        const componentRef = containerRef.createComponent<DefaultViewerComponent>(DefaultViewerComponent);
+        componentRef.instance.tree = tree;
+        componentRef.instance.solution = solution;
+        return of(null);
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..2b8761ab671da56e72986d8aa26107734d6f1f9f
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.html
@@ -0,0 +1,8 @@
+<nb-alert class="mb-0 nothing-here">
+    <div class="content">
+        <div class="msg">Nichts ausgewählt.</div>
+        <div class="text-muted mt-1">
+            Die Lösung beinhaltet mehrere Dateien, wähle eine aus.
+        </div>
+    </div>
+</nb-alert>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d90a5c702e4e30a5c967792ae7a44bf8b18459a7
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.scss
@@ -0,0 +1,14 @@
+.nothing-here {
+    text-align: center;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    
+    .content {
+        max-width: 800px;
+    }
+
+    .msg {
+        font-size: 20px;
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..abc7ab5de780968751542e52fba1ec08c91b1baf
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'ngx-nothing-viewer',
+  templateUrl: './nothing-viewer.component.html',
+  styleUrls: ['./nothing-viewer.component.scss']
+})
+export class NothingViewerComponent implements OnInit {
+
+  constructor() { }
+
+  ngOnInit(): void {
+  }
+
+}
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.factory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7bd3b2646d6da4f954562f9e6a5e79883ebc17e1
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.factory.ts
@@ -0,0 +1,20 @@
+import { Injectable, ViewContainerRef } from "@angular/core";
+import { Solution } from "app/features/solutions/models/solution";
+import { BaseComponent } from "app/shared/components/base/base.component";
+import { FileTree } from "app/shared/models/file-tree";
+import { Observable, of } from "rxjs";
+import { NothingViewerComponent } from "./nothing-viewer.component";
+
+@Injectable({
+    providedIn: 'root'
+})
+export class NothingViewerFactory extends BaseComponent {
+    constructor() {
+        super();
+    }
+
+    createViewer(containerRef: ViewContainerRef): Observable<void> {
+        containerRef.createComponent<NothingViewerComponent>(NothingViewerComponent);
+        return of(null);
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..30d8b3f67f8bd97ee5d5ec8e525cf8a4a304b85e
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.html
@@ -0,0 +1 @@
+<embed *ngIf="pdfEmbedUrl" [src]="pdfEmbedUrl" type="application/pdf">
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..137e8da861d68caa560752d96d9b452342aa3ca9
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.scss
@@ -0,0 +1,4 @@
+embed {
+    height: 100%;
+    width: 100%;
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..892cc111b9645d326759f10483195045d0fda4df
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PdfViewerComponent } from './pdf-viewer.component';
+
+describe('PdfViewerComponent', () => {
+  let component: PdfViewerComponent;
+  let fixture: ComponentFixture<PdfViewerComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ PdfViewerComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(PdfViewerComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ab152ab493d52f509f6d840dd52630da180047f2
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.ts
@@ -0,0 +1,21 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
+
+@Component({
+  selector: 'ngx-pdf-viewer',
+  templateUrl: './pdf-viewer.component.html',
+  styleUrls: ['./pdf-viewer.component.scss']
+})
+export class PdfViewerComponent implements OnInit {
+  @Input() url: string;
+  pdfEmbedUrl: SafeResourceUrl;
+
+  constructor(
+    private sanitizer: DomSanitizer
+  ) { }
+
+  ngOnInit(): void {
+    this.pdfEmbedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.url + '#view=FitH');
+  }
+
+}
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.factory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..778996ec29b162db1ee33a7c588160a51dba3bd5
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.factory.ts
@@ -0,0 +1,44 @@
+import { Injectable, ViewContainerRef } from "@angular/core";
+import { Solution } from "app/features/solutions/models/solution";
+import { SolutionService } from "app/features/solutions/services/solution.service";
+import { BaseComponent } from "app/shared/components/base/base.component";
+import { BackendError } from "app/shared/models/backend-error";
+import { FileTree } from "app/shared/models/file-tree";
+import { NotificationService } from "app/shared/services/notification.service";
+import { Observable } from "rxjs";
+import { catchError, map, takeUntil } from "rxjs/operators";
+import { PdfViewerComponent } from "./pdf-viewer.component";
+
+@Injectable({
+    providedIn: 'root'
+})
+export class PdfViewerFactory extends BaseComponent {
+    constructor(
+        private solutionService: SolutionService,
+        private notificationService: NotificationService
+    ) {
+        super();
+    }
+
+    createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<void> {
+        return this.solutionService.getFileAsUrl(solution.id, tree)
+            .pipe(
+                takeUntil(this.unsubscribe),
+                catchError((error: any) => {
+                    if (BackendError.isBackendError(error)) {
+                        this.notificationService.showError(
+                            error.format(
+                                "Fehler beim Herunterladen der Lösung",
+                            )
+                        );
+                    }
+
+                    throw error;
+                }),
+                map(([url, fileName]) => {
+                    const componentRef = containerRef.createComponent<PdfViewerComponent>(PdfViewerComponent);
+                    componentRef.instance.url = url;
+                }),
+            );
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.html
new file mode 100755
index 0000000000000000000000000000000000000000..41a9be882da89c9c47b6a31559cfd8e4ea72a22e
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.html
@@ -0,0 +1,10 @@
+<div *ngIf="file" class="h-100">
+    
+            <ngx-codemirror class="fillh" [(ngModel)]="file.content" [options]="{
+                    lineNumbers: true,
+                    mode: file.tree.contentType | codemirrorMode,
+                    theme: darkMode | codemirrorTheme,
+                    readOnly: 'nocursor'
+                  }"></ngx-codemirror>
+
+</div>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.scss
new file mode 100755
index 0000000000000000000000000000000000000000..2435b40a30b3a890650d115deaa388758e7f0c49
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.scss
@@ -0,0 +1,8 @@
+@use 'themes' as *;
+
+nb-card {
+    height: 100%;
+    border: 0px;
+    border-radius: 0 !important;
+    background-color: nb-theme(background-basic-color-1)
+}
\ No newline at end of file
diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.spec.ts
similarity index 100%
rename from subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.spec.ts
rename to subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.spec.ts
diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.ts
similarity index 75%
rename from subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.ts
rename to subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.ts
index e6f01f94abcfcc17f2ef20bc3c59f2e4f14bdbf6..0c341cae82acd7e335669e14d5d650029991f6ba 100755
--- a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.ts
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.ts
@@ -5,8 +5,8 @@ import { FileTree } from 'app/shared/models/file-tree';
 import { AppStateService } from 'app/core/services/app.state.service';
 
 export interface File {
-  reference: FileTree;
-  content?: string | Blob;
+  tree: FileTree;
+  content?: string;
 }
 
 @Component({
@@ -16,8 +16,6 @@ export interface File {
 })
 export class SourceCodeViewerComponent extends BaseComponent implements OnInit {
   @Input() file: File;
-  @Output() downloadTreeEvent = new EventEmitter<FileTree>();
-  @Output() toggleEvent = new EventEmitter<void>();
   darkMode = false;
 
   constructor(
@@ -31,12 +29,4 @@ export class SourceCodeViewerComponent extends BaseComponent implements OnInit {
       .pipe(takeUntil(this.unsubscribe))
       .subscribe(darkMode => this.darkMode = darkMode);
   }
-
-  open() {
-    this.downloadTreeEvent.emit(this.file.reference);
-  }
-
-  toggle() {
-    this.toggleEvent.emit();
-  }
 }
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.factory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..42a16a4173d6035240d1852c376cb000e61a3347
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.factory.ts
@@ -0,0 +1,49 @@
+import { Injectable, ViewContainerRef } from "@angular/core";
+import { Solution } from "app/features/solutions/models/solution";
+import { SolutionService } from "app/features/solutions/services/solution.service";
+import { BaseComponent } from "app/shared/components/base/base.component";
+import { BackendError } from "app/shared/models/backend-error";
+import { FileTree } from "app/shared/models/file-tree";
+import { NotificationService } from "app/shared/services/notification.service";
+import { Observable } from "rxjs";
+import { catchError, map, takeUntil } from "rxjs/operators";
+import { SourceCodeViewerComponent } from "./source-code-viewer.component";
+
+@Injectable({
+    providedIn: 'root'
+})
+export class SourceCodeViewerFactory extends BaseComponent {
+    constructor(
+        private solutionService: SolutionService,
+        private notificationService: NotificationService
+    ) {
+        super();
+    }
+
+    createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<void> {
+        return this.solutionService.getFile(solution.id, tree)
+            .pipe(
+                takeUntil(this.unsubscribe),
+                catchError((error: any) => {
+                    if (BackendError.isBackendError(error)) {
+                        this.notificationService.showError(
+                            error.format(
+                                "Fehler beim Herunterladen der Lösung",
+                            )
+                        );
+                    } 
+
+                    throw error;
+                }),
+                map(content => {
+                    let file = {
+                        tree: tree,
+                        content: content
+                    };
+    
+                    const componentRef = containerRef.createComponent<SourceCodeViewerComponent>(SourceCodeViewerComponent);
+                    componentRef.instance.file = file;
+                })
+            );
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer-host.directive.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer-host.directive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..518c2a268fe1e3ad350b766c3bbc34575d528541
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer-host.directive.ts
@@ -0,0 +1,8 @@
+import { Directive, ViewContainerRef } from '@angular/core';
+
+@Directive({
+  selector: '[viewerHost]',
+})
+export class ViewerHostDirective {
+  constructor(public viewContainerRef: ViewContainerRef) { }
+}
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.factory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0fdfff49d60640091fdd5736ea293f51fc070286
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.factory.ts
@@ -0,0 +1,71 @@
+import { Injectable, ViewContainerRef } from "@angular/core";
+import { Solution } from "app/features/solutions/models/solution";
+import { BaseComponent } from "app/shared/components/base/base.component";
+import { FileTree } from "app/shared/models/file-tree";
+import { Observable } from "rxjs";
+import { map } from "rxjs/operators";
+import { DefaultViewerFactory } from "./default-viewer/default-viewer.factory";
+import { NothingViewerFactory } from "./nothing-viewer/nothing-viewer.factory";
+import { PdfViewerFactory } from "./pdf-viewer/pdf-viewer.factory";
+import { SourceCodeViewerFactory } from "./source-code-viewer/source-code-viewer.factory";
+import { Viewer } from "./viewer";
+
+@Injectable({
+    providedIn: 'root'
+})
+export class ViewerFactory extends BaseComponent {
+    constructor(
+        private sourceCodeViewerFactory: SourceCodeViewerFactory,
+        private pdfViewerFactory: PdfViewerFactory,
+        private nothingViewerFactory: NothingViewerFactory,
+        private defaultViewerFactory: DefaultViewerFactory,
+    ) {
+        super();
+    }
+
+    private getViewer(tree: FileTree): Viewer {
+        if (tree == null || tree.directory) {
+            return Viewer.none;
+        }
+
+        if (tree.src) {
+            return Viewer.sourceCode;
+        }
+
+        if (!tree.src && tree.isPdf) {
+            return Viewer.pdf;
+        }
+
+        return Viewer.default;
+    }
+
+    createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<Viewer> {
+
+        let viewer = this.getViewer(tree);
+        let create$: Observable<void>;
+
+        if (viewer == Viewer.none) {
+            create$ = this.nothingViewerFactory.createViewer(containerRef);
+        }
+
+        else if (viewer == Viewer.pdf) {
+            create$ = this.pdfViewerFactory.createViewer(solution, tree, containerRef);
+        }
+
+        else if (viewer == Viewer.sourceCode) {
+            create$ = this.sourceCodeViewerFactory.createViewer(solution, tree, containerRef);
+        }
+
+        else if (viewer == Viewer.default) {
+            create$ = this.defaultViewerFactory.createViewer(solution, tree, containerRef);
+        }
+
+        else {
+            throw `no factory for viewer ${viewer}`;
+        }
+
+        return create$.pipe(
+            map(() => viewer)
+        );
+    }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7cf4a1abae00a27097920f9af78a8c4e793da31b
--- /dev/null
+++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.ts
@@ -0,0 +1,6 @@
+export enum Viewer {
+    none,
+    default,
+    sourceCode,
+    pdf
+}
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.html
deleted file mode 100644
index c4ad412dbf48ecf520f8808261a28cbb294db7ef..0000000000000000000000000000000000000000
--- a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="row">
-    <div *ngIf="!compactMode" class="col-12 col-lg-3">
-        <ngx-file-browser [tree]="solution.tree" (treeSelectedEvent)="onTreeSelected($event)"></ngx-file-browser>
-    </div>
-    <div class="col-12" [class.col-lg-9]="!compactMode" [class.col-lg-12]="compactMode">
-        <span *ngIf="!currentTree">Nichts ausgewählt</span>
-        <ngx-source-code-viewer *ngIf="currentTree && viewer == FileViewer.sourceCode" [file]="currentFile" (toggleEvent)="compactMode = !compactMode" (downloadTreeEvent)="onDownloadTree($event)"></ngx-source-code-viewer>
-        <embed *ngIf="currentTree && viewer == FileViewer.pdf && pdfEmbedUrl" [src]="pdfEmbedUrl" style="height: 600px;width: 100%;" type="application/pdf">
-        <span *ngIf="currentTree && viewer == FileViewer.none">Diese Datei kann nicht dargestellt werden.</span>
-    </div>
-</div>
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.spec.ts
deleted file mode 100644
index 7fca4c23cd87556c0b1d75a06a948f7fee4a304f..0000000000000000000000000000000000000000
--- a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { SolutionViewerComponent } from './solution-viewer.component';
-
-describe('SolutionViewerComponent', () => {
-  let component: SolutionViewerComponent;
-  let fixture: ComponentFixture<SolutionViewerComponent>;
-
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
-      declarations: [ SolutionViewerComponent ]
-    })
-    .compileComponents();
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(SolutionViewerComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.ts
deleted file mode 100644
index 74648e8f53b42a69549f08d5c44ea90c24902571..0000000000000000000000000000000000000000
--- a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { Component, Input, OnInit } from '@angular/core';
-import { BaseComponent } from 'app/shared/components/base/base.component';
-import { FileTree } from 'app/shared/models/file-tree';
-import { takeUntil } from 'rxjs/operators';
-import { SolutionService } from '../../services/solution.service';
-import { File } from 'app/shared/components/source-code-viewer/source-code-viewer.component';
-import { NotificationService } from 'app/shared/services/notification.service';
-import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
-import { BackendError } from 'app/shared/models/backend-error';
-import { Solution } from '../../models/solution';
-
-enum FileViewer {
-  sourceCode,
-  pdf,
-  none
-}
-
-@Component({
-  selector: 'solution-viewer',
-  templateUrl: './solution-viewer.component.html',
-  styleUrls: ['./solution-viewer.component.scss']
-})
-export class SolutionViewerComponent extends BaseComponent implements OnInit {
-  loading: boolean;
-  currentTree: FileTree;
-  currentFile: File;
-  _solution: Solution;
-  compactMode: boolean = false;
-
-  FileViewer = FileViewer;
-  pdfEmbedUrl: SafeResourceUrl;
-
-  constructor(
-    private solutionService: SolutionService,
-    private notificationService: NotificationService,
-    private sanitizer: DomSanitizer
-  ) {
-    super();
-  }
-
-  ngOnInit(): void {
-  }
-
-  @Input() set solution(val: Solution) {
-    this._solution = val;
-    if (this._solution.tree.children.length != 0) {
-      let first = this._solution.tree.children[0]
-      this.compactMode = val.tree.children.length == 1 && first.children.length == 0;
-      this.onTreeSelected(first);
-    }
-  }
-
-  get solution() {
-    return this._solution;
-  }
-
-  get viewer(): FileViewer {
-    if (this.currentTree == null) {
-      return null;
-    }
-
-    if (this.currentTree.src) {
-      return FileViewer.sourceCode;
-    }
-
-    if (!this.currentTree.src && this.currentTree.isPdf) {
-      return FileViewer.pdf;
-    }
-
-    return FileViewer.none;
-  }
-
-  onTreeSelected(tree: FileTree) {
-    if (tree.children.length != 0) {
-      return;
-    }
-    
-    this.currentTree = tree;
-    this.currentFile = {
-      reference: tree,
-      content: this.currentFile?.content
-    };
-
-    if(this.viewer == FileViewer.pdf) {
-      this.solutionService.getFileAsUrl(this.solution.id, tree)
-      .pipe(takeUntil(this.unsubscribe))
-      .subscribe(([url, fileName]) => {
-        this.pdfEmbedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url + '#view=FitH');
-      }, (error: any) => {
-        if (BackendError.isBackendError(error)) {
-          this.notificationService.showError(
-            error.format(
-              "Fehler beim Herunterladen der Lösung",
-            )
-          );
-        } else {
-          throw error;
-        }
-      });
-    }
-
-    if (this.viewer == FileViewer.sourceCode) {
-      this.solutionService.getFile(this.solution.id, tree)
-        .pipe(takeUntil(this.unsubscribe))
-        .subscribe(content => {
-          this.currentFile = {
-            reference: tree,
-            content: content
-          };
-        }, (error: any) => {
-          if (BackendError.isBackendError(error)) {
-            this.notificationService.showError(
-              error.format(
-                "Fehler beim Herunterladen der Lösung",
-              )
-            );
-          } else {
-            throw error;
-          }
-        });
-    }
-  }
-
-  onDownloadTree(tree: FileTree) {
-    this.solutionService.downloadFile(this.solution.id, tree)
-    .pipe(takeUntil(this.unsubscribe))
-    .subscribe(() => {}, (error: any) => {
-      if (BackendError.isBackendError(error)) {
-        this.notificationService.showError(
-          error.format(
-            "Fehler beim Herunterladen der Lösung",
-          )
-        );
-      } else {
-        throw error;
-      }
-    });
-  }
-}
diff --git a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html
index 44b6288bb8f1653fc5fbf3fa1a95b38333c29ca9..e366caf158bee90fa63ebd9ac70f0c83a9d23342 100644
--- a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html
+++ b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html
@@ -8,6 +8,7 @@
     <div *ngIf="selectedMode == SubmissionMode.FILE" class="form-group">
         <label for="file" class="label">Datei</label>
         <dropzone id="file" (fileChanged)="handleFile($event)"></dropzone>
+        <small>Die maximale Dateigröße beträgt {{ config.maxUploadSizeInMb }} MB.</small>
     </div>
     <div *ngIf="selectedMode == SubmissionMode.TEXT" class="form-group">
         <label for="solutionContent" class="label">Lösung</label>
diff --git a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts
index bbd2d2ab9b56edbf0d0055e2c799bb6f19d97bdd..296e55d69bc4a097fcb49d121c803d5a646a8bbe 100644
--- a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts
+++ b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts
@@ -21,6 +21,7 @@ import { Client } from '../../models/client';
 import { Solution } from '../../models/solution';
 import { SolutionService, SubmitFileSolution, SubmitInlineSolution } from '../../services/solution.service';
 import { SubatoPrincipal } from 'app/core/models/principal/subato-principal';
+import { PublicConfig } from 'app/core/models/public-config';
 
 @Component({
   selector: 'ngx-submit-solution-form',
@@ -42,6 +43,7 @@ export class SubmitSolutionFormComponent extends BaseComponent implements OnInit
   templateFile: FileRef;
   darkMode = false;
   principal: SubatoPrincipal;
+  config: PublicConfig;
 
   SubmissionMode = SubmissionMode;
 
@@ -69,6 +71,7 @@ export class SubmitSolutionFormComponent extends BaseComponent implements OnInit
     private notificationService: NotificationService,
     private fb: UntypedFormBuilder) {
     super();
+    this.config = appStateService.config;
   }
 
   ngAfterViewInit(): void {
diff --git a/subato-web/src/app/features/solutions/models/solution.ts b/subato-web/src/app/features/solutions/models/solution.ts
index f14ba31af5d365d96b34b67fc039d240c4cd1875..fc92ac7227aa97b5bff0d02e710f9dd12ca5b9aa 100755
--- a/subato-web/src/app/features/solutions/models/solution.ts
+++ b/subato-web/src/app/features/solutions/models/solution.ts
@@ -9,6 +9,7 @@ import { Iso8601Helper } from "app/shared/helper/iso8601.helper";
 import { Task } from "app/features/tasks/models/task";
 import { Exercise } from "app/features/exercises/models/exercise";
 import { Submission } from "app/features/exercises/models/submission";
+import { Client } from "./client";
 
 export class Solution {
     id: number;
@@ -23,6 +24,7 @@ export class Solution {
     attemptsExceeded: boolean;
     comments: SolutionComment[];
     commentsCount: number;
+    submittedWith: Client;
 
     constructor(solution: any) {
         this.id = solution.id;
@@ -34,6 +36,7 @@ export class Solution {
         this.evalResult = solution.evalResult ? new EvalResult(solution.evalResult) : null;
         this.submission = solution.submission ? new Submission(solution.submission) : null;
         this.delayed = solution.delayed;
+        this.submittedWith = solution.submittedWith;
         this.attemptsExceeded = solution.attemptsExceeded;
         this.commentsCount = solution.commentsCount;
         this.comments = solution.comments ? solution.comments.map(c => new SolutionComment(c)) : [];
diff --git a/subato-web/src/app/features/solutions/pages/solution/solution.component.html b/subato-web/src/app/features/solutions/pages/solution/solution.component.html
index cbabfcb445d45747c5250dea4a0e8631a27c864e..8432e0072642c66563a7fa304c8abd76e27d8b87 100755
--- a/subato-web/src/app/features/solutions/pages/solution/solution.component.html
+++ b/subato-web/src/app/features/solutions/pages/solution/solution.component.html
@@ -14,8 +14,12 @@
                            <ng-container *ngIf="!solution.exercise">{{ solution.submittedBy | userName }}</ng-container>
                         </ng-container> um {{ solution.submittedAt |
                         date:'medium' }}
+                        via 
+                        <span *ngIf="solution.submittedWith == Client.Web" class="badge badge-primary">Web</span>
+                        <span *ngIf="solution.submittedWith == Client.EclipsePlugin" class="badge badge-primary">Eclipse-Plugin</span>
                      </span>
                   </div>
+                  
                </nb-card-header>
             </nb-card>
          </div>
@@ -38,6 +42,11 @@
          Du hast nach Ende der Abgabefrist abgegeben oder die maximale Anzahl an Versuchen erreicht. Deine Lösung wird
          uns aber als nachgereicht angezeigt.
       </nb-alert>
+   
+      <div class="row">
+         <div class="col-12">
+      </div>
+      </div>
 
       <div class="row">
          <div [class.col-xl-9]="solution.exercise" class="col-12">
@@ -55,8 +64,8 @@
                         <nb-accordion-item-header>
                            Lösung
                         </nb-accordion-item-header>
-                        <nb-accordion-item-body>
-                           <solution-viewer [solution]="solution"></solution-viewer>
+                        <nb-accordion-item-body class="no-padding">
+                           <solution-ide [solution]="solution"></solution-ide>
                         </nb-accordion-item-body>
                      </nb-accordion-item>
                   </nb-accordion>
diff --git a/subato-web/src/app/features/solutions/pages/solution/solution.component.ts b/subato-web/src/app/features/solutions/pages/solution/solution.component.ts
index 4bbafda305ef005d95dd83b60c3ac97a4a66ca65..708a82f3b81eb5b2efa4b28ccb3215501ead3fbb 100755
--- a/subato-web/src/app/features/solutions/pages/solution/solution.component.ts
+++ b/subato-web/src/app/features/solutions/pages/solution/solution.component.ts
@@ -16,6 +16,7 @@ import { SubatoPrincipal } from 'app/core/models/principal/subato-principal';
 import { Solution } from '../../models/solution';
 import { SolutionService } from '../../services/solution.service';
 import { SolutionBreadcrumbBuilder } from './solution-breadcrumb-builder';
+import { Client } from '../../models/client';
 
 @Component({
   selector: 'ngx-solution',
@@ -27,6 +28,8 @@ export class SolutionComponent extends BaseComponent implements OnInit {
   taskInstances: TaskInstance[];
   principal: SubatoPrincipal;
 
+  Client = Client;
+
   loading: boolean = true;
   justSubmitted = false;
 
diff --git a/subato-web/src/app/features/solutions/services/solution.service.ts b/subato-web/src/app/features/solutions/services/solution.service.ts
index bd937940eea912c9f7c77fdbe5eb1395573dc907..66ca42b0e6b6b5608460f6c846fe2526940c4360 100755
--- a/subato-web/src/app/features/solutions/services/solution.service.ts
+++ b/subato-web/src/app/features/solutions/services/solution.service.ts
@@ -152,4 +152,8 @@ export class SolutionService extends BaseService {
     downloadFile(solutionId: number, item: FileTree): Observable<void> {
         return this.downloadService.downloadFile(this.getFileUrl(solutionId, item.uuid), true);
     }
+
+    downloadSolution(solutionId: number): Observable<void> {
+        return this.downloadService.downloadFile(`${environment.backendUrl}/solution/${solutionId}/download`, true);
+    }
 }
\ No newline at end of file
diff --git a/subato-web/src/app/features/solutions/solutions.module.ts b/subato-web/src/app/features/solutions/solutions.module.ts
index fa044425e91fa0bd208a305aff19f23c8e4bc646..1e8f812e2ed8ef4f91336e3d9a29f5639d1ec94e 100644
--- a/subato-web/src/app/features/solutions/solutions.module.ts
+++ b/subato-web/src/app/features/solutions/solutions.module.ts
@@ -8,8 +8,9 @@ import { SubmitSolutionFormComponent } from './components/submit-solution-form/s
 import { SolutionsRoutingModule } from './solutions-routing.module';
 import { SolutionComponent } from './pages/solution/solution.component';
 import { EvalResultComponent } from './components/eval-result/eval-result.component';
-import { SolutionViewerComponent } from './components/solution-viewer/solution-viewer.component';
 import { AcceptanceTestComponent } from './components/acceptance-test/acceptance-test.component';
+import { AngularSplitModule } from 'angular-split';
+import { SolutionIdeModule } from './components/solution-ide/solution-ide.module';
 
 
 @NgModule({
@@ -17,26 +18,27 @@ import { AcceptanceTestComponent } from './components/acceptance-test/acceptance
     SolutionComponent,
 
     AcceptanceTestComponent,
-    SolutionViewerComponent,
     EvalResultComponent,
     SolutionHistoryComponent,
     SolutionCommentsComponent,
     SolutionFeedbackScreenComponent,
-    SubmitSolutionFormComponent
+    SubmitSolutionFormComponent,
   ],
   imports: [
     CommonModule,
     SharedModule,
-    SolutionsRoutingModule
+    SolutionsRoutingModule,
+    AngularSplitModule,
+    SolutionIdeModule
   ],
   exports: [
     SubmitSolutionFormComponent,
-    SolutionViewerComponent,
     SubmitSolutionFormComponent,
     SolutionHistoryComponent,
     SolutionCommentsComponent,
     AcceptanceTestComponent,
-    EvalResultComponent
+    EvalResultComponent,
+    SolutionIdeModule
   ]
 })
 export class SolutionsModule { }
diff --git a/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html b/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html
index c1ef489de90d1190a075ff9eac559b389f064f83..7d1555e36738a2eb23aafb94caf69400867f5f69 100644
--- a/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html
+++ b/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html
@@ -33,11 +33,12 @@
             <nb-icon icon="edit-2-outline" pack="eva"></nb-icon>
             <span class="d-none d-md-block d-lg-block d-xl-block">Bearbeiten</span>
           </a>
-          <a *ngIf="principal && principal.hasSuperAdminRole()" nbButton class="ml-2"
+          <button *ngIf="principal && principal.hasSuperAdminRole()" nbButton class="ml-2"
+            [disabled]="deletingLanguage == lang"
             [nbSpinner]="deletingLanguage == lang" (click)="deleteLanguage(lang)" [status]="'danger'">
             <nb-icon icon="trash-2-outline" pack="eva"></nb-icon>
             <span class="d-none d-md-block d-lg-block d-xl-block">Löschen</span>
-          </a>
+          </button>
         </div>
       </nb-list-item>
     </nb-list>
diff --git a/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html b/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html
index 609e83c52764710b2d041559a2abc21eb494324a..25d1c0d9de45a7bccd245187956bcb47960a3563 100644
--- a/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html
+++ b/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html
@@ -34,9 +34,9 @@
                 </div>
             </div>
             <div class="interaction">
-                <a nbButton (click)="$event.stopPropagation(); markAsRead(fb)" [status]="'primary'">
+                <button nbButton (click)="$event.stopPropagation(); markAsRead(fb)" [status]="'primary'">
                     <nb-icon icon="checkmark-outline" pack="eva"></nb-icon> Gelesen
-                </a>
+                </button>
             </div>
         </nb-list-item>
     </nb-card-body>
diff --git a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html
index 5511fbef554195267dd0bfa702169fd3d69b3937..270872f6baaaa098080d272d3c7a32095723f429 100755
--- a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html
+++ b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html
@@ -8,7 +8,10 @@
         <span>Die Aufgabe muss als zip-Datei im <a [href]="stefDocsUrl" target="_blank">Subato Task Exchange Format (STEF)</a> hochgeladen werden. </span>
       </nb-alert>
 
-      <dropzone [file]="file" (fileChanged)="handleFile($event)"></dropzone>
+      <div class="mb-3">
+        <dropzone [file]="file" (fileChanged)="handleFile($event)"></dropzone>
+        <small>Die maximale Dateigröße beträgt {{ config.maxUploadSizeInMb }} MB.</small>
+      </div>
 
       <div style="display: flex; justify-content: space-between">
         <button nbButton [nbSpinner]="importing" [status]="'success'" [disabled]="fileForm.invalid || importing" (click)="save()">
diff --git a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts
index ad00d7020ea1917999df6d18d2894f9326a16255..7e058ce5c93c1b20852a196fb77328453b002599 100755
--- a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts
+++ b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts
@@ -11,6 +11,8 @@ import {DropzoneComponent} from 'app/shared/components/dropzone/dropzone.compone
 import { BackendError } from 'app/shared/models/backend-error';
 import { TaskPool } from '../../models/task-pool';
 import { environment } from 'environments/environment';
+import { PublicConfig } from 'app/core/models/public-config';
+import { AppStateService } from 'app/core/services/app.state.service';
 
 @Component({
   selector: 'ngx-task-upload',
@@ -26,15 +28,18 @@ export class TaskUploadComponent extends FormComponent implements OnInit {
   task: Task;
   file: File;
 
+  config: PublicConfig;
   stefDocsUrl = environment.stefDocsUrl;
 
   constructor(
     private taskPoolService: TaskPoolService,
+    private appStateService: AppStateService,
     private router: Router,
     private route: ActivatedRoute,
     private notificationService: NotificationService,
     private fb: UntypedFormBuilder) {
     super();
+    this.config = appStateService.config
   }
 
   ngOnInit(): void {
diff --git a/subato-web/src/app/features/tasks/models/task.ts b/subato-web/src/app/features/tasks/models/task.ts
index 3805f0a7750a6f267a74238b89c74dae3c069deb..84e42caba50532678756fa51f697f71151158aab 100755
--- a/subato-web/src/app/features/tasks/models/task.ts
+++ b/subato-web/src/app/features/tasks/models/task.ts
@@ -22,6 +22,7 @@ export class Task {
     feedbackStats: TaskFeedbackStats;
     acceptanceTestAvailable: boolean;
     evaluator: Evaluator;
+    repositoryUrl: string;
 
     constructor(task: any) {
         this.id = task.id;
@@ -39,5 +40,6 @@ export class Task {
         this.language = task.language ? new Language(task.language) : null;
         this.acceptanceTestAvailable = task.acceptanceTestAvailable;
         this.supportedByEclipsePlugin = task.supportedByEclipsePlugin;
+        this.repositoryUrl = task.repositoryUrl;
     }
 }
\ No newline at end of file
diff --git a/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html b/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html
index 35be0bc4fab542eac95d47802b4ba7dc8cf3d2cf..36b793631d0c29f710b92ff32408de7b678cb8d2 100755
--- a/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html
+++ b/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html
@@ -22,19 +22,19 @@
               </div>
             </div>
             <div style="align-self: center; gap: 15px" class="d-flex">
-              <a *ngIf="taskPool.linkedToRepository" nbButton [status]="'basic'" [nbSpinner]="syncing" (click)="sync()">
+              <button *ngIf="taskPool.linkedToRepository" nbButton [status]="'basic'" [disabled]="syncing" [nbSpinner]="syncing" (click)="sync()">
                 <nb-icon icon="refresh-outline" pack="eva"></nb-icon>
                 <span class="d-none d-md-block d-lg-block d-xl-block">Synchronisieren</span>
-              </a>
+              </button>
               <a nbButton [routerLink]="['edit']" [status]="'basic'">
                 <nb-icon icon="edit-2-outline" pack="eva"></nb-icon>
                 <span class="d-none d-md-block d-lg-block d-xl-block">Bearbeiten</span>
               </a>
-              <a nbButton *ngIf="principal.hasSuperAdminRole()" [nbSpinner]="deletingPool"
+              <button nbButton *ngIf="principal.hasSuperAdminRole()" [disabled]="deletingPool" [nbSpinner]="deletingPool"
                 (click)="$event.stopPropagation(); deletePool()" [status]="'danger'">
                 <nb-icon icon="trash-2-outline" pack="eva"></nb-icon>
                 <span class="d-none d-md-block d-lg-block d-xl-block">Löschen</span>
-              </a>
+              </button>
             </div>
           </nb-card-header>
         </nb-card>
@@ -50,7 +50,7 @@
     <div class="row">
       <div class="col-lg-8 col-12">
         <nb-alert *ngIf="taskPool.linkedToRepository" accent="primary">
-          <span>Dieser Aufgaben-Pool ist mit einem <a [href]="taskPool.repositoryUrl">Git-Repository</a>
+          <span>Dieser Aufgaben-Pool ist mit einem <a [href]="taskPool.repositoryUrl" target="_blank">Git-Repository</a>
             verknüpft.</span>
           <span>Für jede Aufgabe muss im Repository ein Ordner im <a [href]="stefDocsUrl" target="_blank">Subato
               Task Exchange Format (STEF)</a> angelegt werden. </span>
diff --git a/subato-web/src/app/features/tasks/pages/task/task.component.html b/subato-web/src/app/features/tasks/pages/task/task.component.html
index a64de79517461272f85693e2fe1afe04e00e59c2..8dbbef5534f6b83c49167b922811d83d0ee3bc5f 100644
--- a/subato-web/src/app/features/tasks/pages/task/task.component.html
+++ b/subato-web/src/app/features/tasks/pages/task/task.component.html
@@ -16,16 +16,16 @@
               <div *ngIf="task.description" class="mt-1 text-muted" [innerHTML]="task.description"></div>
             </div>
             <div style="align-self: center">
-              <a nbButton (click)="$event.stopPropagation(); downloadSpec()"
+              <button nbButton (click)="$event.stopPropagation(); downloadSpec()"
                 [status]="'basic'">
                 <nb-icon icon="download-outline" pack="eva"></nb-icon>
                 <span class="d-none d-md-block d-lg-block d-xl-block">Herunterladen</span>
-              </a>
-              <a nbButton [nbSpinner]="archivingTask"
+              </button>
+              <button *ngIf="!task.pool.linkedToRepository" nbButton [nbSpinner]="archivingTask" [disabled]="archivingTask"
                 (click)="$event.stopPropagation(); archiveTask()" [status]="'danger'" class="ml-2">
                 <nb-icon icon="archive-outline" pack="eva"></nb-icon>
                 <span class="d-none d-md-block d-lg-block d-xl-block">Archivieren</span>
-              </a>
+              </button>
             </div>
           </nb-card-header>
         </nb-card>
@@ -38,11 +38,23 @@
       </div>
     </div>
     
+    <div class="row">
+      <div class="col-12">
+        
+      </div>
+    </div>
+
     <div class="row">
       <div class="col-lg-8 col-12">
         <div class="row">
           <div class="col-12">
+            <nb-alert *ngIf="task.pool.linkedToRepository && task.repositoryUrl" accent="primary">
+              <span>Diese Aufgabe stammt aus einem <a [href]="task.pool.repositoryUrl" target="_blank">Git-Repository</a>
+                und kann <a [href]="task.repositoryUrl" target="_blank">hier</a> direkt eingesehen werden.</span>
+            </nb-alert>
             <ngx-task-summary [task]="task"></ngx-task-summary>
+
+
           </div>
         </div>
 
diff --git a/subato-web/src/app/features/tasks/pages/task/task.component.ts b/subato-web/src/app/features/tasks/pages/task/task.component.ts
index 584c872ed1d995a6630caf83d8e00698a3868085..076e864c3017b02cce7f9472cf4e889283d73669 100644
--- a/subato-web/src/app/features/tasks/pages/task/task.component.ts
+++ b/subato-web/src/app/features/tasks/pages/task/task.component.ts
@@ -36,7 +36,7 @@ export class TaskComponent extends BaseComponent implements OnInit {
   };
 
   breadcrumbs: Breadcrumb[] = [];
-
+  
   constructor(
     private authService: AuthService,
     private taskService: TaskService,
diff --git a/subato-web/src/app/shared/components/dropzone/dropzone.component.html b/subato-web/src/app/shared/components/dropzone/dropzone.component.html
index 89e8ef07c0aaffb5a2112e0d81e0ec8e8cf81fe5..d680f734e691c8fef266f7a07e40af89de5e9ff2 100644
--- a/subato-web/src/app/shared/components/dropzone/dropzone.component.html
+++ b/subato-web/src/app/shared/components/dropzone/dropzone.component.html
@@ -1,4 +1,4 @@
-<div class="custom-dropzone mb-3" ngx-dropzone (change)="fileSelected($event)">
+<div class="custom-dropzone mb-1" ngx-dropzone (change)="fileSelected($event)">
     <p *ngIf="file == null" class="center-font">Datei hier ablegen (oder klicken)</p>
     <div *ngIf="file != null" class="custom-preview" [removable]="true">
         <div class="custom-label">{{ file.name }} ({{ file.type }})</div>
diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.html b/subato-web/src/app/shared/components/file-browser/file-browser.component.html
deleted file mode 100755
index 8c91121b444826924c2ffe0e1e86885c078da2ec..0000000000000000000000000000000000000000
--- a/subato-web/src/app/shared/components/file-browser/file-browser.component.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<nb-card class="h-100">
-    <nb-card-body style="padding: 0px;">
-        <table [nbTreeGrid]="data" equalColumnsWidth>
-
-            <tr nbTreeGridRow *nbTreeGridRowDef="let row; columns: allColumns" style="cursor: pointer;"></tr>
-  
-            <ng-container [nbTreeGridColumnDef]="customColumn">
-              <td nbTreeGridCell *nbTreeGridCellDef="let row" (click)="onTreeClicked(row.data)" style="cursor: pointer;">
-  
-                <nb-tree-grid-row-toggle
-                  [expanded]="row.expanded"
-                  *ngIf="row.data.children.length !== 0">
-                </nb-tree-grid-row-toggle>
-  
-                {{row.data.name}}
-  
-              </td>
-            </ng-container>
-  
-            <ng-container *ngFor="let column of defaultColumns" [nbTreeGridColumnDef]="column">
-              <td nbTreeGridCell *nbTreeGridCellDef="let row">{{row.data[column]}}</td>
-            </ng-container>
-  
-          </table>
-    </nb-card-body>
-</nb-card>
\ No newline at end of file
diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.scss b/subato-web/src/app/shared/components/file-browser/file-browser.component.scss
deleted file mode 100755
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.ts b/subato-web/src/app/shared/components/file-browser/file-browser.component.ts
deleted file mode 100755
index bdcaad5c1092df7e95bc5f1e5da89e59ce689575..0000000000000000000000000000000000000000
--- a/subato-web/src/app/shared/components/file-browser/file-browser.component.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
-import { FileTree } from 'app/shared/models/file-tree';
-
-interface TreeNode<T> {
-  data: T;
-  children?: TreeNode<T>[];
-  expanded?: boolean;
-}
-
-@Component({
-  selector: 'ngx-file-browser',
-  templateUrl: './file-browser.component.html',
-  styleUrls: ['./file-browser.component.scss']
-})
-export class FileBrowserComponent implements OnInit {
-  @Output() treeSelectedEvent = new EventEmitter<FileTree>();
-  @Input() tree: FileTree;
-
-  data: TreeNode<FileTree>[] = [];
-
-  customColumn = 'filename';
-  defaultColumns = [];
-  allColumns = [ this.customColumn, ...this.defaultColumns ];
-  
-  constructor() { }
-
-  private treeNodeFor(tree: FileTree): TreeNode<FileTree> {
-    return {
-      data: tree,
-      children: tree.children.map(child => this.treeNodeFor(child)),
-      expanded: false
-    };
-  }
-
-  ngOnInit(): void {
-    this.data = this.tree.children.map(tree => this.treeNodeFor(tree));
-  }
-
-  onTreeClicked(tree: FileTree) {
-    if(tree.children.length === 0) {
-      this.treeSelectedEvent.emit(tree);
-    }
-  }
-}
diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.html b/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.html
deleted file mode 100755
index 372f150471617dbe2283421dab15d971d648638a..0000000000000000000000000000000000000000
--- a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<div *ngIf="file">
-        <nb-card [nbSpinner]="file.content == null" nbSpinnerStatus="basic">
-            <nb-card-header class="d-flex" style="align-items: center;">
-                <a nbButton (click)="toggle()" [status]="'basic'" class="mr-3">
-                    <nb-icon icon="menu-outline" pack="eva"></nb-icon> 
-                  </a>
-                <span style="flex-grow: 1">{{ file.reference.name }}</span>
-                <div>
-                    <a nbButton (click)="open()" [status]="'basic'">
-                        <nb-icon icon="download-outline" pack="eva"></nb-icon> 
-                    </a>
-                </div>
-            </nb-card-header>
-            <nb-card-body class="p-0" style="max-height: 600px;">
-                <ngx-codemirror  class="fillh" [(ngModel)]="file.content" style="" [options]="{
-                    lineNumbers: true,
-                    mode: file.reference.contentType | codemirrorMode,
-                    theme: darkMode | codemirrorTheme,
-                    readOnly: 'nocursor'
-                  }"></ngx-codemirror>
-            </nb-card-body>
-        </nb-card>
-</div>
\ No newline at end of file
diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.scss b/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.scss
deleted file mode 100755
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/subato-web/src/app/shared/directives/maximize.directive.ts b/subato-web/src/app/shared/directives/maximize.directive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..893402b92f01de2b41b601c463fba07958ad407d
--- /dev/null
+++ b/subato-web/src/app/shared/directives/maximize.directive.ts
@@ -0,0 +1,50 @@
+import { Directive, ElementRef, HostListener, Inject, Renderer2 } from "@angular/core";
+import { BehaviorSubject } from "rxjs";
+import { tap } from "rxjs/operators";
+import {DOCUMENT, ViewportScroller} from '@angular/common';
+
+// https://sreyaj.dev/fullscreen-toggle-angular-using-directives 
+// + enhanced by .noscroll + esc key + restore scroll pos
+
+@Directive({
+  selector: "[maximize]",
+  exportAs: "maximize" // <-- Make not of this here
+})
+export class MaximizeDirective {
+  private isMaximizedSubject = new BehaviorSubject(false);
+  isMaximized$ = this.isMaximizedSubject.pipe();
+  scrollPosition: [number, number];
+
+  constructor(
+    private el: ElementRef, 
+    private renderer: Renderer2,  
+    @Inject(DOCUMENT) private document: Document,
+    private scroller: ViewportScroller) {}
+
+  toggle() {
+    this.isMaximizedSubject?.getValue() ? this.minimize() : this.maximize();
+  }
+  maximize() {
+    if (this.el) {
+      this.scrollPosition = this.scroller.getScrollPosition();
+      this.isMaximizedSubject.next(true);
+      this.renderer.addClass(this.el.nativeElement, "fullscreen");
+      this.renderer.addClass(this.document.body, 'noscroll');
+    }
+  }
+  minimize() {
+    if (this.el) {
+      this.isMaximizedSubject.next(false);
+      this.renderer.removeClass(this.el.nativeElement, "fullscreen");
+      this.renderer.removeClass(this.document.body, "noscroll");
+      this.scroller.scrollToPosition(this.scrollPosition);
+    }
+  }
+
+  @HostListener('document:keyup.escape', ['$event']) onEscKeyUpHandler(event: KeyboardEvent) {
+    let isMaximized = this.isMaximizedSubject?.getValue();
+    if(isMaximized) {
+      this.minimize();
+    }
+  }
+}
\ No newline at end of file
diff --git a/subato-web/src/app/shared/models/file-tree.ts b/subato-web/src/app/shared/models/file-tree.ts
index ac1b074a01e93a6e2c8f2fce1e7034d9282ca713..4ac0505ca166ca6d985d674f89b932bb12009c4f 100755
--- a/subato-web/src/app/shared/models/file-tree.ts
+++ b/subato-web/src/app/shared/models/file-tree.ts
@@ -4,6 +4,7 @@ export class FileTree {
     contentType: string;
     children: FileTree[];
     src: boolean;
+    directory: boolean;
 
     constructor(item: any) {
         this.name = item.name;
@@ -11,9 +12,29 @@ export class FileTree {
         this.contentType = item.contentType;
         this.children = item.children ? item.children.map(c => new FileTree(c)) : [];
         this.src = item.src;
+        this.directory = item.directory;
     }
 
     get isPdf() {
-        return this.name.endsWith(".pdf");
+        return this.contentType == 'application/pdf';
+    }
+
+    filterByName(searchString: string): FileTree {
+        const filteredChildren = this.children
+            .map(child => child.filterByName(searchString))
+            .filter(child => child !== null);
+
+        if (this.name.toLowerCase().includes(searchString.toLowerCase()) || filteredChildren.length > 0) {
+            let tree = new FileTree({});
+            tree.name = this.name;
+            tree.children = filteredChildren;
+            tree.contentType = this.contentType;
+            tree.src = this.src;
+            tree.uuid = this.uuid;
+            tree.directory = this.directory;
+            return tree;
+        } else {
+            return null;
+        }
     }
 }
\ No newline at end of file
diff --git a/subato-web/src/app/shared/shared.module.ts b/subato-web/src/app/shared/shared.module.ts
index 0be377271ec0e47e24942801356556e22cd49500..168fd4a4a0d88c18dc2d1b4443f74ae3e3eb3aa8 100755
--- a/subato-web/src/app/shared/shared.module.ts
+++ b/subato-web/src/app/shared/shared.module.ts
@@ -7,8 +7,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { NbMomentDateModule } from '@nebular/moment';
 import { NbAlertModule, NbCardModule, NbIconModule, NbInputModule, NbBadgeModule, NbListModule, NbButtonModule, NbAccordionModule, NbDatepickerModule, NbTimepickerModule, NbToggleModule, NbSpinnerModule, NbCheckboxModule, NbSelectModule, NbTreeGridModule, NbFormFieldModule, NbActionsModule, NbContextMenuModule, NbLayoutModule, NbMenuModule, NbSearchModule, NbSidebarModule, NbUserModule, NbAutocompleteComponent, NbAutocompleteModule, NbTabsetModule, NbProgressBarModule, NbTagModule, NbPopoverModule, NbStepperModule } from '@nebular/theme';
 import { BaseComponent } from './components/base/base.component';
-import { FileBrowserComponent } from './components/file-browser/file-browser.component';
-import { SourceCodeViewerComponent } from './components/source-code-viewer/source-code-viewer.component';
 import { ValidationHintComponent } from './components/validation-hint/validation-hint.component';
 import { ValidationSummaryComponent } from './components/validation-summary/validation-summary.component';
 import { NbEvaIconsModule } from '@nebular/eva-icons';
@@ -43,16 +41,17 @@ import { NavigationListComponent } from './components/navigation-list/navigation
 import { NavigationListItemComponent } from './components/navigation-list-item/navigation-list-item.component';
 import { TrustUrlPipe } from './pipes/trust-url.pipe';
 import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal-menu.component';
+import { MaximizeDirective } from './directives/maximize.directive';
 
 @NgModule({
   declarations: [
     LoadingScreenComponent,
     BaseComponent,
-    FileBrowserComponent,
-    SourceCodeViewerComponent,
     ValidationHintComponent,
     ValidationSummaryComponent,
 
+    MaximizeDirective,
+
     PropPipe,
     FnPipe,
     CodemirrorThemePipe,
@@ -110,6 +109,7 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal
     NbAutocompleteModule,
     NbTagModule,
     NbPopoverModule,
+    NbTreeGridModule,
     DragDropModule,
     CodemirrorModule,
     NgJoinPipeModule,
@@ -117,7 +117,7 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal
     NgWherePipeModule,
     NgRoundPipeModule,
     NgMapPipeModule,
-
+    
     PropPipe,
     FnPipe,
     CodemirrorThemePipe,
@@ -126,11 +126,11 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal
     UserNamePipe,
     TrustUrlPipe,
     
+    MaximizeDirective,
+
     RouteReuseLifeCycleDirective,
     LoadingScreenComponent,
     BaseComponent,
-    FileBrowserComponent,
-    SourceCodeViewerComponent,
     ValidationHintComponent,
     ValidationSummaryComponent,
     ErrorAlertComponent,
@@ -194,7 +194,7 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal
     NgPluckPipeModule,
     NgWherePipeModule,
     NgRoundPipeModule,
-    NgMapPipeModule
+    NgMapPipeModule,
   ]
 })
 export class SharedModule { }
diff --git a/subato-web/src/environments/shared.ts b/subato-web/src/environments/shared.ts
index 3af8ae960a96ba5182f662e7c6d8df121b77f0a4..17f3208c7c5a716a327ad50d989a59b62f260e3b 100644
--- a/subato-web/src/environments/shared.ts
+++ b/subato-web/src/environments/shared.ts
@@ -2,7 +2,7 @@ let docsUrl = 'http://docs.subato-test2.local.cs.hs-rm.de';
 let manualUrl = `${docsUrl}/docs`;
 
 export const shared = {
-    'version': '2.0.0-beta2',
+    'version': '2.0.0-beta3',
     docsUrl: docsUrl,
     manualUrl: manualUrl,
     stefDocsUrl: `${docsUrl}/stef`,
diff --git a/subato/build.gradle b/subato/build.gradle
index eb9bd43df4182d38f130f216f18fe8b083a73bdb..6f436d51e3ad1737777b208ab71c90a8b7ff206c 100755
--- a/subato/build.gradle
+++ b/subato/build.gradle
@@ -16,7 +16,7 @@ plugins {
 }
 
 group = 'de.hsrm.sls'
-version = '2.0.0-beta2-SNAPSHOT'
+version = '2.0.0-beta3-SNAPSHOT'
 
 repositories {
     mavenCentral()
diff --git a/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java b/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java
index f03ba415c851eb1c09bd0922df0f6c29fdac442d..aad33ca4f586b708b2d0d0254a72367ddee9c68a 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java
@@ -69,19 +69,19 @@ public class KeycloakLoginHandler {
         String username = alias;
 
         if(alias != null) {
-            logger.info(String.format("Logged in with alias user, skipping import/update..."));
+            logger.debug(String.format("Logged in with alias user, skipping import/update..."));
         } else {
             username = getUsername(auth);
             var mail = getMail(auth);
 
             var user = userRepository.findByLoginNameIgnoreCase(username).orElse(null);
             if(user == null && mail != null) {
-                logger.info(String.format("User with loginName %s not found, trying email %s", username, mail));
+                logger.debug(String.format("User with loginName %s not found, trying email %s", username, mail));
                 user = userRepository.findByMailIgnoreCase(getMail(auth)).orElse(null);
             }
 
             if (user == null) {
-                logger.info("User not found, importing now...");
+                logger.debug("User not found, importing now...");
                 user = new User();
             }
 
@@ -117,7 +117,7 @@ public class KeycloakLoginHandler {
                 .collect(Collectors.toList());
         user.getRoles().addAll(roles);
         user = userRepository.save(user);
-        logger.info("Imported/updated user %s".formatted(user.getLoginName()));
+        logger.debug("Imported/updated user %s".formatted(user.getLoginName()));
 
         return user;
     }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java b/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java
index acbd50bd0a94034fbad6ccd526954ed091700d36..8176d2ee449db40603af3926b051a377a0da00dd 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java
@@ -24,6 +24,7 @@ public class TaskMasker implements Masker {
             obj.setEvaluator(null);
             obj.setPool(null);
             obj.setFeedbackStats(null);
+            obj.setRepositoryUrl(null);
         }
     }
 
diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java
index 5bf85a5667ddee28342af8fd4659f4894bfbcd58..cf6e19bc5011ea0cc01860379e821a53b998e66c 100755
--- a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java
@@ -8,7 +8,8 @@ import de.hsrm.sls.subato.auth.policy.exercises.ExercisesAction;
 import de.hsrm.sls.subato.courses.course.CourseNotFoundException;
 import de.hsrm.sls.subato.courses.course.CourseService;
 import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressDto;
-import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressMapper;
+import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressResultDto;
+import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressResultMapper;
 import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressService;
 import de.hsrm.sls.subato.exercises.taskinstance.*;
 import de.hsrm.sls.subato.shared.DangerousActionException;
@@ -57,7 +58,7 @@ public class ExerciseController {
     private PrincipalService principalService;
 
     @Autowired
-    private ExerciseProgressMapper exerciseProgressMapper;
+    private ExerciseProgressResultMapper exerciseProgressResultMapper;
 
     @Autowired
     private ExerciseProgressService exerciseProgressService;
@@ -160,12 +161,12 @@ public class ExerciseController {
 
     @GetMapping("/exercise/progress")
     @Operation(summary = "Liefert Informationen zum Fortschritt (Zusammenfassung) eines Benutzers über alle Übungen eines Kurses hineweg")
-    List<ExerciseProgressDto> getProgressForUser(@RequestParam Long courseId, @RequestParam Long userId) {
+    ExerciseProgressResultDto getProgressForUser(@RequestParam Long courseId, @RequestParam Long userId) {
         try {
             policy.checkAndRaise(CheckPolicyRequest.fromPrincipal(Pair.of(courseId, userId), CoursesAction.COURSE_PROGRESS_VIEW));
 
-            var exerciseProgresses = exerciseProgressService.generateFor(courseId, userId);
-            return exerciseProgresses.stream().map(exerciseProgress -> exerciseProgressMapper.map(exerciseProgress)).toList();
+            var result = exerciseProgressService.generateFor(courseId, userId);
+            return exerciseProgressResultMapper.map(result);
         } catch(CourseNotFoundException ex) {
             throw new ResponseStatusException(BAD_REQUEST, CourseNotFoundException.REASON);
         }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResult.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..da3da080a5e8bafac397e7bcfd7131eb8bf52462
--- /dev/null
+++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResult.java
@@ -0,0 +1,31 @@
+package de.hsrm.sls.subato.exercises.exercise.progress;
+
+import de.hsrm.sls.subato.exercises.submission.progress.SubmissionsProgress;
+
+import java.util.List;
+
+public class ExerciseProgressResult {
+    private List<ExerciseProgress> exerciseProgresses;
+    private SubmissionsProgress submissionsProgress;
+
+    public ExerciseProgressResult(List<ExerciseProgress> exerciseProgresses, SubmissionsProgress submissionsProgress) {
+        this.exerciseProgresses = exerciseProgresses;
+        this.submissionsProgress = submissionsProgress;
+    }
+
+    public List<ExerciseProgress> getExerciseProgresses() {
+        return exerciseProgresses;
+    }
+
+    public void setExerciseProgresses(List<ExerciseProgress> exerciseProgresses) {
+        this.exerciseProgresses = exerciseProgresses;
+    }
+
+    public SubmissionsProgress getSubmissionsProgress() {
+        return submissionsProgress;
+    }
+
+    public void setSubmissionsProgress(SubmissionsProgress submissionsProgress) {
+        this.submissionsProgress = submissionsProgress;
+    }
+}
diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultDto.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..16aa8f824957374bd859d6d9f9954811bbb2148a
--- /dev/null
+++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultDto.java
@@ -0,0 +1,26 @@
+package de.hsrm.sls.subato.exercises.exercise.progress;
+
+import de.hsrm.sls.subato.exercises.submission.progress.SubmissionsProgressDto;
+
+import java.util.List;
+
+public class ExerciseProgressResultDto {
+    private List<ExerciseProgressDto> exerciseProgresses;
+    private SubmissionsProgressDto submissionsProgress;
+
+    public List<ExerciseProgressDto> getExerciseProgresses() {
+        return exerciseProgresses;
+    }
+
+    public void setExerciseProgresses(List<ExerciseProgressDto> exerciseProgresses) {
+        this.exerciseProgresses = exerciseProgresses;
+    }
+
+    public SubmissionsProgressDto getSubmissionsProgress() {
+        return submissionsProgress;
+    }
+
+    public void setSubmissionsProgress(SubmissionsProgressDto submissionsProgress) {
+        this.submissionsProgress = submissionsProgress;
+    }
+}
diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressMapper.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultMapper.java
similarity index 52%
rename from subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressMapper.java
rename to subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultMapper.java
index 040cab647f4070bf6517d437bfc15ccd99fba573..74a9e6f3b09c72f0be440283dfb1558d8eb5c135 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressMapper.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultMapper.java
@@ -3,6 +3,6 @@ package de.hsrm.sls.subato.exercises.exercise.progress;
 import org.mapstruct.Mapper;
 
 @Mapper(componentModel = "spring")
-public interface ExerciseProgressMapper {
-    ExerciseProgressDto map(ExerciseProgress exerciseProgress);
+public interface ExerciseProgressResultMapper {
+    ExerciseProgressResultDto map(ExerciseProgressResult result);
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java
index a35eadd9dc89049dc57540a587131c6bdfc12cb0..6baabc0b59b0cc37a3f38cf5e0d6e5cd31d20f88 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java
@@ -8,6 +8,7 @@ import de.hsrm.sls.subato.courses.membership.StaffGroup;
 import de.hsrm.sls.subato.exercises.exercise.ExerciseFilter;
 import de.hsrm.sls.subato.exercises.exercise.ExerciseService;
 import de.hsrm.sls.subato.exercises.submission.SubmissionService;
+import de.hsrm.sls.subato.exercises.submission.progress.SubmissionsProgress;
 import de.hsrm.sls.subato.exercises.taskinstance.TaskInstanceFilter;
 import de.hsrm.sls.subato.exercises.taskinstance.TaskInstanceService;
 import de.hsrm.sls.subato.user.User;
@@ -40,7 +41,7 @@ public class ExerciseProgressService {
     @Autowired
     private CourseService courseService;
 
-    public List<ExerciseProgress> generateFor(long courseId, long userId) {
+    public ExerciseProgressResult generateFor(long courseId, long userId) {
         var course = courseService.findById(courseId);
         var user = userService.findById(userId);
         var currentUser = (SubatoPrincipal) principalService.getPrincipal();
@@ -49,7 +50,7 @@ public class ExerciseProgressService {
                         currentUser.hasGroup(course, StaffGroup.TUTOR));
     }
 
-    public List<ExerciseProgress> generateFor(Course course, User user, boolean includeInvisibleExercises) {
+    public ExerciseProgressResult generateFor(Course course, User user, boolean includeInvisibleExercises) {
         var exerciseProgresses = new ArrayList<ExerciseProgress>();
         var exercises = exerciseService.findAll(new ExerciseFilter().courseId(course.getId()));
 
@@ -87,6 +88,12 @@ public class ExerciseProgressService {
             exerciseProgresses.add(exerciseProgress);
         }
         exerciseProgresses.sort(Comparator.comparing((ExerciseProgress exerciseProgress) -> exerciseProgress.getExercise().getId()).reversed());
-        return exerciseProgresses;
+
+        var submissions = exerciseProgresses.stream()
+                .filter(p -> p.getSubmission() != null)
+                .map(p -> p.getSubmission())
+                .toList();
+        var submissionsProgress = SubmissionsProgress.fromSubmissions(submissions);
+        return new ExerciseProgressResult(exerciseProgresses, submissionsProgress);
     }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java b/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java
index 9dd521c53d5c866ee654fb3193cc2c830254ed1f..86cc6dc1b499d232b58977bdccab10cd4e6cabeb 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java
@@ -6,4 +6,5 @@ package de.hsrm.sls.subato.shared;
  */
 public class ExceptionReasons {
     public static final String CONSTRAINT_VALIDATION = "Eingabe ist ungültig";
+    public static final String MAX_UPLOAD_SIZE_EXCEEDED = "Diese Datei überschreitet das Limit von (etwa) %s MB";
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java
index 541bb8e500efec30c66231577a2fae09c88f8fb6..91bee28564979ec26dd81fb63faadff70f36d349 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java
@@ -38,7 +38,7 @@ public class CacheConfig extends CachingConfigurerSupport {
     public CaffeineCache currentUserCache(MeterRegistry registry) {
         var cache = new CaffeineCache("currentUser",
                 Caffeine.newBuilder()
-                        .expireAfterAccess(60, TimeUnit.MINUTES)
+                        .expireAfterAccess(5, TimeUnit.MINUTES)
                         .build());
 
         Gauge.builder("subato.user.count",() -> {
@@ -55,7 +55,7 @@ public class CacheConfig extends CachingConfigurerSupport {
     public CaffeineCache evaluatorRegistryCache() {
         return new CaffeineCache("evaluatorRegistry",
                 Caffeine.newBuilder()
-                        .expireAfterAccess(60, TimeUnit.MINUTES)
+                        .expireAfterAccess(5, TimeUnit.MINUTES)
                         .build());
     }
 
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/ConfigController.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/ConfigController.java
new file mode 100644
index 0000000000000000000000000000000000000000..0569e7657777c3f01ab3f054a77ebc66899f21f2
--- /dev/null
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/ConfigController.java
@@ -0,0 +1,28 @@
+package de.hsrm.sls.subato.shared.config;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.MultipartConfigElement;
+
+@Component
+@RestController
+@Tag(name = "Querschnitt")
+public class ConfigController {
+    @Autowired
+    MultipartConfigElement multipartConfigElement;
+
+    @GetMapping("/config")
+    @Operation(summary = "Liefert ausgewählte Konfigurationswerte, damit diese von Clients berücksichtigt werden können")
+    public PublicConfig getConfig() {
+        var maxFileSizeInMb = multipartConfigElement.getMaxFileSize() / 1024 / 1024;
+
+        PublicConfig response = new PublicConfig();
+        response.setMaxUploadSizeInMb(maxFileSizeInMb);
+        return response;
+    }
+}
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/PublicConfig.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/PublicConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..48fd43b9d9aa94bc2179b5c61b194d845a6e7383
--- /dev/null
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/PublicConfig.java
@@ -0,0 +1,13 @@
+package de.hsrm.sls.subato.shared.config;
+
+public class PublicConfig {
+    private long maxUploadSizeInMb;
+
+    public long getMaxUploadSizeInMb() {
+        return maxUploadSizeInMb;
+    }
+
+    public void setMaxUploadSizeInMb(long maxUploadSizeInMb) {
+        this.maxUploadSizeInMb = maxUploadSizeInMb;
+    }
+}
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java
index ad6c6f8c7ef55776f32a1ca621006a6d630cf384..51e4e58dc22cf7772b0d718b325a1fe0f29b945e 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java
@@ -33,7 +33,7 @@ import java.util.stream.Collectors;
 
 @Configuration
 public class SwaggerConfiguration implements WebMvcConfigurer {
-    private final String API_VERSION = "2.0.0-beta2";
+    private final String API_VERSION = "2.0.0-beta3";
 
     @Value("${keycloak.auth-server-url}")
     private String keycloakUrl;
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java b/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java
index cbde71aaf519d1a725e71cab0ad8fb5bdb6c7305..d158075f411638ae52e9792c16fff8dd780cd128 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java
@@ -5,6 +5,7 @@ import de.hsrm.sls.subato.shared.ExceptionReasons;
 import org.apache.catalina.connector.ClientAbortException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.annotation.AnnotationUtils;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -14,8 +15,10 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.ControllerAdvice;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
 import org.springframework.web.server.ResponseStatusException;
 
+import javax.servlet.MultipartConfigElement;
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.ConstraintViolationException;
 import java.io.IOException;
@@ -29,6 +32,18 @@ public class GlobalExceptionHandler {
     final String NOT_AUTHENTICATED_MESSAGE = "Nicht eingeloggt.";
     final String UNAUTHORIZED_MESSAGE = "Keine Rechte für diese Aktion.";
 
+    @Autowired
+    MultipartConfigElement multipartConfigElement;
+
+    @ExceptionHandler(MaxUploadSizeExceededException.class)
+    public ResponseEntity<ErrorResponse> maxUploadSizeExceededException(MaxUploadSizeExceededException ex) {
+        var statusCode = HttpStatus.BAD_REQUEST;
+
+        var maxFileSizeInMb = multipartConfigElement.getMaxFileSize() / 1024 / 1024;
+        var errorResponse = new ErrorResponse(statusCode.value(), ExceptionReasons.MAX_UPLOAD_SIZE_EXCEEDED.formatted(maxFileSizeInMb), null);
+        return ResponseEntity.status(statusCode.value()).body(errorResponse);
+    }
+
     // https://stackoverflow.com/questions/35451244/how-to-handle-clientabortexception-in-spring-mvc
     @ExceptionHandler(ClientAbortException.class)
     public void handleLockException(ClientAbortException exception, HttpServletRequest request) {
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java
index c983476069f20bf15cfe85723ac8fda10a585aa5..e76ca190cecf5bae807038a0db6d500ad2c5bd31 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java
@@ -2,20 +2,23 @@ package de.hsrm.sls.subato.shared.fs;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import de.hsrm.sls.subato.shared.mime.MimeMapping;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
 
 @Service
 public class FileTreeCache {
     @Autowired
     FileTreeService fileTreeService;
 
-    private FileTree buildTree(Path path) {
+    private FileTree buildTree(Path path, List<MimeMapping> mimeMappingOverrides) {
         var file = path.toFile();
 
         if(!file.exists()) {
@@ -26,14 +29,14 @@ public class FileTreeCache {
             throw new RuntimeException("path is not a directory");
         }
 
-        return fileTreeService.createTreeFrom(file, "", true, false);
+        return fileTreeService.createTreeFrom(file, "", true, false, mimeMappingOverrides);
     }
 
-    public synchronized void saveTree(FileTree tree, Path basePath) {
+    private synchronized void saveTree(FileTree tree, Path basePath) {
         try {
             ObjectMapper objectMapper = new ObjectMapper();
             var serialized = objectMapper.writeValueAsString(tree);
-            var treeFile = basePath.resolve("tree.json").toFile();
+            var treeFile = getTreeIndexPath(basePath).toFile();
             Files.write(treeFile.toPath(), serialized.getBytes(StandardCharsets.UTF_8));
         } catch (JsonProcessingException e) {
             throw new RuntimeException(e);
@@ -42,10 +45,14 @@ public class FileTreeCache {
         }
     }
 
-    public synchronized FileTree getTree(Path basePath) {
-        var cacheFilePath = basePath.resolve("tree.json");
+    public Path getTreeIndexPath(Path basePath) {
+        return basePath.resolve("tree.json");
+    }
+
+    public synchronized FileTree getTree(Path basePath, List<MimeMapping> mimeMappingOverrides) {
+        var cacheFilePath = getTreeIndexPath(basePath);
         if(!cacheFilePath.toFile().exists()) {
-            var tree = buildTree(basePath);
+            var tree = buildTree(basePath, mimeMappingOverrides);
             saveTree(tree, basePath);
             return tree;
         }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java
index 353d552f83cedf4d2c8e255251477d5b2075215b..e4ae4de1e588f2820dce87a1b6c5fcc371533d02 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java
@@ -4,23 +4,25 @@ import com.google.common.base.Strings;
 import de.hsrm.sls.subato.shared.fs.FileTree;
 import de.hsrm.sls.subato.shared.helper.FileHelper;
 import de.hsrm.sls.subato.shared.mime.MimeDetector;
+import de.hsrm.sls.subato.shared.mime.MimeMapping;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.io.File;
 import java.nio.file.Path;
+import java.util.List;
 
 @Service
 public class FileTreeService {
     @Autowired
     MimeDetector mimeDetector;
 
-    public FileTree createTreeFrom(File file, String basePath, boolean recursive, boolean includeRoot, String contentTypeOverride) {
+    public FileTree createTreeFrom(File file, String basePath, boolean recursive, boolean includeRoot, List<MimeMapping> mimeMappingOverrides) {
         if(!file.exists()) {
             return null;
         }
 
-        var contentType = !Strings.isNullOrEmpty(contentTypeOverride) ? contentTypeOverride : mimeDetector.getContentType(file);
+        var contentType = mimeDetector.getContentType(file, mimeMappingOverrides);
         var isSourceFile = FileHelper.isSourceFile(contentType);
 
         var fileName = includeRoot ? file.getName() : "";
@@ -33,15 +35,11 @@ public class FileTreeService {
                 var relativePath = Path.of(basePath).resolve(Path.of(fileName));
 
                 for (var f : file.listFiles()) {
-                    item.getChildren().add(createTreeFrom(f, relativePath.toString(), recursive, true));
+                    item.getChildren().add(createTreeFrom(f, relativePath.toString(), recursive, true, mimeMappingOverrides));
                 }
             }
         }
 
         return item;
     }
-
-    public FileTree createTreeFrom(File file, String basePath, boolean recursive, boolean includeRoot) {
-        return createTreeFrom(file, basePath, recursive, includeRoot, null);
-    }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java
index d85d5eaabe69c3dd3c88ff6f1e97112a1127b237..666af5c8cd6c352db8577033297ef7c40d406ca0 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java
@@ -7,6 +7,10 @@ import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Responsible for mapping file names to mime types.
@@ -29,13 +33,31 @@ public class MimeDetector {
         logger.info("Configured mime mappings: %s".formatted(mimeProperties.getMime()));
     }
 
+    private Map<String, String> mergeMappings(List<MimeMapping> globalMappings, List<MimeMapping> overrides) {
+        var mimeMappings = new ArrayList<MimeMapping>();
+        mimeMappings.addAll(globalMappings);
+        mimeMappings.addAll(overrides);
+
+        var d = new HashMap<String, String>();
+        for(var mapping : mimeMappings) {
+            d.put(mapping.getMap(), mapping.getAs());
+        }
+
+        return d;
+    }
+
     /**
      * @return actual content type or application/octet-stream if it can't be guessed
      */
-    public String getContentType(String fileName) {
-        for(var mimeMapping : mimeProperties.getMime()) {
-            if(fileName.endsWith(mimeMapping.getMap())) {
-                return mimeMapping.getAs();
+    public String getContentType(String fileName, List<MimeMapping> mimeMappingOverrides) {
+        var mergedMappings = mergeMappings(mimeProperties.getMime(), mimeMappingOverrides);
+
+        for(var entry : mergedMappings.entrySet()) {
+            var map = entry.getKey();
+            var as = entry.getValue();
+
+            if(fileName.endsWith(map)) {
+                return as;
             }
         }
 
@@ -43,7 +65,11 @@ public class MimeDetector {
         return contentType;
     }
 
-    public String getContentType(File file) {
-        return getContentType(file.getName());
+    public String getContentType(String fileName) {
+        return getContentType(fileName, new ArrayList<>());
+    }
+
+    public String getContentType(File file, List<MimeMapping> mimeMappingOverrides) {
+        return getContentType(file.getName(), mimeMappingOverrides);
     }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java
index 0dc4546ef2706c2aa512aeba1b2d19e8cf55a41f..471792c0e7cc630646676725620d681ea2b82fb2 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java
@@ -4,6 +4,14 @@ public class MimeMapping {
     private String map;
     private String as;
 
+    public MimeMapping(String map, String as) {
+        this.map = map;
+        this.as = as;
+    }
+
+    public MimeMapping() {
+    }
+
     public String getMap() {
         return map;
     }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java
index adb6f97c460ec2edd9a13459ca9f21c23aa776aa..4b53875dc38ea3725ae05bef2594eaffba902b42 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java
@@ -1,7 +1,9 @@
 package de.hsrm.sls.subato.solutions.evaluation.evaluator;
 
 import de.hsrm.sls.subato.solutions.evaluation.EvaluatorConfig;
+import de.hsrm.sls.subato.user.User;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpMethod;
@@ -22,4 +24,7 @@ public class EvaluatorRegistryService {
         var response = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<List<EvaluatorEntry>>() {});
         return response.getBody();
     }
+
+    @CacheEvict(value = "evaluatorRegistry")
+    public void evictCache() {}
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java
index 82dc4d03ea9c31b199defe1a26036c1039451b68..f78c23518b035fe646d4f5f86203dcc61d597917 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java
@@ -33,7 +33,7 @@ public class EvaluatorService {
         evaluator.setStyle(evaluatorVersion.getCapability().isStyle());
         evaluator.setTest(evaluatorVersion.getCapability().isTest());
 
-        return evaluator;
+        return evaluatorRepository.save(evaluator);
     }
 
     public Evaluator resolveReference(String reference) {
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java
index 16fee0138fcabd80be81c86d560176604ed54914..7a02ce325e13001017a962363eaa2d98bf296e06 100755
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java
@@ -82,6 +82,7 @@ public class Solution {
     /**
      * how was this solution submitted?
      */
+    @NotNull
     private Client submittedWith;
 
     @OneToMany(mappedBy = "solution", cascade = { CascadeType.ALL })
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java
index 4b110bd12bb260a113b9b398f6c18f8fba354a8a..77351bd54cf2b27c4d865604fd2cd8363359f36c 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java
@@ -4,6 +4,11 @@ import de.hsrm.sls.subato.shared.config.SubatoConfig;
 import de.hsrm.sls.subato.shared.fs.FileContent;
 import de.hsrm.sls.subato.shared.fs.FileTreeCache;
 import de.hsrm.sls.subato.shared.fs.FileTree;
+import de.hsrm.sls.subato.shared.helper.ZipHelper;
+import de.hsrm.sls.subato.shared.mime.MimeMapping;
+import de.hsrm.sls.subato.tasks.stef.parser.SpecParser;
+import de.hsrm.sls.subato.tasks.task.TaskContentRepository;
+import net.lingala.zip4j.model.ExcludeFileFilter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.FileSystemUtils;
@@ -11,6 +16,8 @@ import org.springframework.util.FileSystemUtils;
 import java.io.File;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.UUID;
 
 @Service
@@ -21,6 +28,9 @@ public class SolutionContentRepository {
     @Autowired
     private FileTreeCache fileTreeCache;
 
+    @Autowired
+    private TaskContentRepository taskContentRepository;
+
     private Path getSolutionPath(Solution solution) {
         return Paths.get(subatoConfig.getDataDir())
                 .resolve("solution")
@@ -35,7 +45,8 @@ public class SolutionContentRepository {
     }
 
     public FileTree getTree(Solution solution) {
-        return fileTreeCache.getTree(getSolutionPath(solution));
+        var spec = taskContentRepository.getSpec(solution.getTask());
+        return fileTreeCache.getTree(getSolutionPath(solution), spec.getMeta().getMimeMappings());
     }
 
     /**
@@ -47,13 +58,21 @@ public class SolutionContentRepository {
      */
     public File getFile(Solution solution, UUID uuid) {
         var solutionPath = getSolutionPath(solution);
-        var solutionTree = fileTreeCache.getTree(solutionPath);
+        var spec = taskContentRepository.getSpec(solution.getTask());
+
+        var solutionTree = fileTreeCache.getTree(solutionPath, spec.getMeta().getMimeMappings());
         var tree = solutionTree.find(i -> i.getUuid().equals(uuid));
         var file = tree.getFile(solutionPath);
 
         return file;
     }
 
+    public File getSolutionAsZip(Solution solution) {
+        var root = getSolutionPath(solution);
+        ExcludeFileFilter excludeFilter = file -> file.toPath().equals(fileTreeCache.getTreeIndexPath(root));
+        return ZipHelper.zip(root.toFile(), false, excludeFilter);
+    }
+
     public void delete(Solution solution) {
         var solutionPath = getSolutionPath(solution);
         FileSystemUtils.deleteRecursively(solutionPath.toFile());
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java
index 3d9fdd57ede1f58fdeee8eb02aeac807d704aebb..16d9103431785d8b9b4a595477aedc03c69d2e5a 100755
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java
@@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.web.PageableDefault;
@@ -133,6 +134,19 @@ public class SolutionController {
         }
     }
 
+    @GetMapping("/solution/{id}/download")
+    ResponseEntity<Resource> downloadSolution(@PathVariable Long id) throws IOException {
+        try {
+            var solution = solutionService.findById(id);
+            policy.checkAndRaise(CheckPolicyRequest.fromPrincipal(solution, SolutionsAction.SOLUTION_VIEW));
+            var zipFile = solutionService.getSolutionAsZip(solution);
+            var resource = new UrlResource(zipFile.toURI());
+            return fileResponseBuilder.build(resource, "solution%s.zip".formatted(solution.getId()), true);
+        } catch(SolutionNotFoundException ex) {
+            throw new ResponseStatusException(BAD_REQUEST, "Die Lösung existiert nicht");
+        }
+    }
+
     @PostMapping(path ="/solution/submit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     @Operation(summary = solutionSubmitSummary, description = solutionSubmitDescription)
     SolutionDetailDto submit(
@@ -172,6 +186,8 @@ public class SolutionController {
             throw new ResponseStatusException(BAD_REQUEST, InvalidModeException.REASON);
         } catch(EvaluationFailedException ex) {
             throw new ResponseStatusException(INTERNAL_SERVER_ERROR, EvaluationFailedException.REASON, ex);
+        } catch(InvalidSolutionException ex) {
+            throw new ResponseStatusException(BAD_REQUEST, "%s: %s".formatted(InvalidSolutionException.REASON, ex.getMessage()));
         }
     }
 
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java
index 2192cec9c5e7b822be71a145a393463cb2c330e3..8395eec1dbef0de7072953c10326222a1d3fa1a1 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java
@@ -79,6 +79,10 @@ public class SolutionService {
         return solutionContentRepository.getFile(solution, uuid);
     }
 
+    File getSolutionAsZip(Solution solution) {
+        return solutionContentRepository.getSolutionAsZip(solution);
+    }
+
     public FileTree getTree(Solution solution) {
         return solutionContentRepository.getTree(solution);
     }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java
index e6369237c52a65c5a7a836330ec42517e864b52d..29e2c83644d55b5359e250b4938ce10eda636310 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java
@@ -22,6 +22,8 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
 import java.time.LocalDateTime;
 
 /**
@@ -71,6 +73,7 @@ public class ExerciseSubmitHandler implements SubmitHandler {
         solution.setTask(request.task());
         solution.setSubmittedAt(submittedAt);
         solution.setSubmittedWith(request.client());
+        solution.setExercise(request.exercise());
 
         if(user != null && !Strings.isNullOrEmpty(request.comment())) {
             var comment = new SolutionComment();
@@ -81,7 +84,12 @@ public class ExerciseSubmitHandler implements SubmitHandler {
             solution.getComments().add(comment);
         }
 
-        solution.setExercise(request.exercise());
+        var validator = Validation.buildDefaultValidatorFactory().getValidator();
+        var violations = validator.validate(solution);
+        if (!violations.isEmpty()) {
+            throw new ConstraintViolationException(violations);
+        }
+
         solution = solutionRepository.save(solution);
 
         if(user != null) {
diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java
index 3ef3c6ad1c4f30710df5d6dcd8a5fe442c151926..c19d045c8eb9386dac200e139eeee14f69fc5125 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java
@@ -15,6 +15,8 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
 import java.time.LocalDateTime;
 
 /**
@@ -55,6 +57,13 @@ public class TaskSubmitHandler implements SubmitHandler {
         solution.setTask(request.task());
         solution.setSubmittedAt(submittedAt);
         solution.setSubmittedWith(request.client());
+
+        var validator = Validation.buildDefaultValidatorFactory().getValidator();
+        var violations = validator.validate(solution);
+        if (!violations.isEmpty()) {
+            throw new ConstraintViolationException(violations);
+        }
+
         solution = solutionRepository.save(solution);
 
         solutionContentRepository.store(request.content(), solution);
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java
index fae84f8769d9bc1d09ed565902769a125daed1d4..ae8a412f858a4d505bf17bfdec37fff6135382e7 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java
@@ -1,9 +1,13 @@
 package de.hsrm.sls.subato.tasks.imp;
 
+import com.google.common.base.Strings;
 import de.hsrm.sls.subato.shared.fs.FileContent;
 import de.hsrm.sls.subato.shared.fs.TmpDirProvider;
+import de.hsrm.sls.subato.solutions.evaluation.evaluator.EvaluatorRegistryService;
+import de.hsrm.sls.subato.solutions.evaluation.evaluator.EvaluatorRepository;
 import de.hsrm.sls.subato.tasks.imp.log.LogType;
 import de.hsrm.sls.subato.tasks.imp.log.TaskImportLogService;
+import de.hsrm.sls.subato.tasks.lang.LanguageRepository;
 import de.hsrm.sls.subato.tasks.stef.file.FileType;
 import de.hsrm.sls.subato.tasks.stef.parser.InvalidSpecException;
 import de.hsrm.sls.subato.tasks.stef.StefSpec;
@@ -23,6 +27,7 @@ import org.springframework.util.FileSystemUtils;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.stream.Collectors;
 
@@ -51,6 +56,15 @@ public class TaskImportService {
     @Autowired
     private TmpDirProvider tmpDirProvider;
 
+    @Autowired
+    private EvaluatorRepository evaluatorRepository;
+
+    @Autowired
+    private LanguageRepository languageRepository;
+
+    @Autowired
+    private EvaluatorRegistryService evaluatorRegistryService;
+
     /**
      * checks whether there already is a task in the pool with the id given in the spec or not.
      * if not, a new task will be created and linked to the pool.
@@ -72,8 +86,13 @@ public class TaskImportService {
         task.setDescription(spec.getMeta().getDescription());
         task.setAttempts(spec.getMeta().getAttempts());
         task.setContent(spec.getText());
-        task.setEvaluator(spec.getMeta().getEvaluator());
-        task.setLanguage(spec.getMeta().getLanguage());
+
+        var evaluator = spec.getMeta().getEvaluator() != null ? evaluatorRepository.findById(spec.getMeta().getEvaluator()).get() : null;
+        task.setEvaluator(evaluator);
+
+        var language = spec.getMeta().getLanguage() != null ? languageRepository.findById(spec.getMeta().getLanguage()).get() : null;
+        task.setLanguage(language);
+
         task.setDefaultSubmissionMode(spec.getMeta().getSubmissionModes().getDefaultMode());
         task.setSupportsFileSubmissionMode(spec.getMeta().getSubmissionModes().getModes().contains(SubmissionMode.FILE));
         task.setSupportsTextSubmissionMode(spec.getMeta().getSubmissionModes().getModes().contains(SubmissionMode.TEXT));
@@ -122,8 +141,13 @@ public class TaskImportService {
             // copy the tmp dir
             taskContentRepository.store(new ImportSpecRequest(spec, task), dir);
 
+            logger.debug("Imported task %s".formatted(task.getIdName()));
             return task;
+        } catch(InvalidSpecException ex) {
+            logger.debug("Format of task %s is invalid: %s".formatted(dir.getName(), ex.getMessage()));
+            throw ex;
         } catch(Exception ex) {
+            logger.error("Failed to import task %s".formatted(dir.getName()), ex);
             throw new TaskImportException(ex);
         }
     }
@@ -136,6 +160,8 @@ public class TaskImportService {
         var tmpPath = tmpDirProvider.createTmpPath();
         var tmpDir = tmpPath.toFile();
 
+        evaluatorRegistryService.evictCache();
+
         try {
             var content = FileContent.fromMultipartFile(file);
             content.saveTo(tmpDir, true);
@@ -164,8 +190,10 @@ public class TaskImportService {
         var importedTasks = new ArrayList<Task>();
         var problems = 0L;
         var totalTaskDirectories = 0L;
-        
-        logger.info("Importing tasks in %s for pool %s".formatted(rootDir.getAbsolutePath(), pool.getName()));
+
+        evaluatorRegistryService.evictCache();
+
+        logger.debug("Importing tasks in %s for pool %s...".formatted(rootDir.getAbsolutePath(), pool.getName()));
         for(var dir : rootDir.listFiles()) {
             if(!dir.isDirectory()) {
                 continue;
@@ -179,15 +207,16 @@ public class TaskImportService {
 
             try {
                 var task = importTask(pool, dir);
-                importedTasks.add(task);
 
-                logger.info("Imported task %s".formatted(task.getIdName()));
+                var relativePath = rootDir.toPath().relativize(dir.toPath());
+                task.setRepositoryPath(relativePath.toString());
+                task = taskRepository.save(task);
+
+                importedTasks.add(task);
             } catch(InvalidSpecException ex) {
-                logger.info("Format of task %s is invalid: %s".formatted(dir.getName(), ex.getMessage()));
                 importLogService.log(pool, LogType.PROBLEM, "%s: %s. %s".formatted(dir.getName(), InvalidSpecException.REASON, ex.getMessage()));
                 problems += 1;
             } catch(TaskImportException ex) {
-                logger.error("Failed to import task %s".formatted(dir.getName()), ex);
                 importLogService.log(pool, LogType.PROBLEM, "%s: %s".formatted(dir.getName(), TaskImportException.REASON));
                 problems += 1;
             }
@@ -197,17 +226,16 @@ public class TaskImportService {
                 .filter(currentTask -> !importedTasks.stream().anyMatch(importedTask -> importedTask.getId() == currentTask.getId()))
                 .collect(Collectors.toList());
         
-        logger.info("Imported %s tasks for pool %s, %s tasks were missing in the root directory. Will be archived now...".formatted(importedTasks.size(), pool.getName(), missingTasks.size()));
         for(var missingTask : missingTasks) {
             missingTask.setArchived(true);
             missingTask = taskRepository.save(missingTask);
 
-            logger.info("Archived task %s".formatted(missingTask.getIdName()));
+            logger.debug("Archived task %s".formatted(missingTask.getIdName()));
             importLogService.log(pool, LogType.INFO, "Aufgabe %s archiviert".formatted(missingTask.getIdName()));
         }
 
-        importLogService.log(pool, LogType.INFO, "Import durchgeführt. %s Aufgaben importiert/aktualisiert, %s Aufgaben archiviert".formatted(importedTasks.size(), missingTasks.size()));
-        logger.info("Done importing tasks for pool %s".formatted(pool.getName()));
+        importLogService.log(pool, LogType.INFO, "Import durchgeführt. %s Aufgaben importiert/aktualisiert, %s Fehler, %s Aufgaben archiviert".formatted(importedTasks.size(), problems, missingTasks.size()));
+        logger.info("Imported %s tasks for pool %s, %s problems occured and %s tasks have been archived.".formatted(importedTasks.size(), pool.getName(), problems, missingTasks.size()));
         return new TaskImportResult(totalTaskDirectories, importedTasks.size(), missingTasks.size(), problems);
     }
 
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java
index 23cbf3570b5bb70579b11653d426ae6777b17fb1..2e2b556f6089a9a8265a98ff42b06b595896c1f5 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java
@@ -1,5 +1,6 @@
 package de.hsrm.sls.subato.tasks.stef;
 
+import de.hsrm.sls.subato.shared.mime.MimeMapping;
 import de.hsrm.sls.subato.solutions.evaluation.evaluator.Evaluator;
 import de.hsrm.sls.subato.tasks.lang.Language;
 import de.hsrm.sls.subato.tasks.stef.file.FileRef;
@@ -15,10 +16,11 @@ public class Meta {
     String description;
     Integer attempts;
     Long legacyId;
-    Language language;
-    Evaluator evaluator;
+    String language;
+    Long evaluator;
     SubmissionModes submissionModes;
     List<FileRef> files;
+    List<MimeMapping> mimeMappings;
 
     public String getId() {
         return id;
@@ -60,19 +62,19 @@ public class Meta {
         this.legacyId = legacyId;
     }
 
-    public Language getLanguage() {
+    public String getLanguage() {
         return language;
     }
 
-    public void setLanguage(Language language) {
+    public void setLanguage(String language) {
         this.language = language;
     }
 
-    public Evaluator getEvaluator() {
+    public Long getEvaluator() {
         return evaluator;
     }
 
-    public void setEvaluator(Evaluator evaluator) {
+    public void setEvaluator(Long evaluator) {
         this.evaluator = evaluator;
     }
 
@@ -91,4 +93,12 @@ public class Meta {
     public void setFiles(List<FileRef> files) {
         this.files = files;
     }
+
+    public List<MimeMapping> getMimeMappings() {
+        return mimeMappings;
+    }
+
+    public void setMimeMappings(List<MimeMapping> mimeMappings) {
+        this.mimeMappings = mimeMappings;
+    }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java
index 56d05328f6332fafef930ac066bc975c09d3cdf7..896edb2434c8ef048ba663eb62251a4108d0406c 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java
@@ -25,6 +25,6 @@ public class EvaluatorExtractor {
             }
         }
 
-        meta.setEvaluator(evaluator);
+        meta.setEvaluator(evaluator != null ? evaluator.getEid() : null);
     }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java
index f44396744a939837a9048ada4cafb52a57b1ff16..beabcd94096ebe9b065a76bb3b767d2b5cb69401 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java
@@ -1,6 +1,7 @@
 package de.hsrm.sls.subato.tasks.stef.parser.meta;
 
 import de.hsrm.sls.subato.shared.fs.FileTreeService;
+import de.hsrm.sls.subato.shared.mime.MimeMapping;
 import de.hsrm.sls.subato.tasks.stef.Meta;
 import de.hsrm.sls.subato.tasks.stef.file.FileRef;
 import de.hsrm.sls.subato.tasks.stef.file.FileType;
@@ -11,6 +12,7 @@ import org.springframework.stereotype.Service;
 
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.List;
 
 @Service
 public class FileRefsExtractor {
@@ -52,7 +54,7 @@ public class FileRefsExtractor {
                 var actualFilePath = Path.of(fileRoot).resolve(targetFolder).resolve(filePath);
                 var basePath = targetFolder.resolve(filePath).getParent();
 
-                var f = fileTreeService.createTreeFrom(actualFilePath.toFile(), basePath != null ? basePath.toString() : "", false, true, fNode.getMimeType());
+                var f = fileTreeService.createTreeFrom(actualFilePath.toFile(), basePath != null ? basePath.toString() : "", false, true, meta.getMimeMappings());
 
                 if (f == null) {
                     throw new InvalidSpecException("Datei %s in meta.xml deklariert aber nicht im Ordner %s gefunden".formatted(filePath, targetFolder));
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java
index 979171c07ce6027b28485f3b58cddf3d24bc4689..79b618b237ebdb127b129cd4ea6a0f12a64f9aa5 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java
@@ -22,6 +22,6 @@ public class LanguageExtractor {
             }
         }
 
-        meta.setLanguage(language);
+        meta.setLanguage(language != null ? language.getId() : null);
     }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java
index 7b43b0d551fdef1010b252796869cbcbab07fd93..ad71722ba15d6760f259b4e6f776900c9dd063d2 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java
@@ -40,6 +40,9 @@ public class MetaParser {
     @Autowired
     private FileRefsExtractor fileRefsExtractor;
 
+    @Autowired
+    private MimeMappingsExtractor mimeMappingsExtractor;
+
     public Meta parse(String fileRoot) {
         var meta = new Meta();
 
@@ -52,6 +55,7 @@ public class MetaParser {
         attemptsExtractor.extract(doc, meta);
         legacyIdExtractor.extract(doc, meta);
         evaluatorExtractor.extract(doc, meta);
+        mimeMappingsExtractor.extract(doc, meta);
         fileRefsExtractor.extract(doc, meta, fileRoot);
         submissionModeExtractor.extract(doc, meta);
 
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MimeMappingsExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MimeMappingsExtractor.java
new file mode 100644
index 0000000000000000000000000000000000000000..0768124931fbb92b03012516a7f5ce206bead960
--- /dev/null
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MimeMappingsExtractor.java
@@ -0,0 +1,32 @@
+package de.hsrm.sls.subato.tasks.stef.parser.meta;
+
+import de.hsrm.sls.subato.shared.mime.MimeMapping;
+import de.hsrm.sls.subato.tasks.stef.Meta;
+import de.hsrm.sls.subato.tasks.stef.parser.InvalidSpecException;
+import de.hsrm.sls.subato.tasks.stef.parser.meta.xml.*;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+
+@Service
+public class MimeMappingsExtractor {
+    public void extract(TaskXml doc, Meta meta) {
+        var mimeMappings = new ArrayList<MimeMapping>();
+
+        if(doc.getMimeMappings() != null) {
+            for (var mapping : doc.getMimeMappings().getMappings()) {
+                if(mapping.getMap() == null) {
+                    throw new InvalidSpecException("%s-Attribut in %s fehlt".formatted(MimeMappingXml.MAP, MimeMappingsXml.MIME_MAPPING));
+                }
+
+                if(mapping.getAs() == null) {
+                    throw new InvalidSpecException("%s-Attribut in %s fehlt".formatted(MimeMappingXml.AS, MimeMappingsXml.MIME_MAPPING));
+                }
+
+                mimeMappings.add(new MimeMapping(mapping.getMap(), mapping.getAs()));
+            }
+        }
+
+        meta.setMimeMappings(mimeMappings);
+    }
+}
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java
index 3f2b571ff8ff4e40561a4bbde5c40e6890523015..4e0dcd659a36892e41e91ca281f132b1e308f850 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java
@@ -7,7 +7,6 @@ public class FileXml {
     public static final String TYPE = "type";
     public static final String PUBLIC = "public";
     public static final String PATH = "path";
-    public static final String MIME_TYPE = "mimeType";
 
     private String path;
     private String type;
@@ -51,13 +50,4 @@ public class FileXml {
     public void setDescription(String description) {
         this.description = description;
     }
-
-    public String getMimeType() {
-        return mimeType;
-    }
-
-    @XmlAttribute(name = MIME_TYPE)
-    public void setMimeType(String mimeType) {
-        this.mimeType = mimeType;
-    }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingXml.java
new file mode 100644
index 0000000000000000000000000000000000000000..62856f95e65c785245f82c713acd9e61f19edb2a
--- /dev/null
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingXml.java
@@ -0,0 +1,30 @@
+package de.hsrm.sls.subato.tasks.stef.parser.meta.xml;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlValue;
+
+public class MimeMappingXml {
+    public static final String MAP = "map";
+    public static final String AS = "to";
+
+    private String map;
+    private String as;
+
+    public String getMap() {
+        return map;
+    }
+
+    @XmlAttribute(name = MAP)
+    public void setMap(String map) {
+        this.map = map;
+    }
+
+    public String getAs() {
+        return as;
+    }
+
+    @XmlAttribute(name = AS)
+    public void setAs(String as) {
+        this.as = as;
+    }
+}
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingsXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingsXml.java
new file mode 100644
index 0000000000000000000000000000000000000000..3e881dc4665ddd6a2b5a6f57d199e1b4879f8484
--- /dev/null
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingsXml.java
@@ -0,0 +1,19 @@
+package de.hsrm.sls.subato.tasks.stef.parser.meta.xml;
+
+import javax.xml.bind.annotation.XmlElement;
+import java.util.List;
+
+public class MimeMappingsXml {
+    public static final String MIME_MAPPING = "mimeMapping";
+
+    private List<MimeMappingXml> mappings;
+
+    public List<MimeMappingXml> getMappings() {
+        return mappings;
+    }
+
+    @XmlElement(name = MIME_MAPPING)
+    public void setMappings(List<MimeMappingXml> mappings) {
+        this.mappings = mappings;
+    }
+}
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java
index 0e9a09060afc73343440e3e9c1c164fd4b8c074c..beae75e9b65123a9b4d50ddda0d38e3521022708 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java
@@ -15,6 +15,7 @@ public class TaskXml {
     public static final String LANG = "lang";
     public static final String SUBMISSION_MODES = "submissionModes";
     public static final String FILES = "files";
+    public static final String MIME_MAPPINGS = "mimeMappings";
 
     private Long subatoId;
     private Integer attempts;
@@ -24,6 +25,7 @@ public class TaskXml {
     private String description;
     private String evaluator;
     private FilesXml files;
+    private MimeMappingsXml mimeMappings;
     private SubmissionModesXml submissionModes;
 
     public String getId() {
@@ -106,4 +108,13 @@ public class TaskXml {
     public void setSubmissionModes(SubmissionModesXml submissionModes) {
         this.submissionModes = submissionModes;
     }
+
+    public MimeMappingsXml getMimeMappings() {
+        return mimeMappings;
+    }
+
+    @XmlElement(name = MIME_MAPPINGS)
+    public void setMimeMappings(MimeMappingsXml mimeMappings) {
+        this.mimeMappings = mimeMappings;
+    }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java
index 769f8b399d253e1221a11a4197ff9779c29efd9b..28d8384e1d21b0a708aaeb509fe693361d6d474b 100755
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java
@@ -9,6 +9,7 @@ import de.hsrm.sls.subato.exercises.taskinstance.TaskInstance;
 import de.hsrm.sls.subato.tasks.pool.TaskPool;
 
 import javax.persistence.*;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -124,6 +125,12 @@ public class Task {
     @OneToMany(mappedBy = "task", cascade = CascadeType.REMOVE)
     private List<Solution> solutions;
 
+    /**
+     * relative url to the task in the repository
+     * only applicable if the linked task pool is linked to a git repository
+     */
+    private String repositoryPath;
+
     public Task() { }
 
     public Long getSourceId() {
@@ -297,12 +304,30 @@ public class Task {
     public void setSupportsTextSubmissionMode(boolean supportsTextSubmissionMode) {
         this.supportsTextSubmissionMode = supportsTextSubmissionMode;
     }
-    
+
+    public boolean isEvaluationPossible() {
+        return evaluationPossible;
+    }
+
+    public String getRepositoryPath() {
+        return repositoryPath;
+    }
+
+    public void setRepositoryPath(String repositoryPath) {
+        this.repositoryPath = repositoryPath;
+    }
+
     public boolean isSupportedByEclipsePlugin() {
         var supportedLanguages = new String[] { Language.JAVA };
         return language != null && Arrays.stream(supportedLanguages).anyMatch(langId -> language.getId().equals(langId));
     }
 
+    public String getRepositoryUrl() {
+        return pool.isLinkedToRepository() && repositoryPath != null
+                ? Path.of(pool.getRepositoryUrl(), "-", "tree", "main", repositoryPath).toString()
+                : null;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java
index 49c2d49052cc182d674cd3902c6978ea24580c2f..11d87dc077e752bd6885cf485011fc0d511915fd 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java
@@ -9,8 +9,10 @@ import de.hsrm.sls.subato.shared.helper.FileHelper;
 import de.hsrm.sls.subato.shared.helper.ZipHelper;
 import de.hsrm.sls.subato.tasks.imp.ImportSpecRequest;
 import de.hsrm.sls.subato.tasks.imp.TaskImportException;
+import de.hsrm.sls.subato.tasks.stef.StefSpec;
 import de.hsrm.sls.subato.tasks.stef.file.FileRef;
 import de.hsrm.sls.subato.tasks.stef.file.FileType;
+import de.hsrm.sls.subato.tasks.stef.parser.SpecParser;
 import net.lingala.zip4j.model.ExcludeFileFilter;
 import org.apache.commons.io.FileUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -36,28 +38,37 @@ public class TaskContentRepository {
     @Autowired
     private FileTreeService fileTreeService;
 
+    @Autowired
+    private SpecParser specParser;
+
     public Path getTaskPath(Task task) {
         return Paths.get(subatoConfig.getDataDir())
                 .resolve("task")
                 .resolve(String.valueOf(task.getId()));
     }
 
-    public List<FileRef> getFileRefs(Task task) {
+    public StefSpec getSpec(Task task) {
         var indexFile = getIndexFile(task);
         if(!indexFile.exists()) {
-            throw new RuntimeException();
+            var spec = specParser.parseSpec(getTaskPath(task).toString());
+            createIndex(spec, task);
+            return spec;
         }
 
         var objectMapper = new ObjectMapper();
         try {
             var specContent = Files.readString(indexFile.toPath(), StandardCharsets.UTF_8);
-            var spec = objectMapper.readValue(specContent, new TypeReference<List<FileRef>>() {});
+            var spec = objectMapper.readValue(specContent, StefSpec.class);
             return spec;
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
     }
 
+    public List<FileRef> getFileRefs(Task task) {
+        return getSpec(task).getMeta().getFiles();
+    }
+
     public Optional<FileRef> getFileRefById(Task task, UUID uuid) {
         var files = getFileRefs(task);
         return files.stream().filter(f -> f.getFile().getUuid().equals(uuid)).findFirst();
@@ -111,7 +122,7 @@ public class TaskContentRepository {
             buildBundle(request);
 
             // create an index for performant access
-            createIndex(request);
+            createIndex(request.spec(), request.task());
         } catch (Exception e) {
             try {
                 FileUtils.copyDirectory(backupPath.toFile(), taskPath.toFile());
@@ -193,7 +204,7 @@ public class TaskContentRepository {
 
             var bundleFile = getBundleFile(request.task());
             var zipFile = ZipHelper.zip(tempDir, bundleFile, false);
-            var item = fileTreeService.createTreeFrom(zipFile, getTaskPath(request.task()).relativize(bundleDir).toString(), false, true);
+            var item = fileTreeService.createTreeFrom(zipFile, getTaskPath(request.task()).relativize(bundleDir).toString(), false, true, new ArrayList<>());
 
             var taskFile = new FileRef();
             taskFile.setPath(bundleFile.getName());
@@ -212,14 +223,14 @@ public class TaskContentRepository {
 
     private File getIndexFile(Task task) {
         var taskPath = getTaskPath(task);
-        return taskPath.resolve(".stef.index.json").toFile();
+        return taskPath.resolve(".stef.cache.json").toFile();
     }
 
-    private void createIndex(ImportSpecRequest request) {
+    private void createIndex(StefSpec spec, Task task) {
         try {
             ObjectMapper objectMapper = new ObjectMapper();
-            var serialized = objectMapper.writeValueAsString(request.spec().getMeta().getFiles());
-            var indexFile = getIndexFile(request.task());
+            var serialized = objectMapper.writeValueAsString(spec);
+            var indexFile = getIndexFile(task);
             Files.write(indexFile.toPath(), serialized.getBytes(StandardCharsets.UTF_8));
         } catch (JsonProcessingException e) {
             throw new RuntimeException(e);
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java
index cfba5810e1ecce5df88668d3809151c14ad05e72..f49ef828360e25151ee728b2ae7be7d2424c5d68 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java
@@ -27,6 +27,7 @@ public class TaskDetailDto {
 
     private boolean acceptanceTestAvailable;
     private boolean supportedByEclipsePlugin;
+    private String repositoryUrl;
 
     public long getId() {
         return id;
@@ -147,4 +148,12 @@ public class TaskDetailDto {
     public void setSupportedByEclipsePlugin(boolean supportedByEclipsePlugin) {
         this.supportedByEclipsePlugin = supportedByEclipsePlugin;
     }
+
+    public String getRepositoryUrl() {
+        return repositoryUrl;
+    }
+
+    public void setRepositoryUrl(String repositoryUrl) {
+        this.repositoryUrl = repositoryUrl;
+    }
 }
diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java
index 2d1f3599a7dd13d12061cb681adc2356c5e9a45e..df53ff343759520761d3edf0e7ad5e88661deefb 100644
--- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java
+++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java
@@ -13,7 +13,11 @@ public class TaskSpecifications {
     }
 
     public static Specification<Task> applyQuery(String q) {
-        return (ex, cq, cb) -> cb.or(ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.name)), ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.description)));
+        return (ex, cq, cb) -> cb.or(
+                ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.name)),
+                ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.idName)),
+                ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.description))
+        );
     }
 
     public static Specification<Task> poolEquals(Long poolId) {
diff --git a/subato/src/main/resources/application.properties b/subato/src/main/resources/application.properties
index 36ade272307edb11cb75fc62d88fbcf9344241df..8b0146a132e58cf4a0eff0105f4b9503407d1074 100755
--- a/subato/src/main/resources/application.properties
+++ b/subato/src/main/resources/application.properties
@@ -16,7 +16,7 @@ subato.tmp_dir=/tmp/subato2-tmp
 subato.evaluator.url=${SUBATO2_EVALUATOR_URL:localhost}
 subato.evaluator.port=${SUBATO2_EVALUATOR_PORT:5081}
 subato.evaluator.registry_port=${SUBATO2_EVALUATOR_REGISTRY_PORT:7777}
-subato.evaluator.timeout=${SUBATO2_EVALUATOR_TIMEOUT:60000}
+subato.evaluator.timeout=${SUBATO2_EVALUATOR_TIMEOUT:90000}
 subato.task-pool-sync.git.user=${SUBATO2_TASK_POOL_SYNC_GIT_USER:Subato}
 subato.task-pool-sync.git.token=${SUBATO2_TASK_POOL_SYNC_GIT_TOKEN:}
 subato.task-pool-sync.enabled=${SUBATO2_TASK_POOL_SYNC_ENABLED:false}
diff --git a/subato/src/main/resources/db/changelog/2023/02/23-01-changelog.yaml b/subato/src/main/resources/db/changelog/2023/02/23-01-changelog.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..119910b0caa5121c271fe093dafb2be930292200
--- /dev/null
+++ b/subato/src/main/resources/db/changelog/2023/02/23-01-changelog.yaml
@@ -0,0 +1,13 @@
+databaseChangeLog:
+  - changeSet:
+      id: 1677115320729-1
+      author: fischer (generated)
+      objectQuotingStrategy: QUOTE_ONLY_RESERVED_WORDS
+      changes:
+        - addColumn:
+            columns:
+              - column:
+                  name: repository_path
+                  type: VARCHAR(255)
+            tableName: task
+
diff --git a/subato/src/main/resources/db/changelog/2023/02/26-01-changelog.yaml b/subato/src/main/resources/db/changelog/2023/02/26-01-changelog.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1d8d4e78d924bd8472c38d59c33c1dee6194a3a2
--- /dev/null
+++ b/subato/src/main/resources/db/changelog/2023/02/26-01-changelog.yaml
@@ -0,0 +1,13 @@
+databaseChangeLog:
+  - changeSet:
+      id: 1677416347841-1
+      author: fischer (generated)
+      objectQuotingStrategy: QUOTE_ONLY_RESERVED_WORDS
+      changes:
+        - addNotNullConstraint:
+            columnDataType: INT
+            columnName: submitted_with
+            tableName: solution
+            validate: true
+            defaultNullValue: 1
+
diff --git a/subato/src/main/resources/db/changelog/db.changelog-master.yaml b/subato/src/main/resources/db/changelog/db.changelog-master.yaml
index c1264041d7a4bb7792dafe7baed866703b60b02a..424ae0cbba0370d4487e1fd5b65a9ccf33f9e10b 100644
--- a/subato/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/subato/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -1,3 +1,7 @@
 databaseChangeLog:
   - include:
-      file: db/changelog/2023/02/08-01-changelog.yaml
\ No newline at end of file
+      file: db/changelog/2023/02/08-01-changelog.yaml
+  - include:
+      file: db/changelog/2023/02/23-01-changelog.yaml
+  - include:
+      file: db/changelog/2023/02/26-01-changelog.yaml
\ No newline at end of file
diff --git a/subato/src/test/resources/application.properties b/subato/src/test/resources/application.properties
index 7c5ea70774a34a8b067918051ea8544730fb57be..8109a8a7c6911538485f411576bb72992759b81d 100755
--- a/subato/src/test/resources/application.properties
+++ b/subato/src/test/resources/application.properties
@@ -18,12 +18,13 @@ spring.jackson.default-property-inclusion=non_null
 spring.jackson.mapper.default-view-inclusion=true
 server.servlet.session.persistent=false
 
+subato.mime_file=${SUBATO2_MIME_FILE:config/mime.yml}
 subato.data_dir=subato2-data-test
 subato.tmp_dir=/tmp/subato2-tmp-test
 subato.evaluator.url=subato-test2.local.cs.hs-rm.de
 subato.evaluator.port=5081
 subato.evaluator.registry_port=7777
-subato.evaluator.timeout=60000
+subato.evaluator.timeout=90000
 subato.task-pool-sync.git.user=${SUBATO2_TASK_POOL_SYNC_GIT_USER:Subato}
 subato.task-pool-sync.git.token=${SUBATO2_TASK_POOL_SYNC_GIT_TOKEN:}
 subato.task-pool-sync.enabled=${SUBATO2_TASK_POOL_SYNC_ENABLED:false}