diff --git a/app/auth/views.py b/app/auth/views.py
index 9470480f6ca27367331f20879a550bf26734be45..6b87f7440fb4c41360654e99b2d0aeb433e18003 100644
--- a/app/auth/views.py
+++ b/app/auth/views.py
@@ -17,10 +17,13 @@ def before_request():
     Checks if a user is unconfirmed when visiting specific sites. Redirects to
     unconfirmed view if user is unconfirmed.
     """
-    if (current_user.is_authenticated and not current_user.confirmed
-            and request.blueprint != 'auth'
-            and request.endpoint != 'static'):
-        return redirect(url_for('auth.unconfirmed'))
+    if current_user.is_authenticated:
+        current_user.ping()
+        if not current_user.confirmed \
+                and request.endpoint \
+                and request.blueprint != 'auth' \
+                and request.endpoint != 'static':
+            return redirect(url_for('auth.unconfirmed'))
 
 
 @auth.route('/login', methods=['GET', 'POST'])
diff --git a/app/models.py b/app/models.py
index e5c29953232c22e8335ee0ebc5b4d4660b361b74..7866ef3c07280988b633363f13b163c857376a11 100644
--- a/app/models.py
+++ b/app/models.py
@@ -106,13 +106,18 @@ class User(UserMixin, db.Model):
     id = db.Column(db.Integer, primary_key=True)
     # Fields
     confirmed = db.Column(db.Boolean, default=False)
+    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
     email = db.Column(db.String(254), unique=True, index=True)
     password_hash = db.Column(db.String(128))
-    registration_date = db.Column(db.DateTime(), default=datetime.utcnow)
+    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
     role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
     username = db.Column(db.String(64), unique=True, index=True)
     # Setting Fields
     setting_dark_mode = db.Column(db.Boolean, default=False)
+    setting_job_status_mail_notifications = db.Column(db.String(16),
+                                                      default='end')
+    setting_job_status_site_notifications = db.Column(db.String(16),
+                                                      default='all')
     # Relationships
     corpora = db.relationship('Corpus', backref='creator', lazy='dynamic',
                               cascade='save-update, merge, delete')
@@ -205,6 +210,10 @@ class User(UserMixin, db.Model):
         """
         return self.can(Permission.ADMIN)
 
