Index: app.py
===================================================================
--- app.py	(revision d9deb6686c5d9c4a8445309fa4e45bf74b861d47)
+++ app.py	(revision 64e6f27b1d07573faa62c456741356533cddbdd7)
@@ -56,6 +56,4 @@
     )
 
-
-
 @app.route('/test-db')
 def test_db():
@@ -81,5 +79,5 @@
 def login():
     if request.method == 'POST':
-        email = request.form['email'].strip()
+        email = (request.form['email'] or '').strip().lower()  # normalize
         password = request.form['password']
         user = DatabaseManager.authenticate_user(email, password)
@@ -92,22 +90,32 @@
     return render_template('login.html')
 
-
 @app.route('/register', methods=['GET', 'POST'])
 def register():
     teachers = DatabaseManager.get_all_teachers()
     if request.method == 'POST':
-        name = request.form['name']
-        surname = request.form['surname']
-        email = request.form['email']
+        name     = (request.form['name'] or '').strip()
+        surname  = (request.form['surname'] or '').strip()
+        email    = (request.form['email'] or '').strip().lower()  # normalize
         password = request.form['password']
-        role = request.form['role']
-        teacher_id = request.form.get('teacher_id') if role == 'student' else None
+        role     = request.form['role']
+        # студент мора да избере наставник (server-side валидација)
+        teacher_id = None
+        if role == 'student':
+            raw_tid = (request.form.get('teacher_id') or '').strip()
+            if not raw_tid:
+                return render_template('register.html', error='Избери наставник.', teachers=teachers)
+            try:
+                teacher_id = int(raw_tid)
+            except ValueError:
+                return render_template('register.html', error='Невалиден наставник.', teachers=teachers)
+
         password_hash = AuthManager.hash_password(password)
         user_id = DatabaseManager.register_user(name, surname, email, password_hash, role, teacher_id)
         if user_id:
             return redirect(url_for('login'))
-        return render_template('register.html', error='Грешка при регистрација', teachers=teachers)
+        # ако паднало на UNIQUE(email) ќе врати None → прикажи френдли порака
+        return render_template('register.html', error='Овој email веќе постои.', teachers=teachers)
+
     return render_template('register.html', teachers=teachers)
-
 
 @app.route('/logout')
@@ -116,8 +124,4 @@
     return redirect(url_for('index'))
 
-
-# ------------------------------
-# Dashboard
-# ------------------------------
 @app.route('/dashboard')
 @require_login()
@@ -409,4 +413,19 @@
 
 
+def _to_element_id(v):
+    # пробај како int
+    try:
+        return int(v)
+    except Exception:
+        pass
+    # fallback: симбол/име -> id
+    row = DatabaseManager.execute_query("""
+        SELECT element_id
+        FROM elements
+        WHERE UPPER(symbol)=UPPER(%s) OR UPPER(element_name)=UPPER(%s)
+        LIMIT 1
+    """, (v, v)) or []
+    return row[0]['element_id'] if row else None
+
 @app.route('/api/simulate-reaction', methods=['POST'])
 @require_login()
@@ -414,26 +433,23 @@
     try:
         data = request.get_json(silent=True) or {}
-        element1_symbol = (data.get('element1') or '').strip()
-        element2_symbol = (data.get('element2') or '').strip()
-        if not element1_symbol or not element2_symbol:
-            return jsonify({'success': False, 'message': 'Недостигаат параметри (element1/element2)'}), 400
-
-        reactions = DatabaseManager.get_all_reactions() or []
-        for reaction in reactions:
-            if ((reaction['element1_symbol'] == element1_symbol and reaction['element2_symbol'] == element2_symbol) or
-                (reaction['element1_symbol'] == element2_symbol and reaction['element2_symbol'] == element1_symbol)):
-                experiment = DatabaseManager.get_experiment_by_reaction(reaction['reaction_id'])
-                return jsonify({
-                    'success': True,
-                    'product': reaction['product'],
-                    'conditions': reaction.get('conditions'),
-                    'reaction_id': reaction['reaction_id'],
-                    'experiment_id': experiment['experiment_id'] if experiment else None,
-                    'elements': f"{reaction['element1_name']} + {reaction['element2_name']}"
-                }), 200
+        e1 = _to_element_id(data.get('element1_id') or data.get('element1'))
+        e2 = _to_element_id(data.get('element2_id') or data.get('element2'))
+        if not e1 or not e2:
+            return jsonify({'success': False, 'message': 'Недостигаат валидни element_id вредности.'}), 400
+
+        rx = DatabaseManager.get_reaction_by_element_ids(e1, e2)
+        if not rx:
+            return jsonify({'success': False, 'message': 'Реакцијата не е дефинирана во системот.'}), 200
+
+        rx_full = DatabaseManager.get_reaction_by_id(rx['reaction_id']) or {}
+        exp     = DatabaseManager.get_experiment_by_reaction(rx['reaction_id'])
 
         return jsonify({
-            'success': False,
-            'message': f'Реакцијата меѓу {element1_symbol} и {element2_symbol} не е дефинирана во системот.'
+            'success': True,
+            'product': rx.get('product'),
+            'conditions': rx.get('conditions'),
+            'reaction_id': rx['reaction_id'],
+            'experiment_id': exp['experiment_id'] if exp else None,
+            'elements': f"{rx_full.get('element1_name','')} + {rx_full.get('element2_name','')}"
         }), 200
     except Exception as e:
