diff --git a/mo/db.py b/mo/db.py
index 758b8ae59acfb03aed1f9944a24d6cdb9c345603..fe74c77135807e40f67e3c5ee9900ead88cbc7bd 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -470,6 +470,17 @@ class JobState(MOEnum):
     done = auto()
     failed = auto()
 
+    def friendly_name(self) -> str:
+        return job_state_names[self]
+
+
+job_state_names = {
+    JobState.ready: 'čeká na spuštění',
+    JobState.running: 'zpracovává se',
+    JobState.done: 'dokončen',
+    JobState.failed: 'selhal',
+}
+
 
 class Job(Base):
     __tablename__ = 'jobs'
diff --git a/mo/web/__init__.py b/mo/web/__init__.py
index e3334be71a0a0ae64796dc7da95d0d265128fb00..e5261a5d432fe311de52b7056ec9e8edd6868736 100644
--- a/mo/web/__init__.py
+++ b/mo/web/__init__.py
@@ -127,6 +127,7 @@ import mo.web.menu
 import mo.web.misc
 import mo.web.org
 import mo.web.org_contest
+import mo.web.org_jobs
 import mo.web.org_place
 import mo.web.org_round
 import mo.web.org_users
diff --git a/mo/web/org_jobs.py b/mo/web/org_jobs.py
new file mode 100644
index 0000000000000000000000000000000000000000..30d61eeb0802c28232b9882aaae807d0b768bc8e
--- /dev/null
+++ b/mo/web/org_jobs.py
@@ -0,0 +1,66 @@
+from flask import render_template, g, redirect, url_for, flash
+from flask_wtf.form import FlaskForm
+from sqlalchemy.orm import joinedload
+import werkzeug.exceptions
+import wtforms
+
+import mo
+import mo.db as db
+from mo.jobs import TheJob
+from mo.web import app
+import mo.web.util
+
+
+class JobDeleteForm(FlaskForm):
+    delete_job_id = wtforms.IntegerField()
+    delete = wtforms.SubmitField('Smazat')
+
+
+@app.route('/org/jobs/', methods=('GET', 'POST'))
+def org_jobs():
+    sess = db.get_session()
+
+    form_delete_job = JobDeleteForm()
+    if form_delete_job.validate_on_submit():
+        tj = TheJob(form_delete_job.delete_job_id.data)
+        job = tj.load()
+        if not job:
+            flash('Dávka mezitím zmizela.', 'success')
+        elif not (job.user_id == g.user.user_id or g.user.is_admin):
+            flash('Tuto dávku nemáte právo smazat', 'danger')
+        elif job.state == db.JobState.running:
+            flash('Běžící dávku nelze smazat', 'danger')
+        else:
+            tj.remove_loaded()
+            flash('Dávka smazána', 'success')
+        return redirect(url_for('org_jobs'))
+
+    job_query = (sess.query(db.Job)
+                 .options(joinedload(db.Job.user)))
+    if not g.user.is_admin:
+        job_query = job_query.filter_by(user=g.user)
+
+    jobs = job_query.order_by(db.Job.created_at.desc()).all()
+
+    return render_template(
+        'org_jobs.html',
+        jobs=jobs,
+        db=db,
+        form_delete_job=form_delete_job,
+    )
+
+
+@app.route('/org/jobs/<int:id>/output')
+def org_job_output(id: int):
+    sess = db.get_session()
+    job = sess.query(db.Job).get(id)
+    if job is None:
+        return werkzeug.exceptions.NotFound()
+
+    if not (job.user_id == g.user.user_id or g.user.is_org):
+        return werkzeug.exceptions.Forbidden()
+
+    if job.state != db.JobState.done or job.out_file is None:
+        return werkzeug.exceptions.NotFound()
+
+    return mo.web.util.send_job_result(job)
diff --git a/mo/web/templates/org_jobs.html b/mo/web/templates/org_jobs.html
new file mode 100644
index 0000000000000000000000000000000000000000..6128122fe10370e07ff950281dbd0389d6131e37
--- /dev/null
+++ b/mo/web/templates/org_jobs.html
@@ -0,0 +1,48 @@
+{% extends "base.html" %}
+{% block body %}
+<h2>Dávky ke zpracování</h2>
+
+<p>Činnosti, jejichž zpracování může trvat delší dobu, se zpracovávají
+dávkově. To se týká třeba stahování více řešení najednou – když si je
+objednáte, vytvoří se dávka. Až dávka doběhne, můžete si zde stáhnout výsledek.
+Po nějaké době výsledek vyprší a je automaticky smazán. Budeme rádi, když
+dávku po stažení výstupu smažete sami – šetří to místo na serveru.
+
+<h3>Seznam dávek</h3>
+
+{% if jobs %}
+<table class=data>
+	<thead>
+		<tr>
+			<th>ID
+			<th>Název
+			<th>Spuštěno
+			<th>Stav
+			<th>Vyprší
+			{% if g.user.is_admin %}<th>Vlastník{% endif %}
+			<th>Akce
+	<tbody>
+	{% for j in jobs %}
+		<tr class="job-{{ j.state.name }}">
+			<td>{{ j.job_id }}
+			<td>{{ j.description }}
+			<td>{{ j.created_at|timeformat }}
+			<td><b>{{ j.state.friendly_name() }}</b>{% if j.finished_at != None %} {{ j.finished_at|timedelta }}{% endif %}
+			<td>{% if j.expires_at %}{{ j.expires_at|timedelta}}{% endif %}
+			{% if g.user.is_admin %}<td>{{ j.user.full_name() }}{% endif %}
+			<td>
+				<div class='btn-group'><form action="" method="POST" class="btn-group">
+					{{ form_delete_job.csrf_token() }}
+				{% if j.out_file %}
+					<a class='btn btn-xs btn-primary' href='{{ url_for('org_job_output', id=j.job_id) }}'>Výsledek</a>
+				{% endif %}
+					<input type="hidden" name="delete_job_id" value="{{ j.job_id }}">
+					<button type="submit" class="btn btn-xs btn-danger">Smazat</button>
+				</form></div>
+	{% endfor %}
+</table>
+{% else %}
+<p>Žádné dávky nejsou nyní naplánované.
+{% endif %}
+
+{% endblock %}
diff --git a/mo/web/util.py b/mo/web/util.py
index cb356f6e800ae88ef84489d7b7a5eb3a302d0423..9aeeabbf84df08cb15b4b9b96aa83f3eff815415 100644
--- a/mo/web/util.py
+++ b/mo/web/util.py
@@ -8,6 +8,7 @@ import werkzeug.utils
 import wtforms
 
 import mo.db as db
