diff --git a/bin/fix-submits b/bin/fix-submits
index 9fccdc73e2cf501b8e8e2e00acbeefd0a7311842..8bde2641512ea89c6d7bcd05fc45a3de698707f4 100755
--- a/bin/fix-submits
+++ b/bin/fix-submits
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 
 import argparse
+import datetime
 import os
 from sqlalchemy.orm import joinedload
 import subprocess
@@ -29,7 +30,7 @@ def fix_paper(id: int):
              .get(id))
     assert paper is not None
     assert paper.orig_file_name is not None
-    print(f"=== Paper #{id} ({paper.orig_file_name})")
+    print(f"=== Paper #{id} ({paper.orig_file_name}){' [RETRY]' if paper.fixed_at else ''}")
 
     tmp_file = tempfile.NamedTemporaryFile(dir=mo.util.data_dir('tmp'), prefix='fix-')
     res = subprocess.run(['qpdf', os.path.join(mo.util.data_dir('submits'), paper.orig_file_name), tmp_file.name])
@@ -37,16 +38,16 @@ def fix_paper(id: int):
         sub = mo.submit.Submitter()
         try:
             sub.submit_fix(paper, tmp_file.name)
-            sess.commit()
             fixed += 1
         except mo.submit.SubmitException:
-            sess.rollback()
             errors += 1
     else:
         print(f'--> ERROR: qpdf failed with exit code {res.returncode}')
-        sess.rollback()
         errors += 1
 
+    paper.fixed_at = datetime.datetime.now()
+    sess.commit()
+
 
 if args.id is not None:
     fix_paper(args.id)
diff --git a/constraints.txt b/constraints.txt
index 0fba56cb92a81b77389414ce6b606e64914e0228..ffbd91dfd5c61e59d173287e87cc557da30cedea 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -24,6 +24,7 @@ pycparser==2.20
 pyparsing==2.4.7
 PyPDF2==1.26.0
 python-dateutil==2.8.1
+python-magic==0.4.24
 python-poppler==0.2.2
 pytz==2020.5
 pyzbar==0.1.8
diff --git a/db/db.ddl b/db/db.ddl
index 3709f66c44c519f7a0bc3ffa1783bfa30c61b6c2..378c9d9e3c41fe291c43f032ab993f0387d3ed9c 100644
--- a/db/db.ddl
+++ b/db/db.ddl
@@ -31,6 +31,12 @@ CREATE TABLE users (
 	email_notify	boolean		NOT NULL DEFAULT true		-- přeje si dostávat mailové notifikace
 );
 