@@ -441,23 +457,27 @@
         return jsonify({'success': False, 'message': f'Серверска грешка: {str(e)}'}), 500
 
-
 @app.route('/api/check-reaction', methods=['POST'])
 @require_login()
 def check_reaction():
-    data = request.get_json() or {}
-    element1_symbol = data.get('element1')
-    element2_symbol = data.get('element2')
-    reactions = DatabaseManager.get_all_reactions() or []
-    for reaction in reactions:
-        if ((reaction['element1_symbol'] == element1_symbol and reaction['element2_symbol'] == element2_symbol) or
-            (reaction['element1_symbol'] == element2_symbol and reaction['element2_symbol'] == element1_symbol)):
-            return jsonify({
-                'success': True,
-                'product': reaction['product'],
-                'conditions': reaction['conditions'],
-                'reaction_id': reaction['reaction_id']
-            })
-    return jsonify({'success': False, 'message': 'Реакцијата не е дефинирана во системот'})
-
+    try:
+        data = request.get_json(silent=True) or {}
+        e1 = _to_element_id(data.get('element1_id') or data.get('element1'))
+        e2 = _to_element_id(data.get('element2_id') or data.get('element2'))
+        if not e1 or not e2:
+            return jsonify({'success': False, 'message': 'Недостигаат валидни element_id вредности.'}), 400
+
+        rx = DatabaseManager.get_reaction_by_element_ids(e1, e2)
+        if not rx:
+            return jsonify({'success': False, 'message': 'Реакцијата не е дефинирана во системот.'}), 200
+
+        return jsonify({
+            'success': True,
+            'product': rx['product'],
+            'conditions': rx['conditions'],
+            'reaction_id': rx['reaction_id']
+        }), 200
+    except Exception as e:
+        app.logger.exception("check_reaction failed")
+        return jsonify({'success': False, 'message': f'Серверска грешка: {str(e)}'}), 500
 
 @app.route('/save-experiment', methods=['POST'])
@@ -467,12 +487,19 @@
     reaction_id = data.get('reaction_id')
     experiment = DatabaseManager.get_experiment_by_reaction(reaction_id)
+
     if not experiment:
         if session['role'] != 'teacher':
             return jsonify({'success': False, 'message': 'Не постои експеримент за оваа реакција. Контактирајте го вашиот професор.'})
+
         result_description = data.get('result', 'Експериментална симулација')
-        safety_warning = data.get('safety_warning', 'Стандардни безбедносни мерки')
-        experiment_id = DatabaseManager.insert_experiment(session['user_id'], reaction_id, result_description, safety_warning)
+        # важно: ако нема вредност, испрати None → тригерот ќе пополни
+        safety_warning = (data.get('safety_warning') or '').strip() or None
+
+        experiment_id = DatabaseManager.insert_experiment(
+            session['user_id'], reaction_id, result_description, safety_warning
+        )
     else:
         experiment_id = experiment['experiment_id']
+
     if experiment_id:
         DatabaseManager.track_experiment_participation(session['user_id'], experiment_id)
Index: templates/base.html
===================================================================
--- templates/base.html	(revision d9deb6686c5d9c4a8445309fa4e45bf74b861d47)
+++ templates/base.html	(revision 64e6f27b1d07573faa62c456741356533cddbdd7)
@@ -21,4 +21,18 @@
         <li class="nav-item"><a class="nav-link" href="/laboratory">Лабораторија</a></li>
       </ul>
