diff --git a/app/modules/services/ui.py b/app/modules/services/ui.py
index 3884199..fef1357 100644
--- a/app/modules/services/ui.py
+++ b/app/modules/services/ui.py
@@ -1255,6 +1255,7 @@ def task_templates_detail(request: Request, catalogue_id: int):
task_template_file_map=task_template_file_map,
system_update_diff_map=system_update_diff_map,
system_update_retirement_map=system_update_retirement_map,
+ system_update_available_count=sum(1 for task in tasks if getattr(task, "system_update_available", False) and getattr(task, "source_system_task_id", None)),
can_manage_tasks=_can_manage_firm_tasks(db, user),
default_tasks=default_tasks,
active_default_task_count=sum(1 for task in default_tasks if task.is_active),
@@ -1645,6 +1646,101 @@ def firm_task_template_toggle_active(
db.close()
+@router.post('/templates/{catalogue_id}/system-updates/upgrade-all')
+def firm_tasks_accept_all_system_updates(
+ request: Request,
+ catalogue_id: int,
+ csrf_token: str = Form(...),
+):
+ """Upgrade every pending firm-task system revision for the active firm/service.
+
+ Firm-only tasks are intentionally untouched. Existing per-task upgrade/keep actions
+ remain available. Engagement synchronization runs once after all template updates so
+ pending/unstarted open-engagement tasks are refreshed safely without rewriting
+ started/completed/reviewed history.
+ """
+ validate_csrf(request, csrf_token)
+ db = CommonSessionLocal()
+ try:
+ user = get_current_user(request, db=db)
+ if not user:
+ return RedirectResponse(url='/login', status_code=303)
+ if not _can_manage_firm_tasks(db, user):
+ return _redirect_denied()
+
+ tenant_id = _active_tenant_id(request, user)
+ selection = get_firm_selection(db, tenant_id=tenant_id, catalogue_id=catalogue_id)
+ if not selection or not selection.is_enabled:
+ return RedirectResponse(url='/services', status_code=303)
+
+ pending_tasks = db.scalars(
+ select(FirmServiceTaskTemplate)
+ .where(
+ FirmServiceTaskTemplate.tenant_id == tenant_id,
+ FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
+ FirmServiceTaskTemplate.system_update_available.is_(True),
+ FirmServiceTaskTemplate.source_system_task_id.is_not(None),
+ )
+ .order_by(FirmServiceTaskTemplate.sequence_no, FirmServiceTaskTemplate.id)
+ ).all()
+
+ if not pending_tasks:
+ return RedirectResponse(
+ url=f'/services/templates/{catalogue_id}?system_bulk_none=1',
+ status_code=303,
+ )
+
+ upgraded = 0
+ retired = 0
+ for task in pending_tasks:
+ source = db.get(ServiceDefaultTaskTemplate, int(task.source_system_task_id))
+ if source is None:
+ continue
+ if not bool(source.is_active):
+ retired += 1
+ accept_system_update_for_firm_task(
+ db,
+ task=task,
+ user_id=user.id,
+ sync_open_engagements=False,
+ )
+ upgraded += 1
+
+ engagement = sync_open_engagement_tasks_for_service(
+ db,
+ tenant_id=tenant_id,
+ catalogue_id=catalogue_id,
+ user_id=user.id,
+ include_started_open_tasks=False,
+ safe_system_rollout=True,
+ )
+ db.commit()
+
+ return RedirectResponse(
+ url=(
+ f'/services/templates/{catalogue_id}?system_bulk_upgraded=1'
+ f'&bulk_count={upgraded}'
+ f'&bulk_retired={retired}'
+ f'&engagement_created={engagement.get("created", 0)}'
+ f'&engagement_updated={engagement.get("updated_pending", 0)}'
+ f'&engagement_deactivated={engagement.get("deactivated_pending", 0)}'
+ f'&engagement_preserved={engagement.get("preserved_history", 0)}'
+ ),
+ status_code=303,
+ )
+ except ValueError:
+ db.rollback()
+ return RedirectResponse(
+ url=f'/services/templates/{catalogue_id}?error=system_bulk_upgrade_failed',
+ status_code=303,
+ )
+ except Exception:
+ db.rollback()
+ raise
+ finally:
+ db.close()
+
+
@router.post('/templates/{catalogue_id}/tasks/{task_id}/system-update/upgrade')
def firm_task_accept_system_update(
request: Request,