+-- Uživatel s user_id=0 vždy existuje a je to systémový uživatel s právy admina.
+-- Nemá nastavené heslo, takže se na něj nejde přihlásit.
+-- Nelze mu ani heslo resetovat (to nejde adminům obecně), ani poslat pozvánku (má last_login_at).
+INSERT INTO users(user_id, email, first_name, last_name, is_admin, email_notify, last_login_at, note)
+	VALUES(0, 'system', 'Systém', 'OSMO', true, false, NOW(), 'Systémový uživatel');
+
 -- Hierarchie regionů a organizací
 
 CREATE TYPE place_type AS ENUM (
@@ -219,11 +225,13 @@ CREATE TABLE papers (
 	bytes		int		DEFAULT NULL,			-- velikost souboru
 	file_name	varchar(255)	DEFAULT NULL,			-- relativní cesta k souboru
 	orig_file_name	varchar(255)	DEFAULT NULL,			-- původní cesta, pokud PDF bylo poničené
-	note		text		NOT NULL DEFAULT ''		-- komentář uploadujícího
+	note		text		NOT NULL DEFAULT '',		-- komentář uploadujícího
+	fixed_at	timestamp with time zone	DEFAULT NULL
 	-- Sémantika práce s poničenými soubory:
 	--    - správná PDF mají orig_file_name=NULL
 	--    - pokud někdo odevzdá poničené, vyplní se orig_file_name, ale file_name=NULL
-	--    - časem se spustí oprava, která vyplní i file_name a přepočítá bytes
+	--    - časem se spustí oprava, která vyplní i file_name, přepočítá bytes a nastaví fixed_at
+	--    - pokud oprava selže, nastaví pouze fixed_at
 );
 
 CREATE INDEX papers_for_task_index ON papers (for_task);
@@ -312,7 +320,8 @@ CREATE TYPE job_type AS ENUM (
 	'download_submits',
 	'upload_feedback',
 	'create_protocols',
-	'process_scans'
+	'process_scans',
+	'fix_submits'
 );
 
 CREATE TYPE job_state AS ENUM (
@@ -320,7 +329,8 @@ CREATE TYPE job_state AS ENUM (
 	'ready',
 	'running',
 	'done',				-- Hotovo, out_json a out_file jsou platné
-	'failed'			-- Interní chyba při zpracování, viz log
+	'failed',			-- Chyba při zpracování
+	'internal_error'		-- Interní chyba při zpracování (viz log)
 );
 
 CREATE TABLE jobs (
diff --git a/db/upgrade-20211130.sql b/db/upgrade-20211130.sql
new file mode 100644
index 0000000000000000000000000000000000000000..462a1726220b1ba89d490b91bc029ece86be29fd
--- /dev/null
+++ b/db/upgrade-20211130.sql
@@ -0,0 +1,11 @@
+SET ROLE 'mo_osmo';
+
+ALTER TABLE papers ADD COLUMN
+	fixed_at	timestamp with time zone	DEFAULT NULL;
+
+INSERT INTO users(user_id, email, first_name, last_name, is_admin, email_notify, last_login_at, note)
+	VALUES(0, 'system', 'Systém', 'OSMO', true, false, NOW(), 'Systémový uživatel');
+
+ALTER TYPE job_state ADD VALUE 'internal_error';
+
+ALTER TYPE job_type ADD VALUE 'fix_submits';
diff --git a/mo/db.py b/mo/db.py
index ed3bcd3c872283fffc7faf25d159b1bdd476dd2d..226aef6905462a5566102a38536c6937483aa908 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -140,7 +140,7 @@ class Place(Base):
 
 
 def get_root_place() -> Place:
-    """Obvykle voláme mo.rights.Gatekeeper.get_root_place(), kterékešuje."""
+    """Obvykle voláme mo.rights.Gatekeeper.get_root_place(), které kešuje."""
     return get_session().query(Place).filter_by(parent=None).one()
 
 
@@ -373,6 +373,13 @@ class User(Base):
         return self.password_hash is None
 
 
+def get_system_user() -> User:
+    """Uživatel s user_id=0 je systémový (viz db.ddl)"""
+    user = get_session().query(User).get(0)
+    assert user is not None
+    return user
+
+
 class Contest(Base):
     __tablename__ = 'contests'
     __table_args__ = (
@@ -629,6 +636,7 @@ class Paper(Base):
     file_name = Column(String(255))
     orig_file_name = Column(String(255))
     note = Column(Text, nullable=False, server_default=text("''::text"))
+    fixed_at = Column(DateTime(True))
 
     task = relationship('Task')
     for_user_obj = relationship('User', primaryjoin='Paper.for_user == User.user_id')
@@ -650,6 +658,9 @@ class Paper(Base):
     def is_broken(self) -> bool:
         return self.file_name is None
 
+    def is_nonfixable(self) -> bool:
+        return self.is_broken() and self.fixed_at is not None
+
     def is_fixed(self) -> bool:
         return self.orig_file_name is not None and self.file_name is not None
 
@@ -692,6 +703,7 @@ class JobType(MOEnum):
     create_protocols = auto()
     process_scans = auto()
     sort_scans = auto()
+    fix_submits = auto()
 
 
 class JobState(MOEnum):
@@ -700,6 +712,7 @@ class JobState(MOEnum):
     running = auto()
     done = auto()
     failed = auto()
+    internal_error = auto()
 
     def friendly_name(self) -> str:
         return job_state_names[self]
@@ -711,6 +724,7 @@ job_state_names = {
     JobState.running: 'zpracovává se',
     JobState.done: 'dokončena',
     JobState.failed: 'selhala',
+    JobState.internal_error: 'interní chyba',
 }
 
 
diff --git a/mo/jobs/__init__.py b/mo/jobs/__init__.py
index 2d7da1a06762287ec011e153a8292657d6acbaab..71fd12966ebae07d2192cfeab29bebcabf1b25b2 100644
--- a/mo/jobs/__init__.py
+++ b/mo/jobs/__init__.py
@@ -136,6 +136,9 @@ class TheJob:
         job.state = db.JobState.running
         job.finished_at = None
         job.expires_at = None
+        job.result = ""
+        job.out_json = None
+        job.out_file = None
         sess.commit()
 
         try:
@@ -144,7 +147,11 @@ class TheJob:
             _handler_table[job.type](self)
             if self.errors:
                 logger.info(f'Job: Neúspěšně dokončen job #{job.job_id} ({job.result})')
-                job.state = db.JobState.failed
+                if job.user_id == 0:
+                    # Joby běžící na systémového uživatele produkují interní chyby
+                    job.state = db.JobState.internal_error
+                else:
+                    job.state = db.JobState.failed
                 job.out_json = {'errors': self.errors}
                 if job.result == "":
                     job.result = 'Došlo k chybám, viz detail'
@@ -153,11 +160,12 @@ class TheJob:
                 job.state = db.JobState.done
         except Exception as e:
             logger.error(f'Job: Chyba při zpracování jobu #{job.job_id}: %s', e, exc_info=e)
-            job.state = db.JobState.failed
+            job.state = db.JobState.internal_error
             job.result = 'Interní chyba, informujte prosím správce systému.'
 
         job.finished_at = mo.util.get_now()
-        job.expires_at = job.finished_at + timedelta(minutes=self.expires_in_minutes)
+        if job.state != db.JobState.internal_error:
+            job.expires_at = job.finished_at + timedelta(minutes=self.expires_in_minutes)
         sess.commit()
 
 
diff --git a/mo/jobs/protocols.py b/mo/jobs/protocols.py
index 65b6b9e7d8e3970cfd63371be3697d9daf306d6d..ae9e8a7360e2275cf968ee6fbb519a7e3e53bb5c 100644
--- a/mo/jobs/protocols.py
+++ b/mo/jobs/protocols.py
@@ -1,7 +1,7 @@
 # Implementace jobů na práci s protokoly
 
 from PIL import Image
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 import multiprocessing
 import os
 import poppler
@@ -218,6 +218,12 @@ class ScanJobPage:
     code: Optional[str]
 
 
+@dataclass
+class ScanJobResult:
+    pages: List[ScanJobPage] = field(default_factory=list)
+    error: Optional[str] = None
+
+
 @job_handler(db.JobType.process_scans)
 def handle_process_scans(the_job: TheJob):
     job = the_job.job
@@ -291,7 +297,11 @@ def handle_process_scans(the_job: TheJob):
     num_pages = 0
     for fi, fn in enumerate(in_files):
         prev_page: Optional[db.ScanPage] = None
-        for pi, pr in enumerate(results[fi]):
+        res = results[fi]
+        if res.error:
+            the_job.error(f'{fn}: {res.error}')
+            return
+        for pi, pr in enumerate(res.pages):
             sp = db.ScanPage(
                 job_id=job.job_id,
                 file_nr=fi,
@@ -321,19 +331,23 @@ def handle_process_scans(the_job: TheJob):
     the_job.expires_in_minutes = config.JOB_EXPIRATION_LONG
 
 
-def _process_scan_file(args: ScanJobArgs) -> List[ScanJobPage]:
+def _process_scan_file(args: ScanJobArgs) -> ScanJobResult:
     # Zpracuje jeden soubor se skeny. Běží v odděleném procesu.
 
-    # FIXME: Ošetření chyb
+    res = ScanJobResult()
     logger.debug(f'Scan: Analyzuji soubor {args.in_path}')
     pdf = poppler.load_from_file(args.in_path)
+    if not pdf._document:
+        # XXX: Poppler neumí hlásit chybu při otevírání dokumentu (https://github.com/cbrunet/python-poppler/issues/48)
+        # Tak zatím saháme dovnitř a detekujeme si ji sami.
+        res.error = 'Soubor není ve formátu PDF'
+        return res
 
     renderer = poppler.PageRenderer()
     renderer.set_render_hint(poppler.RenderHint.antialiasing, True)
     renderer.set_render_hint(poppler.RenderHint.text_antialiasing, True)
     dpi = 300
 
-    output = []
     for page_nr in range(pdf.pages):
         page = pdf.create_page(page_nr)
         page_img = renderer.render_page(page, xres=dpi, yres=dpi)
@@ -360,7 +374,7 @@ def _process_scan_file(args: ScanJobArgs) -> List[ScanJobPage]:
             qr = code.data.decode('US-ASCII')
             # FIXME: Tady by se dala podle kódu otočit stránka
 
-        output.append(ScanJobPage(code=qr))
+        res.pages.append(ScanJobPage(code=qr))
 
         full_img.save(f'{args.out_prefix}-{page_nr:04d}-full.png')
 
@@ -369,7 +383,7 @@ def _process_scan_file(args: ScanJobArgs) -> List[ScanJobPage]:
 
         logger.debug(f'Scan: Strana #{page_nr}: {qr}')
 
-    return output
+    return res
 
 
 #
diff --git a/mo/jobs/submit.py b/mo/jobs/submit.py
index 136d17f07449464558216267a80c2ea988f23d24..57ca8b41d57f720dbcab1fdd832bc78b510800da 100644
--- a/mo/jobs/submit.py
+++ b/mo/jobs/submit.py
@@ -1,11 +1,13 @@
 # Implementace jobů pracujících se submity
 
 from dataclasses import dataclass
+import datetime
 import os
 import re
 import shutil
 from sqlalchemy import and_
 from sqlalchemy.orm import joinedload
+import subprocess
 from tempfile import NamedTemporaryFile
 from typing import List, Optional
 import unicodedata
@@ -311,3 +313,77 @@ def handle_upload_feedback(the_job: TheJob):
                        + inflect_by_number(len(the_job.errors), 'nastala', 'nastaly', 'nastalo')
                        + ' '
                        + inflect_number(len(the_job.errors), 'chyba', 'chyby', 'chyb'))
+
+
+#
+# Job fix_submits: Opravuje rozbité submity
+#
+# Vstupní JSON:
+#        { 'papers': [ seznam paper_id k opravení ],
+#        }
+#
+# Výstupní JSON:
+#        null
+#
+
+
+def check_broken_submits() -> None:
+    sess = db.get_session()
+    papers = sess.query(db.Paper).filter_by(file_name=None, fixed_at=None).all()
+
+    if not papers:
+        return
+
+    the_job = TheJob()
+    job = the_job.create(db.JobType.fix_submits, db.get_system_user())
+    job.description = 'Oprava rozbitých protokolů'
+    job.in_json = {'papers': [p.paper_id for p in papers]}
+    the_job.submit()
+
+
+@job_handler(db.JobType.fix_submits)
+def handle_fix_submits(the_job: TheJob):
+    job = the_job.job
+    assert job.in_json is not None
+    paper_ids: List[int] = job.in_json['papers']  # type: ignore
+    sess = db.get_session()
+
+    cnt_fixed = 0
+    cnt_failed = 0
+    cnt_skipped = 0
+
+    for paper_id in paper_ids:
+        log_prefix = f'Protokol #{paper_id}'
+        paper = (sess.query(db.Paper)
+                 .options(joinedload(db.Paper.for_user_obj))
+                 .get(paper_id))
+        if paper is None or paper.fixed_at is not None:
+            cnt_skipped += 1
+            logger.debug(f'Job: {log_prefix}: Již byl vyřízen')
+            continue
+        logger.debug(f'Job: {log_prefix}: Opravuji')
+
+        tmp_file = NamedTemporaryFile(dir=data_dir('tmp'), prefix='fix-')
+        res = subprocess.run(['qpdf', os.path.join(data_dir('submits'), paper.orig_file_name), tmp_file.name])
+        if res.returncode in [0, 3]:
+            sub = Submitter()
+            try:
+                sub.submit_fix(paper, tmp_file.name)
+                logger.info(f'Job: {log_prefix}: Opraven')
+                cnt_fixed += 1
+            except SubmitException:
+                the_job.error(f'{log_prefix}: Oprava selhala')
+                cnt_failed += 1
+        else:
+            the_job.error(f'{log_prefix}: qpdf selhalo s exit code {res.returncode}')
+            cnt_failed += 1
+
+        paper.fixed_at = datetime.datetime.now()
+        sess.commit()
+
+    report = [inflect_number(cnt_fixed, 'opraven', 'opraveny', 'opraveno')]
+    if cnt_failed > 0:
+        report.append(inflect_number(cnt_failed, 'selhal', 'selhaly', 'selhalo'))
+    if cnt_skipped > 0:
+        report.append(inflect_number(cnt_skipped, 'přeskočen', 'přeskočeny', 'přeskočeno'))
+    job.result = 'Oprava protokolů: ' + ', '.join(report)
diff --git a/mo/users.py b/mo/users.py
index 32c858ce08ac61166863fe801d3e3aacbb0ef0d5..f5fe5642bd3328ed36d899e84a7c8d10a1d71fcc 100644
--- a/mo/users.py
+++ b/mo/users.py
@@ -286,6 +286,7 @@ def expire_reg_requests():
 
 def request_reset_password(user: db.User, client: str) -> Optional[db.RegRequest]:
     logger.info('Login: Požadavek na reset hesla pro <%s>', user.email)
+    assert not user.is_admin
     rr = new_reg_request(db.RegReqType.reset_passwd, client)
     if rr:
         db.get_session().add(rr)
diff --git a/mo/web/__init__.py b/mo/web/__init__.py
index 2333eeca1ac63ca6d82a066a99cd4bc7aa8d9a8d..c15c9abb0777718a37e93c534c57efc11f8a125d 100644
--- a/mo/web/__init__.py
+++ b/mo/web/__init__.py
@@ -16,6 +16,7 @@ import mo.config as config
 import mo.db as db
 import mo.ext.assets
 import mo.jobs
+import mo.jobs.submit
 import mo.rights
 import mo.users
 import mo.util
@@ -177,6 +178,7 @@ app.before_request(init_request)
 
 def collect_garbage() -> None:
     mo.now = mo.util.get_now()
+    mo.jobs.submit.check_broken_submits()
     mo.jobs.process_jobs()
     mo.users.expire_reg_requests()
 
@@ -189,6 +191,7 @@ def collect_garbage() -> None:
 def gc():
     """Run garbage collector."""
     collect_garbage()
+    db.get_session().rollback()     # Pokud zůstala otevřená DB session
 
 
 try:
diff --git a/mo/web/acct.py b/mo/web/acct.py
index 4cb0451f63ea30d3c32717519b10f3f343ce3f0f..214da32dc193bc510c6d837d8b60ca42dffcc1c9 100644
--- a/mo/web/acct.py
+++ b/mo/web/acct.py
@@ -61,13 +61,16 @@ def login():
         app.logger.error('Login: Neznámý uživatel <%s>', email)
         flash('Neznámý uživatel', 'danger')
     elif form.reset.data:
-        rr = mo.users.request_reset_password(user, request.remote_addr)
-        if rr:
-            db.get_session().commit()
-            mo.email.send_password_reset_email(user, rr.email_token)
-            flash('Na uvedenou adresu byl odeslán e-mail s odkazem na obnovu hesla.', 'success')
+        if user.is_admin:
+            flash('Obnova hesla účtu správce není možná.', 'danger')
         else:
-            flash('Příliš časté požadavky na obnovu hesla.', 'danger')
+            rr = mo.users.request_reset_password(user, request.remote_addr)
+            if rr:
+                db.get_session().commit()
+                mo.email.send_password_reset_email(user, rr.email_token)
+                flash('Na uvedenou adresu byl odeslán e-mail s odkazem na obnovu hesla.', 'success')
+            else:
+                flash('Příliš časté požadavky na obnovu hesla.', 'danger')
     elif not form.passwd.data or not mo.users.check_password(user, form.passwd.data):
         app.logger.error('Login: Špatné heslo pro uživatele <%s>', email)
         flash('Chybné heslo', 'danger')
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index a89b3330780e5ebe0ceef452852354f77251e24f..10cd5c65d228a9a6b2cdd555e24f5782d57d4951 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -6,6 +6,7 @@ from flask_wtf import FlaskForm
 import flask_wtf.file
 import json
 import locale
+import magic
 from markupsafe import Markup
 from sqlalchemy import func, and_, select
 from sqlalchemy.orm import joinedload, aliased
@@ -13,6 +14,7 @@ from sqlalchemy.orm.query import Query
 from sqlalchemy.dialects.postgresql import insert as pgsql_insert
 from typing import Any, List, Tuple, Optional, Dict
 import urllib.parse
+from werkzeug.datastructures import FileStorage
 import werkzeug.exceptions
 import wtforms
 import wtforms.validators as validators
@@ -1745,13 +1747,14 @@ def org_contest_scans(ct_id: int, site_id: Optional[int] = None):
 
     if proc_form.validate_on_submit() and proc_form.process_scans.data:
         files = request.files.getlist(proc_form.files.name)
-        job_id = mo.jobs.protocols.schedule_process_scans(
-            contest, site, proc_form.scans_type.data, g.user,
-            tasks=[t for t in tasks if getattr(proc_form, f'task_{t.task_id}').data],
-            in_file_names=[f.stream.name for f in files],
-        )
-        flash('Zpracování skenů zahájeno. Vyčkejte chvíli, než budou připraveny, a poté je roztřiďte.', 'success')
-        return redirect(ctx.url_for('org_contest_scans_process', job_id=job_id))
+        if check_scan_files(files):
+            job_id = mo.jobs.protocols.schedule_process_scans(
+                contest, site, proc_form.scans_type.data, g.user,
+                tasks=[t for t in tasks if getattr(proc_form, f'task_{t.task_id}').data],
+                in_file_names=[f.stream.name for f in files],
+            )
+            flash('Zpracování skenů zahájeno. Vyčkejte chvíli, než budou připraveny, a poté je roztřiďte.', 'success')
+            return redirect(ctx.url_for('org_contest_scans'))
 
     jobs_query = sess.query(db.Job).filter_by(type=db.JobType.process_scans)
     if not g.user.is_admin:
@@ -1781,6 +1784,17 @@ def org_contest_scans(ct_id: int, site_id: Optional[int] = None):
     )
 
 
+def check_scan_files(files: List[FileStorage]) -> bool:
+    ok = True
+    mag = magic.Magic(mime=True)
+    for i, f in enumerate(files):
+        mime_type = mag.from_file(f.stream.name)
+        if mime_type != 'application/pdf':
+            flash(f'Soubor {f.filename} není ve formátu PDF, nýbrž {mime_type}.', 'danger')
+            ok = False
+    return ok
+
+
 class ScanProcessForm(FlaskForm):
     data = wtforms.HiddenField()
     save = wtforms.SubmitField()
diff --git a/mo/web/org_jobs.py b/mo/web/org_jobs.py
index 8cf9786e86bdc9f7d335dd69da3cec78ddd83808..7851eab283ec46b899d7005f9f504c8dc2e4dd9d 100644
--- a/mo/web/org_jobs.py
+++ b/mo/web/org_jobs.py
@@ -76,7 +76,7 @@ def job_file_size(job: db.Job, name: Optional[str]) -> Optional[int]:
 def org_job(id: int):
     job = get_job(id)
 
-    has_errors = (job.state == db.JobState.failed
+    has_errors = (job.state in (db.JobState.failed, db.JobState.internal_error)
                   and isinstance(job.out_json, dict)
                   and 'errors' in job.out_json)
 
diff --git a/mo/web/org_round.py b/mo/web/org_round.py
index df0623d40a699ea96122185bfec98d9bdf814541..5de37170833f01334ee05a88014203117fa7282d 100644
--- a/mo/web/org_round.py
+++ b/mo/web/org_round.py
@@ -655,8 +655,10 @@ def org_round_create_contests(round_id: int):
     new_places = (sess.query(db.Place)
                       .select_from(db.Contest)
                       .filter(db.Contest.round == prev_round)
-                      .filter(db.Contest.place_id.notin_(have_places_subq))
-                      .join(db.Place)
+                      .join(db.RegionDescendant, db.RegionDescendant.descendant == db.Contest.place_id)
+                      .join(db.Place, db.Place.place_id == db.RegionDescendant.region)
+                      .filter(db.Place.level == round.level)
+                      .filter(db.Place.place_id.notin_(have_places_subq))
                       .all())
 
     form = CreateContestsForm()
diff --git a/mo/web/templates/org_contest_scans_process.html b/mo/web/templates/org_contest_scans_process.html
index 03af33e3767c2553a215b3c54944205bad61850e..c6f3842599f1e44cd9634c5b62f5954685dd97e9 100644
--- a/mo/web/templates/org_contest_scans_process.html
+++ b/mo/web/templates/org_contest_scans_process.html
@@ -22,7 +22,7 @@ Třídění skenů {{ scans_title }} pro {{ ctx.round.name|lower }} kategorie {{
 <script>
 setTimeout(function () { location.reload(1); }, 10_000);
 </script>
-{% elif job.state == JobState.failed %}
+{% elif job.state != JobState.done %}
 <p>Zpracování selhalo, více detailů naleznete v <a href="{{ url_for('org_job', id=job.job_id) }}">detailu dávky</a>.</p>
 {% else %}
 
diff --git a/mo/web/templates/org_submit_list.html b/mo/web/templates/org_submit_list.html
index 09a62ca21fea47eea2cc23b480251453d4c50195..1cbedf61cd8f07d870f64423f34a339555dd0f30 100644
--- a/mo/web/templates/org_submit_list.html
+++ b/mo/web/templates/org_submit_list.html
@@ -69,7 +69,7 @@ Existuje více než jedna verze řešení, finální je podbarvená.
 		{% set late = p.check_deadline(ctx.round) %}
 		<tr{% if p.paper_id == active_sol_id %} class='sol-active'{% endif %}>
 			<td{% if late %} class='sol-warn'{% endif %}>{{ p.uploaded_at|timeformat }}
-			<td>{% if p.is_broken() %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %}
+			<td>{% if p.is_broken() %}nekorektní PDF{% if p.is_nonfixable() %} (oprava selhala){% endif %}{% else %}{{ p.pages|or_dash }}{% endif %}
 			<td>{{ p.bytes|or_dash }}
 			<td>{% if p.uploaded_by_obj == ctx.user %}<i>účastník</i>{% else %}{{ p.uploaded_by_obj|user_link }}{% endif %}
 			<td>{% if late %}<span class='sol-warn'>({{ late }})</span> {% endif %}{{ p.note }}
@@ -122,7 +122,7 @@ Existuje více než jedna verze oprav, finální je podbarvená.
 	{% for p in fb_papers %}
 		<tr{% if p.paper_id == active_fb_id %} class='sol-active'{% endif %}>
 			<td>{{ p.uploaded_at|timeformat }}
-			<td>{% if p.is_broken() %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %}
+			<td>{% if p.is_broken() %}nekorektní PDF{% if p.is_nonfixable() %} (oprava selhala){% endif %}{% else %}{{ p.pages|or_dash }}{% endif %}
 			<td>{{ p.bytes|or_dash }}
 			<td>{{ p.uploaded_by_obj|user_link }}
 			<td>{{ p.note }}
diff --git a/setup.py b/setup.py
index 3f37cbbff8fde7138cf213699d53fd0feb509b97..6a90c1850f9285dcf6bdb90daa8d3918b8a15245 100644
--- a/setup.py
+++ b/setup.py
@@ -41,6 +41,7 @@ setuptools.setup(
         'pikepdf',
         'pillow',
         'psycopg2',
+        'python-magic',
         'python-poppler',
         'pyzbar',
         'sqlalchemy',
diff --git a/static/mo.css b/static/mo.css
index 91cfe734ef48aac2f952406663a6000bf80aed4d..13b0d8d3f5766592c10b17b9c0ca018e209a94e0 100644
--- a/static/mo.css
+++ b/static/mo.css
@@ -259,6 +259,14 @@ table.data tbody tr.job-failed:hover {
 	background-color: #a66;
 }
 
+table.data tbody tr.job-internal_error {
+	background-color: #c22;
+}
+
+table.data tbody tr.job-internal_error:hover {
+	background-color: #a44;
+}
+
 /* Users */
 
 .user-test {