+      <!-- base.html (во body, веднаш под navbar) -->
+{% with messages = get_flashed_messages(with_categories=true) %}
+  {% if messages %}
+    <div class="container mt-3">
+      {% for category, msg in messages %}
+        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+          {{ msg }}
+          <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+        </div>
+      {% endfor %}
+    </div>
+  {% endif %}
+{% endwith %}
+
       <div class="d-flex align-items-center gap-2">
         <span class="text-white-50 small d-none d-md-inline">{{ session.user_name }}</span>
Index: utils/database_manager.py
===================================================================
--- utils/database_manager.py	(revision d9deb6686c5d9c4a8445309fa4e45bf74b861d47)
+++ utils/database_manager.py	(revision 64e6f27b1d07573faa62c456741356533cddbdd7)
@@ -4,4 +4,12 @@
 import psycopg2
 from psycopg2.extras import RealDictCursor, execute_values
+from psycopg2 import errors as pg_errors
+
+
+def _norm_symbol(s: str) -> str:
+    return (s or "").strip().upper()
+
+def _null_if_blank(s: str | None):
+    return None if s is None or (isinstance(s, str) and s.strip() == "") else s
 
 log = logging.getLogger("simlab.db")
@@ -74,5 +82,5 @@
         try:
             with _conn_cur() as cur:
-                # User
+                # норма: сними email каков што е внесен (ако сакаш – .lower())
                 cur.execute(
                     'INSERT INTO "User" (user_name, user_surname, email, password, role) '
@@ -82,5 +90,4 @@
                 user_id = cur.fetchone()['user_id']
 
-                # Subtype
                 if role == 'student' and teacher_id:
                     cur.execute(
@@ -92,7 +99,11 @@
 
                 return user_id
+        except pg_errors.UniqueViolation:
+            log.warning("register_user: email already exists (%s)", email)
+            return None
         except Exception:
             log.exception("register_user failed (email=%s, role=%s)", email, role)
             return None
+
 
     @staticmethod
@@ -158,11 +169,18 @@
         try:
             with _conn_cur() as cur:
+                symbol = _norm_symbol(symbol)
                 cur.execute('''
                     INSERT INTO elements (symbol, element_name, atomic_number, atomic_weight, 
-                                          melting_point, boiling_point, hazard_type, description_element, teacher_id)
+                                        melting_point, boiling_point, hazard_type, description_element, teacher_id)
                     VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
                     RETURNING element_id
                 ''', (symbol, name, atomic_number, atomic_weight, melting_point, boiling_point, hazard_type, description, teacher_id))
                 return cur.fetchone()['element_id']
+        except pg_errors.UniqueViolation:
+            log.warning("add_element: symbol already exists (%s)", symbol)
+            return None
+        except pg_errors.CheckViolation:
+            log.warning("add_element: physical constraint violation (Z>0, mass>0, melting<boiling)")
+            return None
         except Exception:
             log.exception("add_element failed (symbol=%s, name=%s)", symbol, name)
@@ -173,4 +191,5 @@
         try:
             with _conn_cur() as cur:
+                symbol = _norm_symbol(symbol)
                 cur.execute('''
                     UPDATE elements 
@@ -180,4 +199,10 @@
                 ''', (symbol, name, atomic_number, atomic_weight, melting_point, boiling_point, hazard_type, description, element_id))
                 return True
+        except pg_errors.UniqueViolation:
+            log.warning("update_element: symbol already exists (%s)", symbol)
+            return False
+        except pg_errors.CheckViolation:
+            log.warning("update_element: physical constraint violation")
+            return False
         except Exception:
             log.exception("update_element failed (element_id=%s)", element_id)
@@ -225,4 +250,7 @@
                 ''', (name, equipment_type, description, safety_info, teacher_id))
                 return cur.fetchone()['equipment_id']
+        except pg_errors.UniqueViolation:
+            log.warning("add_lab_equipment: equipment_name already exists (%s)", name)
+            return None
         except Exception:
             log.exception("add_lab_equipment failed (name=%s, type=%s)", name, equipment_type)
@@ -261,4 +289,7 @@
     @staticmethod
     def add_reaction(teacher_id, element1_id, element2_id, product, conditions):
+        if element1_id == element2_id:
+            log.warning("add_reaction blocked: element1_id == element2_id")
+            return None
         try:
             with _conn_cur() as cur:
@@ -269,4 +300,10 @@
                 ''', (teacher_id, element1_id, element2_id, product, conditions))
                 return cur.fetchone()['reaction_id']
+        except pg_errors.UniqueViolation:
+            log.warning("add_reaction: duplicate (element1, element2, conditions)")
+            return None
+        except pg_errors.CheckViolation:
+            log.warning("add_reaction: check violation (element1_id <> element2_id)")
+            return None
         except Exception:
             log.exception("add_reaction failed")
@@ -275,4 +312,7 @@
     @staticmethod
     def update_reaction(reaction_id, element1_id, element2_id, product, conditions):
+        if element1_id == element2_id:
+            log.warning("update_reaction blocked: element1_id == element2_id")
+            return False
         try:
             with _conn_cur() as cur:
@@ -283,4 +323,10 @@
                 ''', (element1_id, element2_id, product, conditions, reaction_id))
                 return True
+        except pg_errors.UniqueViolation:
+            log.warning("update_reaction: duplicate (element1, element2, conditions)")
+            return False
+        except pg_errors.CheckViolation:
+            log.warning("update_reaction: check violation (element1_id <> element2_id)")
+            return False
         except Exception:
             log.exception("update_reaction failed (reaction_id=%s)", reaction_id)
@@ -293,7 +339,11 @@
                 cur.execute('DELETE FROM reaction WHERE reaction_id = %s', (reaction_id,))
                 return True
+        except pg_errors.ForeignKeyViolation:
+            log.warning("delete_reaction blocked: Reaction %s has Experiments", reaction_id)
+            return False
         except Exception:
             log.exception("delete_reaction failed (reaction_id=%s)", reaction_id)
             return False
+
 
     @staticmethod
@@ -348,9 +398,10 @@
                     VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP)
                     RETURNING experiment_id
-                ''', (teacher_id, reaction_id, result, safety_warning))
+                ''', (teacher_id, reaction_id, result, _null_if_blank(safety_warning)))
                 return cur.fetchone()['experiment_id']
         except Exception:
             log.exception("insert_experiment failed")
             return None
+
 
     @staticmethod
@@ -435,7 +486,11 @@
                     INSERT INTO userparticipatesinexperiment (user_id, experiment_id)
                     VALUES (%s, %s)
+                    ON CONFLICT (user_id, experiment_id) DO NOTHING
                 ''', (user_id, experiment_id))