+import mo.jobs
 from mo.util import logger
 from mo.web import app
 
@@ -72,3 +73,19 @@ def send_task_paper(paper: db.Paper) -> Response:
     else:
         logger.error(f'Soubor {file} je v papers, ale ve FS neexistuje')
         raise werkzeug.exceptions.NotFound()
+
+
+def send_job_result(job: db.Job) -> Response:
+    assert job.out_file is not None
+    file = mo.jobs.job_file_path(job.out_file)
+
+    if file.endswith('.zip'):
+        type = 'application/zip'
+    else:
+        type = 'application/binary'
+
+    if os.path.isfile(file):
+        return send_file(file, mimetype=type)
+    else:
+        logger.error(f'Soubor {file} je výsledkem jobu, ale ve FS neexistuje')
+        raise werkzeug.exceptions.NotFound()
diff --git a/static/mo.css b/static/mo.css
index 02f61fa5d4a9fa050ed482f427e830735f8a4fe4..422daf27c033ad596c58a49af509323a75accffd 100644
--- a/static/mo.css
+++ b/static/mo.css
@@ -160,3 +160,29 @@ nav#main-menu a.active {
 .sol-late {
 	color: red;
 }
+
+/* Jobs */
+
+table.data tbody tr.job-running {
+	background-color: #f8f;
+}
+
+table.data tbody tr.job-running:hover {
+	background-color: #a6a;
+}
+
+table.data tbody tr.job-done {
+	background-color: #8f8;
+}
+
+table.data tbody tr.job-done:hover {
+	background-color: #6a6;
+}
+
+table.data tbody tr.job-failed {
+	background-color: #f88;
+}
+
+table.data tbody tr.job-failed:hover {
+	background-color: #a66;
+}