## Description Resolved conversations now preserve historical SLA misses without allowing their displayed duration to keep growing. Applied SLAs record a stable completion timestamp that is shared through REST and realtime payloads, and the dashboard freezes FRT, NRT, and RT misses at that point. Legacy completed SLAs without a reliable timestamp remain visible as a static missed state. Terminal SLAs remain frozen when a conversation is reopened; a reopen before finalization continues the same SLA without resetting its deadlines. ### Closes [CW-7597](https://linear.app/chatwoot/issue/CW-7597/freeze-sla-miss-durations-after-conversation-resolution) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How to reproduce 1. Apply an SLA with a resolution-time threshold to a conversation. 2. Let the threshold breach, then resolve the conversation. 3. Observe that the recorded miss duration continues increasing every minute even though the conversation is resolved. ## What changed - Added nullable `applied_slas.completed_at` and exposed it as `sla_completed_at` in conversation, report, and websocket payloads. - Captured completion before broadcasting resolution and preserved it for terminal applied SLAs. - Frozen recorded FRT, NRT, and RT durations in classic and next-generation conversation labels, including a static fallback for legacy rows. - Added a dry-run-first, resumable Rails runner for account-scoped or explicitly global historical repair without enqueuing jobs or touching `updated_at`. Account-scoped production rollout starts with: ```sh ACCOUNT_ID=168154 bundle exec rails runner script/backfill_applied_sla_completed_at.rb ACCOUNT_ID=168154 APPLY=true bundle exec rails runner script/backfill_applied_sla_completed_at.rb ``` ## How Has This Been Tested? - Verified resolution stamping, nonterminal reopen clearing, and terminal reopen preservation. - Verified dry-run, apply, account/global scope, resume, skip, idempotency, and timestamp-preserving backfill behavior. - Verified all three miss types freeze and existing conversation-card behavior remains intact. - 71 focused RSpec examples and 37 focused Vitest examples pass. - RuboCop, ESLint, and diff checks pass. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
86 lines
2.1 KiB
JavaScript
86 lines
2.1 KiB
JavaScript
import { onMounted, onUnmounted, ref, unref, watch } from 'vue';
|
|
import {
|
|
evaluateSLAStatus,
|
|
shouldRefreshSLAStatus,
|
|
} from 'dashboard/helper/slaHelper';
|
|
|
|
const REFRESH_INTERVAL = 60000;
|
|
|
|
export const useSlaStatus = ({ appliedSla, chat, slaEvents }) => {
|
|
const timer = ref(null);
|
|
const slaStatus = ref({
|
|
threshold: null,
|
|
isSlaMissed: false,
|
|
type: null,
|
|
icon: null,
|
|
});
|
|
|
|
const updateSlaStatus = () => {
|
|
slaStatus.value = evaluateSLAStatus({
|
|
appliedSla: unref(appliedSla),
|
|
chat: unref(chat),
|
|
slaEvents: unref(slaEvents) || [],
|
|
});
|
|
};
|
|
|
|
const clearTimer = () => {
|
|
if (timer.value) {
|
|
clearTimeout(timer.value);
|
|
timer.value = null;
|
|
}
|
|
};
|
|
|
|
const createTimer = () => {
|
|
clearTimer();
|
|
if (
|
|
!shouldRefreshSLAStatus({
|
|
appliedSla: unref(appliedSla),
|
|
chat: unref(chat),
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
timer.value = setTimeout(() => {
|
|
updateSlaStatus();
|
|
createTimer();
|
|
}, REFRESH_INTERVAL);
|
|
};
|
|
|
|
const refreshSlaStatus = () => {
|
|
updateSlaStatus();
|
|
createTimer();
|
|
};
|
|
|
|
const getRefreshDependencies = () => {
|
|
const currentChat = unref(chat) || {};
|
|
const currentAppliedSla = unref(appliedSla) || {};
|
|
const currentSlaEvents = unref(slaEvents) || [];
|
|
|
|
return [
|
|
currentChat.status,
|
|
currentChat.firstReplyCreatedAt ?? currentChat.first_reply_created_at,
|
|
currentChat.waitingSince ?? currentChat.waiting_since,
|
|
currentAppliedSla.slaStatus ?? currentAppliedSla.sla_status,
|
|
currentAppliedSla.slaCompletedAt ?? currentAppliedSla.sla_completed_at,
|
|
currentAppliedSla.slaFrtDueAt ?? currentAppliedSla.sla_frt_due_at,
|
|
currentAppliedSla.slaNrtDueAt ?? currentAppliedSla.sla_nrt_due_at,
|
|
currentAppliedSla.slaRtDueAt ?? currentAppliedSla.sla_rt_due_at,
|
|
currentSlaEvents
|
|
.map(
|
|
event =>
|
|
`${event.eventType ?? event.event_type}:${event.createdAt ?? event.created_at}`
|
|
)
|
|
.join('|'),
|
|
];
|
|
};
|
|
|
|
onMounted(refreshSlaStatus);
|
|
onUnmounted(clearTimer);
|
|
watch(getRefreshDependencies, refreshSlaStatus);
|
|
|
|
return {
|
|
slaStatus,
|
|
};
|
|
};
|