+    def ping(self):
+        self.last_seen = datetime.utcnow()
+        db.session.add(self)
+
     def delete(self):
         """
         Delete the user and its corpora and jobs from database and filesystem.
diff --git a/app/profile/forms.py b/app/profile/forms.py
index 60257805ed908eccb813ed9328f3153a32f65dce..14517e293ed0d48ba89e1d75fe43eb1b9ac4d6de 100644
--- a/app/profile/forms.py
+++ b/app/profile/forms.py
@@ -1,6 +1,6 @@
 from flask_wtf import FlaskForm
-from wtforms import (BooleanField, PasswordField, StringField, SubmitField,
-                     ValidationError)
+from wtforms import (BooleanField, PasswordField, SelectField, StringField,
+                     SubmitField, ValidationError)
 from wtforms.validators import DataRequired, Email, EqualTo
 
 
@@ -11,6 +11,20 @@ class EditEmailForm(FlaskForm):
 
 class EditGeneralSettingsForm(FlaskForm):
     dark_mode = BooleanField('Dark mode')
+    job_status_mail_notifications = SelectField(
+        'Job status mail notifications',
+        choices=[('', 'Choose your option'),
+                 ('all', 'Notify on all status changes'),
+                 ('end', 'Notify only when a job ended'),
+                 ('none', 'No status update notifications')],
+        validators=[DataRequired()])
+    job_status_site_notifications = SelectField(
+        'Job status site notifications',
+        choices=[('', 'Choose your option'),
+                 ('all', 'Notify on all status changes'),
+                 ('end', 'Notify only when a job ended'),
+                 ('none', 'No status update notifications')],
+        validators=[DataRequired()])
     save_settings = SubmitField('Save settings')
 
 
diff --git a/app/profile/views.py b/app/profile/views.py
index 89d127dfed9fed9cfc0d790ee0aaa5122f55d59d..f156715fbf824382c2d36a52a78482d12444f96c 100644
--- a/app/profile/views.py
+++ b/app/profile/views.py
@@ -11,8 +11,7 @@ from .. import db
 def settings():
     edit_email_form = EditEmailForm(prefix='edit-email-form')
     edit_general_settings_form = EditGeneralSettingsForm(
-        prefix='edit-general-settings-form'
-    )
+        prefix='edit-general-settings-form')
     edit_password_form = EditPasswordForm(prefix='edit-password-form',
                                           user=current_user)
     # Check if edit_email_form is submitted and valid
@@ -25,7 +24,12 @@ def settings():
     # Check if edit_settings_form is submitted and valid
     if (edit_general_settings_form.save_settings.data
             and edit_general_settings_form.validate_on_submit()):
-        current_user.setting_dark_mode = edit_general_settings_form.dark_mode.data
+        current_user.setting_dark_mode = \
+            edit_general_settings_form.dark_mode.data
+        current_user.setting_job_status_mail_notifications = \
+            edit_general_settings_form.job_status_mail_notifications.data
+        current_user.setting_job_status_site_notifications = \
+            edit_general_settings_form.job_status_site_notifications.data
         db.session.add(current_user)
         db.session.commit()
         flash('Your settings have been updated.')
@@ -41,6 +45,10 @@ def settings():
     # If no form is submitted or valid, fill out fields with current values
     edit_email_form.email.data = current_user.email
     edit_general_settings_form.dark_mode.data = current_user.setting_dark_mode
+    edit_general_settings_form.job_status_site_notifications.data = \
+        current_user.setting_job_status_site_notifications
+    edit_general_settings_form.job_status_mail_notifications.data = \
+        current_user.setting_job_status_mail_notifications
     return render_template(
         'profile/settings.html.j2',
         edit_email_form=edit_email_form,
diff --git a/app/static/js/nopaque.js b/app/static/js/nopaque.js
index 2cba2c1b3e3f4a16ca14f4c3da91acaabd4541a6..d2c6ddbfc3d1bc561bb6b24b269ed42eb757d3fc 100644
--- a/app/static/js/nopaque.js
+++ b/app/static/js/nopaque.js
@@ -43,13 +43,6 @@ nopaque.socket.init = function() {
     var patch;
 
     patch = JSON.parse(msg);
-    for (operation of patch) {
-      /* "/corpusId/valueName" -> ["corpusId", "valueName"] */
-      pathArray = operation.path.split("/").slice(1);
-      if (operation.op === "replace" && pathArray[1] === "status") {
-        nopaque.flash(`<i class="left material-icons">book</i>[<a href="/jobs/${pathArray[0]}">${nopaque.corpora[pathArray[0]].title}</a>] New status: ${operation.value}`);
-      }
-    }
     nopaque.corpora = jsonpatch.apply_patch(nopaque.corpora, patch);
     for (let subscriber of nopaque.corporaSubscribers) {subscriber._update(patch);}
   });
@@ -58,14 +51,17 @@ nopaque.socket.init = function() {
     var patch;
 
     patch = JSON.parse(msg);
-    for (operation of patch) {
-      /* "/jobId/valueName" -> ["jobId", "valueName"] */
-      pathArray = operation.path.split("/").slice(1);
-      if (operation.op === "replace" && pathArray[1] === "status") {
-        nopaque.flash(`<i class="left material-icons">work</i>[<a href="/jobs/${pathArray[0]}">${nopaque.jobs[pathArray[0]].title}</a>] New status: ${operation.value}`);
+    nopaque.jobs = jsonpatch.apply_patch(nopaque.jobs, patch);
+    if (["all", "end"].includes(nopaque.user.settings.jobStatusSiteNotifications)) {
+      for (operation of patch) {
+        /* "/jobId/valueName" -> ["jobId", "valueName"] */
+        pathArray = operation.path.split("/").slice(1);
+        if (operation.op === "replace" && pathArray[1] === "status") {
+          if (nopaque.user.settings.jobStatusSiteNotifications === "end" && !["complete", "failed"].includes(operation.value)) {continue;}
+          nopaque.flash(`<i class="left material-icons">work</i>[<a href="/jobs/${pathArray[0]}">${nopaque.jobs[pathArray[0]].title}</a>] New status: ${operation.value}`);
+        }
       }
     }
-    nopaque.jobs = jsonpatch.apply_patch(nopaque.jobs, patch);
     for (let subscriber of nopaque.jobsSubscribers) {subscriber._update(patch);}
   });
 
diff --git a/app/templates/admin/user.html.j2 b/app/templates/admin/user.html.j2
index 15de85da11430ff16121d5d3dd2af57584c981ee..f68259763ad128eed883a413997e1da5936a2d34 100644
--- a/app/templates/admin/user.html.j2
+++ b/app/templates/admin/user.html.j2
@@ -15,8 +15,9 @@
         <li>Username: {{ user.username }}</li>
         <li>Email: {{ user.email }}</li>
         <li>ID: {{ user.id }}</li>
-        <li>Registration date: {{ user.registration_date.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li>
+        <li>Member sinse: {{ user.member_since.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li>
         <li>Confirmed status: {{ user.confirmed }}</li>
+        <li>Last seen: {{ user.last_seen.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li>
         <li>Role ID: {{ user.role_id }}</li>
         <li>Permissions as Int: {{ user.role.permissions }}</li>
         <li>Role name: {{ user.role.name }}</li>
diff --git a/app/templates/jobs/job.html.j2 b/app/templates/jobs/job.html.j2
index 5b45d19a2b4e212f133792fef3c0c2c672fe0a11..5be1ed992e2e9865ba8bfc386d075a9eacd4f7b3 100644
--- a/app/templates/jobs/job.html.j2
+++ b/app/templates/jobs/job.html.j2
@@ -175,7 +175,6 @@
 
       for (let operation of patch) {
         /* "/jobId/valueName" -> ["jobId", "valueName"] */
-        console.log(operation.value);
         pathArray = operation.path.split("/").slice(1);
         if (pathArray[0] != this.jobId) {continue;}
         switch(operation.op) {
diff --git a/app/templates/macros/materialize.html.j2 b/app/templates/macros/materialize.html.j2
index 40785de613658fc2fc7eee2a8fed71dfa05b7614..bf14c6e738c3ca4913a388e034996a5bca713e3c 100644
--- a/app/templates/macros/materialize.html.j2
+++ b/app/templates/macros/materialize.html.j2
@@ -29,12 +29,15 @@
 {% endmacro %}
 
 {% macro render_boolean_field(field) %}
+  {% set label = kwargs.pop('label', True) %}
   <div class="switch">
     {% if 'material_icon' in kwargs %}
       <i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
     {% endif %}
     <label>
+      {% if label %}
       {{ field.label.text }}
+      {% endif %}
       {{ field(*args, **kwargs) }}
       <span class="lever"></span>
     </label>
@@ -44,12 +47,6 @@
   </div>
 {% endmacro %}
 
-{% macro render_decimal_range_field(field) %}
-  <p class="range-field">
-    {{ field(*args, **kwargs) }}
-  </p>
-{% endmacro %}
-
 {% macro render_file_field(field) %}
   {% set placeholder = kwargs.pop('placeholder', '') %}
   <div class="file-field input-field">
@@ -64,12 +61,15 @@
 {% endmacro %}
 
 {% macro render_generic_field(field) %}
+  {% set label = kwargs.pop('label', True) %}
   <div class="input-field">
     {% if 'material_icon' in kwargs %}
       <i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i>
     {% endif %}
     {{ field(*args, **kwargs) }}
+    {% if label %}
     {{ field.label }}
+    {% endif %}
     {% for error in field.errors %}
       <span class="helper-text red-text">{{ error }}</span>
     {% endfor %}
diff --git a/app/templates/nopaque.html.j2 b/app/templates/nopaque.html.j2
index 8c305ad66349ea5d126f4ab75c208e5b830d83f0..f608ac188d672febe83d33f5d737d98ca3a91883 100644
--- a/app/templates/nopaque.html.j2
+++ b/app/templates/nopaque.html.j2
@@ -49,9 +49,15 @@
     <script src="{{ url_for('static', filename='js/nopaque.js') }}"></script>
     <script src="{{ url_for('static', filename='js/nopaque.lists.js') }}"></script>
     <script>
-      nopaque.user.isAuthenticated = {{ current_user.is_authenticated|tojson }};
-      nopaque.user.settings.darkMode = {{ (current_user.is_authenticated and current_user.setting_dark_mode)|tojson }};
-      nopaque.flashedMessages = {{ get_flashed_messages(with_categories=true)|tojson }};
+      {% if current_user.is_authenticated %}
+      nopaque.user.isAuthenticated = true;
+      nopaque.user.settings.darkMode = {{ current_user.setting_dark_mode|tojson }};
+      nopaque.user.settings.jobStatusMailNotifications = {{ current_user.setting_job_status_mail_notifications|tojson }};
+      nopaque.user.settings.jobStatusSiteNotifications = {{ current_user.setting_job_status_site_notifications|tojson }};
+      {% else %}
+      nopaque.user.isAuthenticated = false;
+      {% endif %}
+      nopaque.flashedMessages = {{ get_flashed_messages(with_categories=True)|tojson }};
     </script>
   </head>
   <body>
diff --git a/app/templates/profile/settings.html.j2 b/app/templates/profile/settings.html.j2
index 3ab20a0d51b4214a61f33f9234bfff5bdfda80e1..b5897ebba2082da545141f88a9dff95e0cafb58f 100644
--- a/app/templates/profile/settings.html.j2
+++ b/app/templates/profile/settings.html.j2
@@ -27,16 +27,21 @@
           <div class="col s12 divider"></div>
           <div class="col s12"><p>&nbsp;</p></div>
           <div class="col s9">
-            <p><i class="material-icons left">notifications</i>Email notifications</p>
-            <p class="light">Receive emails when a job completes.</p>
+            <p><i class="material-icons left">notifications</i>Job status site notifications</p>
+            <p class="light">Receive site notifications about job status changes.</p>
           </div>
           <div class="col s3 right-align">
-            <div class="switch">
-              <label>
-                <input disabled type="checkbox">
-                <span class="lever"></span>
-              </label>
-            </div>
+            {{ M.render_field(edit_general_settings_form.job_status_site_notifications, label=False) }}
+          </div>
+          <div class="col s12"><p>&nbsp;</p></div>
+          <div class="col s12 divider"></div>
+          <div class="col s12"><p>&nbsp;</p></div>
+          <div class="col s9">
+            <p><i class="material-icons left">notifications</i>Job status mail notifications</p>
+            <p class="light">Receive mail notifications about job status changes.</p>
+          </div>
+          <div class="col s3 right-align">
+            {{ M.render_field(edit_general_settings_form.job_status_mail_notifications, label=False) }}
           </div>
           <!--
           Seperate each setting with the following two elements
diff --git a/migrations/versions/099037c4aa06_.py b/migrations/versions/099037c4aa06_.py
new file mode 100644
index 0000000000000000000000000000000000000000..d9adf506911c4a5de16821610da734b634621cf1
--- /dev/null
+++ b/migrations/versions/099037c4aa06_.py
@@ -0,0 +1,30 @@
+"""empty message
+
+Revision ID: 099037c4aa06
+Revises: 66253783654f
+Create Date: 2020-04-27 09:17:15.039728
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '099037c4aa06'
+down_revision = '66253783654f'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('last_seen', sa.DateTime(), nullable=True))
+    op.add_column('users', sa.Column('setting_site_job_status_notifications', sa.String(length=16), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('users', 'setting_site_job_status_notifications')
+    op.drop_column('users', 'last_seen')
+    # ### end Alembic commands ###
diff --git a/migrations/versions/49a42c69e523_.py b/migrations/versions/49a42c69e523_.py
new file mode 100644
index 0000000000000000000000000000000000000000..36281de5c9784ec7ce32eafe37c0b1e80d073ec8
--- /dev/null
+++ b/migrations/versions/49a42c69e523_.py
@@ -0,0 +1,36 @@
+"""empty message
+
+Revision ID: 49a42c69e523
+Revises: 099037c4aa06
+Create Date: 2020-04-27 11:18:32.999099
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '49a42c69e523'
+down_revision = '099037c4aa06'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('member_since', sa.DateTime(), nullable=True))
+    op.add_column('users', sa.Column('setting_job_status_mail_notifications', sa.String(length=16), nullable=True))
+    op.add_column('users', sa.Column('setting_job_status_site_notifications', sa.String(length=16), nullable=True))
+    op.drop_column('users', 'setting_site_job_status_notifications')
+    op.drop_column('users', 'registration_date')
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('registration_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
+    op.add_column('users', sa.Column('setting_site_job_status_notifications', sa.VARCHAR(length=16), autoincrement=False, nullable=True))
+    op.drop_column('users', 'setting_job_status_site_notifications')
+    op.drop_column('users', 'setting_job_status_mail_notifications')
+    op.drop_column('users', 'member_since')
+    # ### end Alembic commands ###