+                return True
         except Exception:
             log.exception("track_experiment_participation failed")
+            return False
+
 
     @staticmethod
@@ -635,5 +690,5 @@
                     JOIN elements el2 ON r.element2_id = el2.element_id
                     WHERE up.user_id = %s
-                    ORDER BY e.time_stamp DESC
+                    ORDER BY up.participation_timestamp DESC
                 """, (student_id,))
                 return cur.fetchall()
@@ -995,5 +1050,5 @@
 
     @staticmethod
-    def get_reaction_by_symbols(sym1: str, sym2: str):
+    def get_reaction_by_element_ids(e1: int, e2: int):
         try:
             with _conn_cur() as cur:
@@ -1001,13 +1056,27 @@
                     SELECT r.reaction_id, r.product, r.conditions
                     FROM reaction r
-                    JOIN elements e1 ON r.element1_id = e1.element_id
-                    JOIN elements e2 ON r.element2_id = e2.element_id
-                    WHERE (UPPER(e1.symbol) = UPPER(%s) AND UPPER(e2.symbol) = UPPER(%s))
-                       OR (UPPER(e1.symbol) = UPPER(%s) AND UPPER(e2.symbol) = UPPER(%s))
+                    WHERE (r.element1_id = %s AND r.element2_id = %s)
+                    OR (r.element1_id = %s AND r.element2_id = %s)
                     LIMIT 1
-                """, (sym1, sym2, sym2, sym1))
+                """, (e1, e2, e2, e1))
                 row = cur.fetchone()
                 return dict(row) if row else None
         except Exception:
-            log.exception("get_reaction_by_symbols failed (%s, %s)", sym1, sym2)
-            return None
+            log.exception("get_reaction_by_element_ids failed (%s, %s)", e1, e2)
+            return None
+
+    @staticmethod
+    def get_experiments_by_reaction(reaction_id: int, limit: int = 50):
+        try:
+            with _conn_cur() as cur:
+                cur.execute("""
+                    SELECT e.experiment_id, e.result, e.time_stamp
+                    FROM experiment e
+                    WHERE e.reaction_id = %s
+                    ORDER BY e.time_stamp DESC
+                    LIMIT %s
+                """, (reaction_id, limit))
+                return cur.fetchall()
+        except Exception:
+            log.exception("get_experiments_by_reaction failed (%s)", reaction_id)
+            return []
