diff --git a/.DS_Store b/.DS_Store
index e5e78613b23e934a047b9c7bd5932dd06a1d227e..7dd6299318eff92ea60bc577e86e374643134dfe 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 335e27633579b4ff3acacfd55986737f1e233e13..ef690c9b96775a43a5d9430ce335df3b62ed52ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,6 @@ appstore/static/zamba/train/videos/
 
 process-result.json
 process-result-dryrun.json
-status.json
\ No newline at end of file
+status.json
+
+megedetector/
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 2856a7502033c46d70fb3c0dad352ec8cb878d06..3037f46ffda698b3fcf7bda51db6fbe921998eed 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
-[submodule "trapper"]
-	path = trapper
-	url = https://gitlab.com/trapper-project/trapper.git
-[submodule "animl-frontend"]
-	path = animl-frontend
-	url = https://github.com/tnc-ca-geo/animl-frontend.git
+# [submodule "trapper"]
+# 	path = trapper
+# 	url = https://gitlab.com/trapper-project/trapper.git
+# [submodule "animl-frontend"]
+# 	path = animl-frontend
+# 	url = https://github.com/tnc-ca-geo/animl-frontend.git
diff --git a/Dockerfile b/Dockerfile
index 965f1864fb9f6f8555d4192cb6d32061139dbb34..8078425594a090464e53808a5b50c101a3a42e41 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,6 +7,8 @@ RUN apt-get update && apt-get install -y \
     gcc \
     g++
 
+RUN apt-get update && apt-get install -y docker.io docker-compose
+
 # Set the working directory in the container
 WORKDIR /app
 
diff --git a/animl-frontend b/animl-frontend
deleted file mode 160000
index 5189331b82cd04851caf7ae4e0645df9ffcca0aa..0000000000000000000000000000000000000000
--- a/animl-frontend
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 5189331b82cd04851caf7ae4e0645df9ffcca0aa
diff --git a/appstore/.DS_Store b/appstore/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..f813590c606f8c51ea6d3f3aaba8e70680f98b84
Binary files /dev/null and b/appstore/.DS_Store differ
diff --git a/appstore/__init__.py b/appstore/__init__.py
index 062a08d14a55ae9e645fc0e1a5894d162a24825f..269808c7750821f2a05c297ae40fb224e5375c26 100644
--- a/appstore/__init__.py
+++ b/appstore/__init__.py
@@ -2,8 +2,9 @@ from flask import Flask
 from flask_cors import CORS
 from sqlalchemy import create_engine
 from celery import Celery
+from appstore.models import Base, AppMetadata
 
-app = Flask(__name__)
+app = Flask(__name__, static_folder='static', template_folder='templates')
 
 # Enable CORS for all routes
 CORS(app)
@@ -18,11 +19,24 @@ celery.conf.update(app.config)
 # Database connection
 engine = create_engine(app.config['DATABASE_URI'], echo=True)
 connection = engine.connect()
+Base.metadata.create_all(engine)
 
 # Import and register blueprints
-from appstore.routes import main_bp, zamba_bp, trapper_bp, animl_bp
+from appstore.routes import main_bp, zamba_bp, trapper_bp, animl_bp, megadetector_bp
 
 app.register_blueprint(main_bp)
 app.register_blueprint(zamba_bp)
 app.register_blueprint(trapper_bp)
-app.register_blueprint(animl_bp)
\ No newline at end of file
+app.register_blueprint(animl_bp)
+app.register_blueprint(megadetector_bp)
+
+# Import utils after app initialization
+from appstore.utils import trapper_metadata, create_app_metadata, animl_metadata
+from sqlalchemy import text
+
+# Create Trapper metadata if it doesn't exist
+for app_name in ['Trapper', 'Animl']:
+    result = connection.execute(text("SELECT * FROM app_metadata WHERE name = :name"), {"name": app_name}).fetchone()
+    if not result:
+        app_metadata = globals()[f"{app_name.lower()}_metadata"]
+        create_app_metadata(app_metadata)
\ No newline at end of file
diff --git a/appstore/__pycache__/__init__.cpython-39.pyc b/appstore/__pycache__/__init__.cpython-39.pyc
index b4e3da8beeaef1739fdfb6a42b0b889b17b292ff..20a3a6886f14a5e5fda062f73cc57b6f559ba09e 100644
Binary files a/appstore/__pycache__/__init__.cpython-39.pyc and b/appstore/__pycache__/__init__.cpython-39.pyc differ
diff --git a/appstore/config.py b/appstore/config.py
index bb77fe5c75b7bb41ec0f78ffab1cb9049954cdf2..1e376eba92aa152cf85e89e0686d7e7860ea0997 100644
--- a/appstore/config.py
+++ b/appstore/config.py
@@ -4,14 +4,23 @@ import os
 SECRET_KEY = 'your_secret_key_here'
 DEBUG = True
 
