diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb
index 508bfaf2ca140751bbfe60897874eddf13d95f08..54405433becf551f87b53318de65d5411168d81e 100644
--- a/app/controllers/assignments_controller.rb
+++ b/app/controllers/assignments_controller.rb
@@ -35,17 +35,31 @@ class AssignmentsController < ApplicationController
     @user = User.find(params[:user_id])
     @assignment = Assignment.new(user: @user, session: @session)
 
-    if @assignment.save
-      Rails.logger.debug("Saved assignment #{@assignment.inspect}")
-      Turbo::StreamsChannel.broadcast_replace_to(
-        @session.conference,
-        target: helpers.dom_id(@session),
-        partial: "sessions/session",
-        locals: { session: @session }
-      )
+    if @assignment.save # Model callbacks will handle candidate removal and broadcast
+      Rails.logger.debug("Saved assignment #{@assignment.inspect}. Model callbacks will handle candidate removal and broadcast.")
+
+      # Ensure @session is fresh for the current user's direct response,
+      # reflecting changes made by model callbacks (candidate removal).
+      # The model's broadcast already uses session.reload.
+      # For the direct response, if @session was already loaded, reloading it here
+      # ensures it's consistent with what the broadcast will show.
+      @session.reload
+
       flash.now[:success] = "User assigned successfully."
+
+      # Calculate new workload for the affected user
+      new_workload_hours = (@user.workload_minutes / 60.0).round(1)
+      dispatch_event_script = "document.dispatchEvent(new CustomEvent('conference-workload:user-updated', { detail: { userId: #{@user.id}, newHours: #{new_workload_hours} } }));"
+
       respond_to do |format|
-        format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) }
+        format.turbo_stream do
+          render turbo_stream: [
+            turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }),
+            turbo_stream.append_all("body", "<script type='text/javascript'>#{dispatch_event_script}</script>".html_safe)
+            # Consider a more robust way if append_all to body with script tag is problematic,
+            # e.g. a dedicated turbo_frame for scripts or a custom turbo_stream action.
+          ]
+        end
         format.html { redirect_to conference_session_path(@session.conference, @session), success: "User assigned successfully." }
       end
     else
@@ -61,16 +75,27 @@ class AssignmentsController < ApplicationController
     @assignment = Assignment.find(params[:id])
     @session = @assignment.session
 
-    if @assignment&.destroy
-      Rails.logger.debug("destroyed assignment")
-      Turbo::StreamsChannel.broadcast_replace_later_to(
-        @session.conference,
-        target: helpers.dom_id(@session),
-        partial: "sessions/session",
-        locals: { session: @session }
-      )
+    # user_for_workload_update must be captured before @assignment is destroyed
+    user_for_workload_update = @assignment.user
+
+    if @assignment.destroy # Model callbacks will handle candidate recreation and broadcast
+      Rails.logger.debug("Destroyed assignment. Model callbacks will handle candidate recreation and broadcast.")
+
+      # Ensure @session is fresh for the current user's direct response,
+      # reflecting changes made by model callbacks (candidate recreation).
+      @session.reload
+
+      # Calculate new workload for the affected user
+      new_workload_hours = (user_for_workload_update.workload_minutes / 60.0).round(1)
+      dispatch_event_script = "document.dispatchEvent(new CustomEvent('conference-workload:user-updated', { detail: { userId: #{user_for_workload_update.id}, newHours: #{new_workload_hours} } }));"
+
       respond_to do |format|
-        format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) }
+        format.turbo_stream do
+          render turbo_stream: [
+            turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }), # Uses reloaded @session
+            turbo_stream.append_all("body", "<script type='text/javascript'>#{dispatch_event_script}</script>".html_safe)
+          ]
+        end
         format.html { redirect_to conference_session_path(@session.conference, @session), notice: "User removed successfully." }
       end
     else
diff --git a/app/controllers/candidates_controller.rb b/app/controllers/candidates_controller.rb
index d59906784beed8224e8639d315969ea0047b8216..a754e3de0f7b64f401de5f0a6f036f0d1ba05a63 100644
--- a/app/controllers/candidates_controller.rb
+++ b/app/controllers/candidates_controller.rb
@@ -20,10 +20,23 @@ class CandidatesController < ApplicationController
         partial: "sessions/session",
         locals: { session: @session }
       )
-      flash.now[:success] = "User assigned successfully."
+      flash.now[:success] = "Successfully registered as candidate." # Message updated for clarity
+
+      # Dispatch event for Stimulus controller to update this user's workload display
+      # Workload itself doesn't change by becoming a candidate, but we need to ensure
+      # the hours are displayed on the newly added/updated avatar.
+      user_for_workload_update = @candidate.user
+      current_workload_hours = (user_for_workload_update.workload_minutes / 60.0).round(1)
+      dispatch_event_script = "document.dispatchEvent(new CustomEvent('conference-workload:user-updated', { detail: { userId: #{user_for_workload_update.id}, newHours: #{current_workload_hours} } }));"
+
       respond_to do |format|