-# File upload settings
+# Zamba file upload settings
 UPLOAD_FOLDER = 'static/zamba/media'
 TRAIN_FOLDER = 'static/zamba/train'
 TRAIN_VIDEOS_FOLDER = 'static/zamba/train/videos'
 
+# Megadetector file upload settings
+MEGADETECTOR_UPLOAD_FOLDER = 'static/megadetector/images'
+MEGADETECTOR_MAIN_FOLDER = 'static/megadetector'
+
 # Celery settings
 CELERY_BROKER_URL = 'redis://localhost:6379/0'
 CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
 
 # Database settings
-DATABASE_URI = 'mysql+pymysql://c22097859:Masterdata822!@csmysql.cs.cf.ac.uk:3306/c22097859_dissertation'
\ No newline at end of file
+DATABASE_URI = 'mysql+pymysql://c22097859:Masterdata822!@csmysql.cs.cf.ac.uk:3306/c22097859_dissertation'
+
+# GitLab container registry settings
+GITLAB_REGISTRY_URL = 'registry.git.cf.ac.uk'
+GITLAB_REGISTRY_USERNAME = 'c22097859'
+GITLAB_ACCESS_TOKEN = 'Ye4xAmRLMarzafBBYznx' # READ_WRITE_REGISTRY
\ No newline at end of file
diff --git a/appstore/models.py b/appstore/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..eeb8a3094da4f39412705543a3e45d4b51c550bc
--- /dev/null
+++ b/appstore/models.py
@@ -0,0 +1,19 @@
+from sqlalchemy import Column, Integer, String, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class AppMetadata(Base):
+    __tablename__ = 'app_metadata'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String(255), nullable=False)
+    description = Column(Text, nullable=True)
+    repository_url = Column(String(255), nullable=False)
+    docker_compose_file = Column(String(255), nullable=True)
+    docker_image = Column(String(255), nullable=True)
+    start_command = Column(Text, nullable=True)
+    stop_command = Column(Text, nullable=True)
+
+    def __repr__(self):
+        return f"<AppMetadata(name='{self.name}', id={self.id})>"
\ No newline at end of file
diff --git a/appstore/routes/__init__.py b/appstore/routes/__init__.py
index b139f0d95e234397c553ad935f3eb1a37371da89..2b73ffd86ed6eeb71a948750408febdad193bd3e 100644
--- a/appstore/routes/__init__.py
+++ b/appstore/routes/__init__.py
@@ -1,4 +1,5 @@
 from .main import bp as main_bp
 from .zamba import bp as zamba_bp
 from .trapper import bp as trapper_bp
-from .animl import bp as animl_bp
\ No newline at end of file
+from .animl import bp as animl_bp
+from .megadetector import bp as megadetector_bp
\ No newline at end of file
diff --git a/appstore/routes/animl.py b/appstore/routes/animl.py
index 26927e6a04381d2ccd3229aabf50103f3626983d..5c69b149d9855ed64bb35ce0d132cb3e31680066 100644
--- a/appstore/routes/animl.py
+++ b/appstore/routes/animl.py
@@ -1,10 +1,54 @@
 from flask import Blueprint, jsonify
+from appstore.utils import get_app_metadata, is_container_running, is_container_running_by_name, is_server_ready, is_container_exist
+from appstore import app
 import subprocess
+import docker
+import time
 
 bp = Blueprint('animl', __name__, url_prefix='/animl')
 
 @bp.route('/start', methods=['POST'])
-def start_animl_detection():
-    command = ['npm', 'start']
-    process = subprocess.Popen(command, cwd='../animl-frontend', stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-    return jsonify({'message': 'Animl started'}), 202
\ No newline at end of file
+def start_animl():
+    start_animl_endpoint()
+
+    max_retries = 60
+    retries = 0
+    animl_url = "http://127.0.0.1:5173/"
+    while retries < max_retries:
+        if is_container_running_by_name('animl-container') and is_server_ready(animl_url):
+            return jsonify({'status': 'running', 'url': animl_url}), 200
+        time.sleep(5)
+        retries += 1
+
+    return jsonify({'error': 'Failed to start Animl', 'status': 'timeout'}), 500
+
+def start_animl_endpoint():
+    client = docker.from_env()
+    app_metadata = get_app_metadata('Animl')
+
+    try:
+        container = client.containers.get('animl-container')
+        if container.status != 'running':
+            container.start()
+        print("Started existing animl-container")
+    except docker.errors.NotFound:
+        print("No existing animl-container found, creating new one")
+        # Login to GitLab container registry
+        client.login(
+            username=app.config['GITLAB_REGISTRY_USERNAME'],
+            password=app.config['GITLAB_ACCESS_TOKEN'],
+            registry=app.config['GITLAB_REGISTRY_URL']
+        )
+
+        # Pull the latest image
+        image = client.images.pull(app_metadata['docker_image'])
+
+        # Run the image
+        container = client.containers.run(
+            image.id,
+            name='animl-container',
+            ports={'5173/tcp': 5173},
+            detach=True
+        )
+
+    return jsonify({'message': 'Animl started', 'container_id': container.id}), 202
\ No newline at end of file
diff --git a/appstore/routes/main.py b/appstore/routes/main.py
index f6c614e6734b0a5eaa97334e411af57ce5bde1fa..d68ef7ece58de55ff178850da0b778ecf4652b80 100644
--- a/appstore/routes/main.py
+++ b/appstore/routes/main.py
@@ -1,5 +1,6 @@
-from flask import Blueprint, render_template, jsonify
+from flask import Blueprint, render_template, jsonify, send_from_directory, current_app
 from appstore import celery
+import os
 
 bp = Blueprint('main', __name__)
 
@@ -16,4 +17,16 @@ def task_status(task_id):
         'status': task.state,
         'result': task.result  # This could be None if task isn't finished
     }