-        format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }) }
-        format.html { redirect_to conference_session_path(@session.conference, @session), success: "User assigned successfully." }
+        format.turbo_stream do
+          render turbo_stream: [
+            turbo_stream.replace(helpers.dom_id(@session), partial: "sessions/session", locals: { session: @session }),
+            turbo_stream.append_all("body", "<script type='text/javascript'>#{dispatch_event_script}</script>".html_safe)
+          ]
+        end
+        format.html { redirect_to conference_session_path(@session.conference, @session), success: "Successfully registered as candidate." }
       end
     else
       flash.now[:alert] = "Failed to record candidate."
diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb
index 704dda1c1195b175e0c281da137e98cd84a4614d..00802ec6933b088a5f973dbe2e3eb966e43f413a 100644
--- a/app/controllers/conferences_controller.rb
+++ b/app/controllers/conferences_controller.rb
@@ -60,8 +60,7 @@ class ConferencesController < ApplicationController
 
   def show
     @conference = Conference.find_by(slug: params[:slug])
-    @sessions = @conference.sessions.scheduled.where.not(starts_at: nil).includes(:stage,
-                                                                        :assignments).where(stage: @conference.relevant_stages).order(:starts_at)
+    @sessions = @conference.sessions.scheduled.where.not(starts_at: nil).includes(:stage, :assignments, :candidates).where(stage: @conference.relevant_stages).order(:starts_at)
     @standby_blocks = @conference.standby_blocks.active.includes(:standby_assignments, :users).order(:starts_at)
 
     if params[:date]
@@ -87,6 +86,20 @@ class ConferencesController < ApplicationController
     # Combine sessions and standby blocks for timeline display
     @all_timeline_items = @sessions.to_a + @standby_blocks.to_a
 
+    # Get all user IDs from assignments and candidates for the current conference's sessions
+    # Note: @sessions is already filtered by relevant stages and potentially date
+    assigned_user_ids = @sessions.flat_map(&:assignments).map(&:user_id)
+    candidate_user_ids = @sessions.flat_map(&:candidates).map(&:user_id)
+    all_relevant_user_ids = (assigned_user_ids + candidate_user_ids).uniq
+
+    # Fetch these users and precompute their workloads
+    # User#workload_minutes already filters by active conferences
+    @user_workloads = User.where(id: all_relevant_user_ids).each_with_object({}) do |user, hash|
+      hash[user.id] = (user.workload_minutes / 60.0).round(1)
+    end
+
+    # @users is used by the filteredlist partial, ensure it contains all users for the dropdown.
+    # If the dropdown should also be limited to relevant users, this can be changed.
     @users = User.all
   end
 
diff --git a/app/javascript/controllers/conference_workload_controller.js b/app/javascript/controllers/conference_workload_controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa364a0ee6215602972f72f919a8bc937fccd046
--- /dev/null
+++ b/app/javascript/controllers/conference_workload_controller.js
@@ -0,0 +1,86 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+  static values = { userWorkloads: Object }
+
+  connect() {
+    this.updateAllAvatarWorkloads()
+    this.boundHandleUserWorkloadUpdate = this.handleUserWorkloadUpdate.bind(this)
+    document.addEventListener("conference-workload:user-updated", this.boundHandleUserWorkloadUpdate)
+  }
+
+  disconnect() {
+    document.removeEventListener("conference-workload:user-updated", this.boundHandleUserWorkloadUpdate)
+  }
+
+  updateAllAvatarWorkloads() {
+    const workloads = this.userWorkloadsValue || {}
+    const avatarContainers = this.element.querySelectorAll(".user-avatar-container[data-user-id]")
+
+    avatarContainers.forEach(container => {
+      const userId = container.dataset.userId
+      const workloadHours = workloads[userId]
+
+      const hoursDisplaySpan = container.querySelector(".user-workload-hours-display")
+      const userName = container.querySelector("span[style^='color:']").textContent.split(" ")[0] // Get current name, assuming it's first part
+
+      if (hoursDisplaySpan) {
+        if (workloadHours !== undefined && workloadHours !== null) {
+          hoursDisplaySpan.textContent = `(${workloadHours}h)`
+          container.title = `${userName} (${workloadHours}h)`
+        } else {
+          hoursDisplaySpan.textContent = "" // Clear if no workload data
+          container.title = userName // Reset title to just name
+        }
+      }
+    })
+  }
+
+  // This method can be called if the userWorkloadsValue is updated dynamically
+  // For example, after a Turbo Stream update that changes the data attribute on the controller's element.
+  userWorkloadsValueChanged() {
+    // This is called when the data-conference-workload-user-workloads-value changes
+    this.updateAllAvatarWorkloads()
+  }
+
+  handleUserWorkloadUpdate(event) {
+    const { userId, newHours } = event.detail;
+    this._updateSpecificUserDisplay(userId, newHours);
+  }
+
+  _updateSpecificUserDisplay(userId, newHours) {
+    // Ensure userWorkloadsValue is initialized if it's not already an object
+    // and update it
+    if (typeof this.userWorkloadsValue !== 'object' || this.userWorkloadsValue === null) {
+      this.userWorkloadsValue = {};
+    }
+    this.userWorkloadsValue[String(userId)] = newHours; // Ensure userId is a string key
+
+    const avatarContainers = this.element.querySelectorAll(`.user-avatar-container[data-user-id="${userId}"]`);
+    avatarContainers.forEach(container => {
+      const hoursDisplaySpan = container.querySelector(".user-workload-hours-display");
+      const nameSpan = container.querySelector("span[style^='color:']");
+      
+      if (!nameSpan) return; // Should not happen if HTML is correct
+
+      // Get the first text node of the nameSpan, which should be the user's name
+      let userName = "";
+      for (let i = 0; i < nameSpan.childNodes.length; i++) {
+        if (nameSpan.childNodes[i].nodeType === Node.TEXT_NODE) {
+          userName = nameSpan.childNodes[i].textContent.trim();
+          break;
+        }
+      }
+      
+      if (hoursDisplaySpan) {
+        if (newHours !== undefined && newHours !== null) {
+          hoursDisplaySpan.textContent = ` (${newHours}h)`; // Note the leading space
+          container.title = `${userName} (${newHours}h)`;
+        } else {
+          hoursDisplaySpan.textContent = "";
+          container.title = userName;
+        }
+      }
+    });
+  }
+}
diff --git a/app/models/assignment.rb b/app/models/assignment.rb
index 73c3745e8d92b36e88eabb293300a73c5a6e7e72..bdb356990bee74249408b8d74a093144b9f06302 100644
--- a/app/models/assignment.rb
+++ b/app/models/assignment.rb
@@ -8,20 +8,61 @@ class Assignment < ApplicationRecord
   validate :no_overlapping_standby_assignments
 
   after_create_commit :notify_assignment_created
+  after_create_commit :remove_candidate_if_exists # Ensure this runs to clean up candidate record
+
   after_destroy_commit :notify_assignment_destroyed
+  after_destroy_commit :recreate_candidate_after_unassignment
+  after_destroy_commit :broadcast_session_update_on_destroy # Add this for consistency
 
   scope :future, -> { joins(:session).where("sessions.starts_at" => Time.now..) }
 
+  # This broadcast will re-render the session.
+  # The remove_candidate_if_exists callback should ensure data is correct before this.
   after_create_commit -> {
-    Rails.logger.debug("Created assignment, broadcasting")
-    broadcast_replace_to "sessions",
+    Rails.logger.debug("Assignment created, broadcasting session update via model callback (create)")
+    broadcast_replace_to stream_name_for_session,
       target: session,
       partial: "sessions/session",
-      locals: { session: session }
+      locals: { session: session.reload }
   }
 
   private
 
+  def remove_candidate_if_exists
+    Candidate.find_by(user: self.user, session: self.session)&.destroy
+  end
+
+  def recreate_candidate_after_unassignment
+    # Only create a candidate if one doesn't already exist for this user and session
+    # This check is important if, for some reason, a user could be unassigned while already being a candidate.
+    unless Candidate.exists?(user: self.user, session: self.session)
+      Candidate.create(user: self.user, session: self.session)
+      # We might want to reload the session object if it's used immediately after in another callback,
+      # but the broadcast will fetch/render with fresh data.
+    end
+  end
+
+  def broadcast_session_update_on_destroy
+    Rails.logger.debug("Assignment destroyed, broadcasting session update via model callback (destroy)")
+    # Important: self.session here refers to the session object associated with the
+    # assignment *before* it was destroyed. We need to ensure it's reloaded from DB
+    # in the partial rendering context to reflect the new candidate.
+    # The session.reload in the locals for the create broadcast is a good pattern.
+    # For destroy, the session object itself still exists.
+    reloaded_session = Session.find_by(id: self.session_id) # Ensure we get a fresh session
+    if reloaded_session
+      broadcast_replace_to stream_name_for_session(reloaded_session), # Pass reloaded session to stream name helper
+        target: reloaded_session,
+        partial: "sessions/session",
+        locals: { session: reloaded_session }
+    end
+  end
+
+  def stream_name_for_session(current_session = self.session)
+    # Default to a generic name, but ideally, this should be scoped, e.g., to the conference
+    current_session.conference # Use the passed session's conference
+  end
+
   def no_overlapping_assignments
     return if session.blank? || user.blank?
 