-    return jsonify(data)
\ No newline at end of file
+    return jsonify(data)
+
+# uncatergorised routes
+@bp.route('/Users/<path:filepath>')
+def serve_user_files(filepath):
+    # This route handles the absolute paths in the HTML
+    if 'static' in filepath:
+        relative_path = filepath.split('static/')[-1]
+        return send_from_directory(os.path.join(current_app.root_path, 'static'), relative_path)
+    elif 'templates' in filepath:
+        relative_path = filepath.split('templates/')[-1]
+        return send_from_directory(os.path.join(current_app.root_path, 'templates'), relative_path)
+    return "File not found", 404
\ No newline at end of file
diff --git a/appstore/routes/megadetector.py b/appstore/routes/megadetector.py
new file mode 100644
index 0000000000000000000000000000000000000000..82aa7ab54775c5e69bec1456644f73f84e94df44
--- /dev/null
+++ b/appstore/routes/megadetector.py
@@ -0,0 +1,83 @@
+from flask import Blueprint, request, render_template, jsonify, send_file, send_from_directory, current_app
+from appstore import app
+from megadetector.detection.run_detector_batch import load_and_run_detector_batch, write_results_to_file
+from megadetector.postprocessing.postprocess_batch_results import process_batch_results, PostProcessingOptions
+from megadetector.utils import path_utils
+import os
+import subprocess
+
+bp = Blueprint('megadetector', __name__, url_prefix='/megadetector')
+
+@bp.route('/', methods=['GET', 'POST'])
+def megadetector():
+    if request.method == 'POST':
+        # image = request.files['image']
+        images = request.files.getlist('image')
+        for image in images:
+            image.save(os.path.join(os.path.dirname(__file__), '..', app.config['MEGADETECTOR_UPLOAD_FOLDER'], image.filename))
+        return render_template('megadetector.html')
+    else:
+        return render_template('megadetector.html')
+
+@bp.route('/run/batch', methods=['POST'])
+def megadetector_run_batch():
+    # Pick a folder to run MD on recursively, and an output file
+    image_folder = os.path.join(os.path.dirname(__file__), '..', app.config['MEGADETECTOR_UPLOAD_FOLDER'])
+    output_file = os.path.join(os.path.dirname(__file__), '..', app.config['MEGADETECTOR_MAIN_FOLDER'], 'output.json')
+    model = request.form.get('model')
+
+    # Install TensorFlow using pip if the model is MDV4 - may break things now
+    if model == 'MDV4':
+        subprocess.check_call([os.sys.executable, '-m', 'pip', 'install', 'tensorflow'])
+
+    image_file_names = path_utils.find_images(image_folder, recursive=True)
+
+    # Run the detector
+    results = load_and_run_detector_batch(model, image_file_names)
+    write_results_to_file(results,
+                        output_file,
+                        relative_path_base=image_folder)
+
+    # Postprocess the results into a HTML report
+    postprocess_results(output_file, image_folder)
+
+    return jsonify({"status": "success", "message": "Batch processing completed"})
+
+def postprocess_results(output_file, image_folder):
+    # Setup options
+    options = PostProcessingOptions()
+    options.md_results_file = output_file
+    options.image_base_dir = image_folder
+    options.output_dir = os.path.join(os.path.dirname(__file__), '..', 'templates', 'megadetector-results')
+
+    # Run the postprocessing and generate a HTML report
+    process_batch_results(options)
+
+@bp.route('/report')
+def megadetector_report():
+    return render_template('megadetector-results/index.html')
+
+@bp.route('/detections_animal.html')
+def megadetector_detections_animal_report():
+    return render_template('megadetector-results/detections_animal.html')
+
+@bp.route('/detections_animal/<path:filename>')
+def serve_detections_animal(filename):
+    # Serve from the templates directory
+    return send_from_directory(os.path.join(current_app.root_path, 'templates', 'megadetector-results', 'detections_animal'), filename)
+
+@bp.route('/static/megadetector/images/<path:filename>')
+def serve_megadetector_images(filename):
+    # Serve from the static directory
+    return send_from_directory(os.path.join(current_app.root_path, 'static', 'megadetector', 'images'), filename)
+
+@bp.route('/Users/<path:filepath>')
+def serve_user_files(filepath):
+    # This route handles the absolute paths in the HTML
+    if 'static' in filepath:
+        relative_path = filepath.split('static/')[-1]
+        return send_from_directory(os.path.join(current_app.root_path, 'static'), relative_path)
+    elif 'templates' in filepath:
+        relative_path = filepath.split('templates/')[-1]
+        return send_from_directory(os.path.join(current_app.root_path, 'templates'), relative_path)
+    return "File not found", 404
\ No newline at end of file
diff --git a/appstore/routes/trapper.py b/appstore/routes/trapper.py
index 3f99e0cad0742a0b3983af859506dee2600120de..367f4368f2b4a438e303116e833ac65d569b388a 100644
--- a/appstore/routes/trapper.py
+++ b/appstore/routes/trapper.py
@@ -1,8 +1,11 @@
 from flask import Blueprint, jsonify
-from appstore.utils import is_container_running, is_trapper_ready
+from appstore.utils import is_container_running, is_server_ready, get_app_metadata
 import subprocess
 import time
 import docker
+import git
+import os
+import shutil
 
 bp = Blueprint('trapper', __name__, url_prefix='/trapper')
 
@@ -10,11 +13,11 @@ bp = Blueprint('trapper', __name__, url_prefix='/trapper')
 def start_trapper():
     start_trapper_endpoint()
 
-    max_retries = 60  # Increased to allow more time
+    max_retries = 60
     retries = 0
     trapper_url = "http://0.0.0.0:8000/"
     while retries < max_retries:
-        if is_container_running('trapper') and is_trapper_ready(trapper_url):
+        if is_container_running('trapper') and is_server_ready(trapper_url):
             return jsonify({'status': 'running', 'url': trapper_url}), 200
         time.sleep(5)
         retries += 1
@@ -22,14 +25,46 @@ def start_trapper():
     return jsonify({'error': 'Failed to start Trapper', 'status': 'timeout'}), 500
 
 def start_trapper_endpoint():
-    trapper_start_path = '../trapper'
-    command = ['./start.sh', '-pb', 'dev']
-
+    app_metadata = get_app_metadata("Trapper")
+    repo_dir = f"/tmp/trapper"
+    
     try:
-        # Start the process and do not wait for it to complete
-        process = subprocess.Popen(command, cwd=trapper_start_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+        # clone the repository if doesn't exist
+        print("Checking if repository exists")
+        if not os.path.exists(repo_dir):
+            print("Cloning repository")
+            git.Repo.clone_from(app_metadata["repository_url"], repo_dir)
+        print("Repository eixsts: ", repo_dir)
+        os.chdir(repo_dir)
+
+        # Check if .env file exists
+        # TODO: amend Dockerfile.dev (add -o to line 53)
+        if not os.path.exists(".env"):
+            if os.path.exists("trapper.env"):
+                shutil.copy("trapper.env", ".env")
+                print("Created .env file from trapper.env")
+            else:
+                print("No trapper.env file found")
+                return False
+        else:
+            print(".env file already exists")
+
+        # Run the start command
+        print("Current directory:", os.getcwd())
+        command = app_metadata["start_command"].split()  # Split the command into a list
+        print(f"Executing command: {' '.join(command)}")
+        result = subprocess.Popen(command, cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+        
+        print(f"Command exit code: {result.returncode}")
+        print(f"Command stdout: {result.stdout}")
+        print(f"Command stderr: {result.stderr}")
+        
+        if result.returncode != 0:
+            return jsonify({'error': 'Trapper start command failed', 'details': result.stderr}), 500
+        
         return jsonify({'message': 'Trapper start initiated'}), 202
     except Exception as e:
+        print(f"Exception occurred: {str(e)}")
         return jsonify({'error': 'Failed to start Trapper', 'details': str(e)}), 500
 
 @bp.route('/logs', methods=['GET'])
diff --git a/appstore/static/.DS_Store b/appstore/static/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..810f04c794565425909e6e8ae9197d3a3bf5e5a3
Binary files /dev/null and b/appstore/static/.DS_Store differ
diff --git a/appstore/static/megadetector/.DS_Store b/appstore/static/megadetector/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..87df1a30da74924da1c3ae450c3fa0b4e6cd0df8
Binary files /dev/null and b/appstore/static/megadetector/.DS_Store differ
diff --git a/appstore/static/megadetector/images/.DS_Store b/appstore/static/megadetector/images/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6
Binary files /dev/null and b/appstore/static/megadetector/images/.DS_Store differ
diff --git a/appstore/static/megadetector/images/nz1.jpg b/appstore/static/megadetector/images/nz1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b210c390b4196518c59c4704741a186efb4d3edb
Binary files /dev/null and b/appstore/static/megadetector/images/nz1.jpg differ
diff --git a/appstore/static/megadetector/images/nz2.jpg b/appstore/static/megadetector/images/nz2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..7ca059fa6ec4fad884996f4196a66f4221725823
Binary files /dev/null and b/appstore/static/megadetector/images/nz2.jpg differ
diff --git a/appstore/static/megadetector/images/sea_star_sample_image_800.jpg b/appstore/static/megadetector/images/sea_star_sample_image_800.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..36446e56d6ce5205e923df5b7e0cc5bab7e176bf
Binary files /dev/null and b/appstore/static/megadetector/images/sea_star_sample_image_800.jpg differ
diff --git a/appstore/static/megadetector/output.json b/appstore/static/megadetector/output.json
new file mode 100644
index 0000000000000000000000000000000000000000..73ac199f6cfb811dbccb935e3c376c92bf726343
--- /dev/null
+++ b/appstore/static/megadetector/output.json
@@ -0,0 +1,94 @@
+{
+ "images": [
+  {
+   "file": "nz1.jpg",
+   "detections": [
+    {
+     "category": "1",
+     "conf": 0.0119,
+     "bbox": [
+      0.5414,
+      0.3274,
+      0.3871,
+      0.467
+     ]
+    },
+    {
+     "category": "1",
+     "conf": 0.892,
+     "bbox": [
+      0.3042,
+      0.1294,
+      0.3585,
+      0.7106
+     ]
+    }
+   ]
+  },
+  {
+   "file": "nz2.jpg",
+   "detections": [
+    {
+     "category": "1",
+     "conf": 0.758,
+     "bbox": [
+      0.5013,
+      0.4493,
+      0.2909,
+      0.2619
+     ]
+    }
+   ]
+  },
+  {
+   "file": "sea_star_sample_image_800.jpg",
+   "detections": [
+    {
+     "category": "3",
+     "conf": 0.00617,
+     "bbox": [
+      0,
+      0,
+      0.2962,
+      0.985
+     ]
+    },
+    {
+     "category": "1",
+     "conf": 0.0181,
+     "bbox": [
+      0.001249,
+      0,
+      0.3062,
+      0.9983
+     ]
+    },
+    {
+     "category": "1",
+     "conf": 0.963,
+     "bbox": [
+      0.1512,
+      0.03499,
+      0.6537,
+      0.8866
+     ]
+    }
+   ]
+  }
+ ],
+ "detection_categories": {
+  "1": "animal",
+  "2": "person",
+  "3": "vehicle"
+ },
+ "info": {
+  "detection_completion_time": "2024-08-16 18:11:21",
+  "format_version": "1.3",
+  "detector": "unknown",
+  "detector_metadata": {
+   "megadetector_version": "unknown",
+   "typical_detection_threshold": 0.5,
+   "conservative_detection_threshold": 0.25
+  }
+ }
+}
\ No newline at end of file
diff --git a/appstore/templates/index.html b/appstore/templates/index.html
index 9589da972d5f7b284d1807ffaeb890abaf0e863c..f301de54c03bbdc459d07d85c721bb139b267fc7 100644
--- a/appstore/templates/index.html
+++ b/appstore/templates/index.html
@@ -4,9 +4,10 @@
         <title>Home Page</title>
     </head>
     <body>
-        <h1>Hello, world!</h1>
+        <h1>Camera Trapper App Store</h1>
         <button id="animl" onclick="startAniml()">Animl</button>
         <button id="zamba" onclick="window.location.href='/zamba'">Zamba</button>
+        <button id="megadetector" onclick="window.location.href='/megadetector'">MegaDetector</button>
         <button id="trapper" onclick="startTrapper()">Trapper</button>
         <div id="loading" style="display: none;">Loading Trapper...</div>
 
@@ -19,7 +20,13 @@
                     }
                 })
                 .then(response => response.json())