diff --git a/app/models/user.rb b/app/models/user.rb
index e0692ad7096134b0dd86bd7c56549fc131a63ac1..d0ab74349a1c4e9758f23bb04e607fae8fc12e3a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -86,7 +86,9 @@ class User < ApplicationRecord
   end
 
   def workload_minutes
-    Assignment.includes(:session).where(user: self).sum { |a| a.session.duration_minutes }
+    Assignment.joins(session: :conference)
+              .where(user: self, conferences: { active: true })
+              .sum { |a| a.session.duration_minutes }
   end
 
   def standby_minutes
diff --git a/app/views/assignments/_user_avatar.html.erb b/app/views/assignments/_user_avatar.html.erb
index 02537db42b8f8fe0ef2cbf5fbde19701adf3cf99..a806f803bac92207c407b9029cdd4b40699d230b 100644
--- a/app/views/assignments/_user_avatar.html.erb
+++ b/app/views/assignments/_user_avatar.html.erb
@@ -1,6 +1,6 @@
 <% user = assignment.user %>
-<span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10" style="background-color: <%= user.avatar_color %>" title="<%= user.name %>">
-  <span style="color: <%= user.text_color %>"><%= user.name %></span>
+<span class="user-avatar-container inline-flex items-center gap-x-0.5 rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10" style="background-color: <%= user.avatar_color %>" title="<%= user.name %>" data-user-id="<%= user.id %>">
+  <span style="color: <%= user.text_color %>"><%= user.name %> <span class="user-workload-hours-display"></span></span>
   <button type="button" class="only-shiftcoordinator hidden group relative -mr-1 size-3.5 rounded-sm hover:bg-gray-500/20">
     <%= link_to conference_session_assignment_path(assignment.session.conference, assignment.session, assignment), data: { turbo_method: :delete, confirm: 'Are you sure?', uid: user.id }, method: :delete do %>
       <span class="sr-only">Remove</span>
diff --git a/app/views/candidates/_user_avatar.html.erb b/app/views/candidates/_user_avatar.html.erb
index a40fc17d097ec5259663c04a759408ff8363c396..26e828877bfbfb2394f2c485ad905f1af152c19c 100644
--- a/app/views/candidates/_user_avatar.html.erb
+++ b/app/views/candidates/_user_avatar.html.erb
@@ -1,7 +1,7 @@
 <% user = candidate.user %>
 <% session = candidate.session %>
-<span class="gap-x-0.5 rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10" style="background-color: <%= user.avatar_color %>" title="<%= user.name %>">
-  <span style="color: <%= user.text_color %>"><%= user.name %></span>
+<span class="user-avatar-container gap-x-0.5 rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10" style="background-color: <%= user.avatar_color %>" title="<%= user.name %>" data-user-id="<%= user.id %>">
+  <span style="color: <%= user.text_color %>"><%= user.name %> <span class="user-workload-hours-display"></span></span>
   <button type="button" class="only-shiftcoordinator hidden group relative -mr-1 size-3.5 rounded-sm hover:bg-gray-500/20">
     <%= link_to conference_session_assignments_path(session.conference, session, user_id: user.id), class: "candidate", data: { turbo_method: :post, turbo_frame: dom_id(session), uid: user.id } do %>
       <span class="sr-only">Add</span>
diff --git a/app/views/conferences/show.html.erb b/app/views/conferences/show.html.erb
index 17b60a339dbc1b5d6cbc248f839a669ca03156ed..9c3cc7d9e45cb4989dba9e21e97e4dac274cbbf4 100644
--- a/app/views/conferences/show.html.erb
+++ b/app/views/conferences/show.html.erb
@@ -13,7 +13,7 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone)
     <%= render partial: 'assignments/filteredlist_option', locals: { user: } %>
   <% end %>
 </template>
-<div class="container mx-auto px-2 py-4 sm:px-4 lg:px-6 lg:py-8 text-black dark:text-white">
+<div class="container mx-auto px-2 py-4 sm:px-4 lg:px-6 lg:py-8 text-black dark:text-white" data-controller="conference-workload" data-conference-workload-user-workloads-value="<%= @user_workloads.to_json %>">
   <div class="mb-4">
     <a href="#now" onclick="document.querySelector('#now')?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); return false" class="underline text-blue-500">Jump to current time</a>
   </div>