-                .then(data => console.log(data))
+                .then(data => {
+                    if (data.status === 'running') {
+                        window.location.href = data.url;
+                    } else {
+                        console.error('Failed to start Animl:', data.error);
+                    }
+                })
                 .catch(error => console.error('Error:', error));
             }
             function startTrapper() {
diff --git a/appstore/templates/megadetector-results/detections_animal.html b/appstore/templates/megadetector-results/detections_animal.html
new file mode 100644
index 0000000000000000000000000000000000000000..0f81481f92740e5bc9e150acfa84c95ef4d1ce5a
--- /dev/null
+++ b/appstore/templates/megadetector-results/detections_animal.html
@@ -0,0 +1,8 @@
+<html><body>
+<h1>DETECTIONS_ANIMAL</h1><p style="font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5"><b>Result type</b>: detections_animal, <b>Image</b>: nz1.jpg, <b>Max conf</b>: 0.892</p>
+<a href="/Users/jesslam/Desktop/CMT403_Dissertation/AppStore/appstore/routes/../static/megadetector/images/nz1.jpg"><img src="detections_animal/detections_animal_nz1.jpg" style="margin:0px;margin-top:5px;margin-bottom:5px;">
+</a><br/><p style="font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5"><b>Result type</b>: detections_animal, <b>Image</b>: nz2.jpg, <b>Max conf</b>: 0.758</p>
+<a href="/Users/jesslam/Desktop/CMT403_Dissertation/AppStore/appstore/routes/../static/megadetector/images/nz2.jpg"><img src="detections_animal/detections_animal_nz2.jpg" style="margin:0px;margin-top:5px;margin-bottom:5px;">
+</a><br/><p style="font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5"><b>Result type</b>: detections_animal, <b>Image</b>: sea_star_sample_image_800.jpg, <b>Max conf</b>: 0.963</p>
+<a href="/Users/jesslam/Desktop/CMT403_Dissertation/AppStore/appstore/routes/../static/megadetector/images/sea_star_sample_image_800.jpg"><img src="detections_animal/detections_animal_sea_star_sample_image_800.jpg" style="margin:0px;margin-top:5px;margin-bottom:5px;">
+</a></body></html>
diff --git a/appstore/templates/megadetector-results/detections_animal/detections_animal_nz1.jpg b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b740f8d80b95d1f5e87124eab11e510c06a9a3a1
Binary files /dev/null and b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz1.jpg differ
diff --git a/appstore/templates/megadetector-results/detections_animal/detections_animal_nz2.jpg b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..8013ef481012e510023171e37ed3c49ef0bd6658
Binary files /dev/null and b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz2.jpg differ
diff --git a/appstore/templates/megadetector-results/detections_animal/detections_animal_sea_star_sample_image_800.jpg b/appstore/templates/megadetector-results/detections_animal/detections_animal_sea_star_sample_image_800.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..788d7bec6a3b3cc6fe4af35591ba55c52359cf8e
Binary files /dev/null and b/appstore/templates/megadetector-results/detections_animal/detections_animal_sea_star_sample_image_800.jpg differ
diff --git a/appstore/templates/megadetector-results/index.html b/appstore/templates/megadetector-results/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..faa7343b353c4709dd5b7e3f94d561d3050cab8c
--- /dev/null
+++ b/appstore/templates/megadetector-results/index.html
@@ -0,0 +1,26 @@
+<html>
+<head>
+        <style type="text/css">
+        a { text-decoration: none; }
+        body { font-family: segoe ui, calibri, "trebuchet ms", verdana, arial, sans-serif; }
+        div.contentdiv { margin-left: 20px; }
+        </style>
+        </head>
+<body>
+
+        <h2>Visualization of results for output.json</h2>
+
+        <p>A sample of 3 images (of 3 total) (0 failures), annotated with detections above confidence 50.00%.</p>
+
+        
+        <div class="contentdiv">
+        <p>Model version: unknown</p>
+        </div>
+        
+        <h3>Sample images</h3>
+
+        <div class="contentdiv">
+<a href="detections_animal.html">Detections: animal</a> (3, 100.0%)<br/>
+Non-detections (0, 0.0%)<br/>
+</div>
+</body></html>
\ No newline at end of file
diff --git a/appstore/templates/megadetector.html b/appstore/templates/megadetector.html
new file mode 100644
index 0000000000000000000000000000000000000000..ba66f88bbd6ea42220b13077ef644a9d6176c3cb
--- /dev/null
+++ b/appstore/templates/megadetector.html
@@ -0,0 +1,63 @@
+<DOCTYPE html>
+    <html>
+        <head>
+            <title>MegaDetector</title>
+            <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+        </head>
+        <body>
+            <h1>MegaDetector</h1>
+            <p>MegaDetector is an AI model that identifies animals, people, and vehicles in camera trap images (which also makes it useful for eliminating blank images).</p>
+
+            <form action="/megadetector" method="post" enctype="multipart/form-data">
+                <label for="image">Upload an image:</label>
+                <input type="file" name="image" id="image" accept="image/*" multiple>
+                <br>
+                <input type="submit" value="Upload">
+            </form>
+            <br>
+            <form action="/megadetector/run/batch" method="post" enctype="multipart/form-data" id="batchForm">
+                <label for="model">Select a model:</label>
+                <select name="model" id="model">
+                    <option value="MDV5A" selected>MDv5a</option>
+                    <option value="MDV5B">MDv5b</option>
+                    <option value="MDV4">MDv4</option>
+                </select>
+                <br>
+                <div id="warning" style="color: red; font-weight: bold; display: none;">
+                    <p>Warning: MDv4 is not recommended, please only select this if you have a really good reason to do so.</p>
+                </div>
+                <input type="submit" value="Run Batch">
+            </form>
+
+            <button onclick="window.location.href='/megadetector/report'">View Report</button>
+
+            <script>
+                document.getElementById('model').addEventListener('change', function() {
+                    const modelFileInput = document.getElementById('warning');
+                    if (this.value === 'MDV4') {
+                        modelFileInput.style.display = 'block';
+                    } else {
+                        modelFileInput.style.display = 'none';
+                    }
+                });
+
+                document.getElementById('batchForm').addEventListener('submit', function(e) {
+                    e.preventDefault();
+                    const formData = new FormData(this);
+                    
+                    fetch('/megadetector/run/batch', {
+                        method: 'POST',
+                        body: formData
+                    })
+                    .then(response => response.json())
+                    .then(data => {
+                        console.log(data);
+                    })
+                    .catch(error => {
+                        console.error('Error:', error);
+                    });
+                });
+            </script>
+        </body>
+    </html>
+</DOCTYPE>
\ No newline at end of file
diff --git a/appstore/utils.py b/appstore/utils.py
index 7217df801134e7ab7b5772c465f0616752028760..352952959301a7fe27c9b0393adc12fedd7acb07 100644
--- a/appstore/utils.py
+++ b/appstore/utils.py
@@ -4,9 +4,75 @@ from appstore import celery
 import subprocess
 import json
 import os
+import git
 import pandas as pd
 from sqlalchemy import text
 from appstore import connection, engine
+from appstore.models import AppMetadata
+
+# variables
+trapper_metadata = AppMetadata(
+    name="Trapper",
+    description="Trapper Expert - core web application for camera trap data management",
+    repository_url="https://gitlab.com/trapper-project/trapper.git",
+    docker_compose_file="docker-compose.yml",
+    start_command="./start.sh -pb dev",
+    stop_command="./start.sh prod stop"
+)
+animl_metadata = AppMetadata(
+    name="Animl",
+    description="Animl - core web application for animal detection",
+    repository_url="https://github.com/tnc-ca-geo/animl-frontend.git",
+    docker_image="registry.git.cf.ac.uk/c22097859/c22097859_cmt403_dissertation/animl:latest",
+    start_command="docker run -p 5173:5173 animl-container animl",
+    stop_command="docker stop animl-container"
+)
+
+# functions
+def create_app_metadata(app_metadata):
+    with engine.connect() as connection:
+        connection.execute(AppMetadata.__table__.insert().values(
+            name=app_metadata.name,
+            description=app_metadata.description,
+            repository_url=app_metadata.repository_url,
+            docker_compose_file=app_metadata.docker_compose_file,
+            docker_image=app_metadata.docker_image,
+            start_command=app_metadata.start_command,
+            stop_command=app_metadata.stop_command
+        ))
+
+def get_app_metadata(app_name):
+    with engine.connect() as connection:
+        result = connection.execute(text("SELECT * FROM app_metadata WHERE name = :name"), {"name": app_name}).fetchone()
+        return {
+            "name": result[1],
+            "description": result[2],
+            "repository_url": result[3],
+            "docker_compose_file": result[4],
+            "docker_image": result[5],
+            "start_command": result[6],
+            "stop_command": result[7]
+        }
+
+def is_container_exist(container_name):
+    client = docker.from_env()
+    try:
+        containers = client.containers.list(filters={'name': container_name})
+        return any(container.status == 'running' for container in containers)
+    except Exception as e:
+        print(f"Error checking container status: {e}")
+        return False
+
+def is_container_running_by_name(container_name):
+    client = docker.from_env()
+    try:
+        container = client.containers.get(container_name)
+        return container.status == 'running'
+    except docker.errors.NotFound:
+        return False
+    except Exception as e:
+        print(f"Error checking container status: {e}")
+        return False
 
 def is_container_running(service_name):
     client = docker.from_env()
@@ -17,13 +83,14 @@ def is_container_running(service_name):
         print(f"Error checking container status: {e}")
         return False
 
-def is_trapper_ready(url, timeout=5):
+def is_server_ready(url, timeout=5):
     try:
         response = requests.get(url, timeout=timeout)
         return response.status_code == 200
     except requests.RequestException:
         return False
 
+# celery tasks
 @celery.task(name='train_zamba_task')
 def train_zamba_task(model, dryRun, labels, data_dir='appstore/static/zamba/train/videos'):
     command = ['zamba', 'train', '--data-dir', data_dir, '--labels', labels, '--model', model, '-y']
diff --git a/docker-compose.yaml b/docker-compose.yaml
deleted file mode 100644
index f07ad548eda2f1af6f7fe30d20ef6560ec392e0e..0000000000000000000000000000000000000000
--- a/docker-compose.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-version: '1'
-services:
-  app:
-    image: appstore
-    ports:
-      - 5001:5000
-    volumes:
-      - ./app:/app
-    environment:
-      - ENV_VARIABLE=value
-  trapper:
-    image: registry.gitlab.com/trapper-project/trapper:latest
-    
\ No newline at end of file
diff --git a/trapper b/trapper
deleted file mode 160000
index 767818867054984c448f3e33fba92177d2248ff5..0000000000000000000000000000000000000000
--- a/trapper
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 767818867054984c448f3e33fba92177d2248ff5