From a558edd76321c3103c1b09806bd080cc8fdd3ac9 Mon Sep 17 00:00:00 2001 From: uddhavdave Date: Thu, 29 Oct 2026 10:16:49 +0000 Subject: [PATCH] fix(incidents): viewer token access, reopen, multi-field update and status confirms (#15994) --- .../src/request/alerts/incidents.rs | 20 ++++- src/core/src/alerts/incidents.rs | 4 + src/core/src/ofga/mod.rs | 21 ++++++ src/core/src/users.rs | 38 +++++++++- src/infra/src/table/alert_incidents.rs | 49 +++++++++++- .../pages/alertsPages/alertsPage.js | 4 + .../alerts/IncidentDetailDrawer.spec.ts | 43 +++++++++++ .../alerts/IncidentDetailDrawer.vue | 74 +++++++++---------- .../components/alerts/IncidentList.spec.ts | 16 ++++ web/src/components/alerts/IncidentList.vue | 20 ++++- .../iam/groups/GroupServiceAccounts.vue | 20 ++++- web/src/components/iam/roles/roleModules.ts | 2 + web/src/locales/languages/ar-SA.json | 5 +- web/src/locales/languages/de-DE.json | 5 +- web/src/locales/languages/en-US.json | 5 +- web/src/locales/languages/es-ES.json | 5 +- web/src/locales/languages/fr-FR.json | 5 +- web/src/locales/languages/it-IT.json | 5 +- web/src/locales/languages/ja-JP.json | 5 +- web/src/locales/languages/ko-KR.json | 5 +- web/src/locales/languages/nl-NL.json | 5 +- web/src/locales/languages/pl-PL.json | 5 +- web/src/locales/languages/pt-PT.json | 5 +- web/src/locales/languages/ru-RU.json | 5 +- web/src/locales/languages/tr-TR.json | 5 +- web/src/locales/languages/vi-VN.json | 5 +- web/src/locales/languages/zh-CN.json | 5 +- web/src/locales/languages/zh-TW.json | 5 +- 28 files changed, 321 insertions(+), 70 deletions(-) diff --git a/src/api/management/src/request/alerts/incidents.rs b/src/api/management/src/request/alerts/incidents.rs index 1f0861867d..aa82e73aaf 100644 --- a/src/api/management/src/request/alerts/incidents.rs +++ b/src/api/management/src/request/alerts/incidents.rs @@ -84,10 +84,9 @@ impl IncidentStatus { } } -/// Update payload enum for incident field updates -/// Ensures only one field can be updated per request +/// One field per request; `deny_unknown_fields` rejects a payload that sends a second one. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(untagged)] +#[serde(untagged, deny_unknown_fields)] pub enum UpdatePayload { /// Update incident title Title { title: String }, @@ -2305,6 +2304,21 @@ mod tests { } } + #[test] + fn test_update_payload_rejects_a_second_field() { + for json in [ + r#"{"title":"x","status":"resolved"}"#, + r#"{"severity":"P4","status":"resolved"}"#, + r#"{"title":"x","severity":"P1"}"#, + r#"{"status":"resolved","note":"x"}"#, + ] { + assert!( + serde_json::from_str::(json).is_err(), + "{json} must not deserialize to one field" + ); + } + } + #[test] fn test_update_status_serde_roundtrip() { for status in [ diff --git a/src/core/src/alerts/incidents.rs b/src/core/src/alerts/incidents.rs index 95ae1de798..c4bcca2479 100644 --- a/src/core/src/alerts/incidents.rs +++ b/src/core/src/alerts/incidents.rs @@ -3112,6 +3112,10 @@ pub async fn update_status_as( } else { infra::table::alert_incidents::update_status(org_id, incident_id, status).await? }; + // The ack guard left a resolved row alone: record and replicate nothing. + if status == "acknowledged" && updated.status != "acknowledged" { + return model_to_incident(updated).await; + } if status == "acknowledged" && updated.acknowledged_by.as_deref() == Some(user_id) { ack_incident_alerts(org_id, incident_id, user_id).await; } diff --git a/src/core/src/ofga/mod.rs b/src/core/src/ofga/mod.rs index a14e29f55b..b2bc68e6c4 100644 --- a/src/core/src/ofga/mod.rs +++ b/src/core/src/ofga/mod.rs @@ -79,6 +79,7 @@ struct PendingMigrations { ai_chat_shares: bool, fleet: bool, public_dashboards: bool, + incident_integrations: bool, missing_ownership: bool, profiles: bool, } @@ -575,6 +576,9 @@ fn all_org_ownership_keys(pending: &PendingMigrations) -> Vec<&'static str> { if pending.profiles { keys.push("profiles"); } + if pending.incident_integrations { + keys.push("incident_integrations"); + } keys } @@ -766,6 +770,11 @@ fn pending_migrations(latest: &str, existing: &str) -> PendingMigrations { log::info!("[OFGA:Local] profiles stream permissions migration needed"); pending.profiles = true; } + let v0_0_62 = version_compare::Version::from("0.0.62").unwrap(); + if meta_version >= v0_0_62 && existing_model_version < v0_0_62 { + log::info!("[OFGA:Local] incident integrations permissions migration needed"); + pending.incident_integrations = true; + } pending } @@ -884,4 +893,16 @@ mod tests { ); assert!(OFGA_MODELS.contains_key("profiles")); } + + #[test] + fn incident_integrations_back_fill_runs_once_across_0_0_62() { + let pending = pending_migrations("0.0.62", "0.0.61"); + assert!(pending.incident_integrations && !pending.profiles); + assert_eq!( + all_org_ownership_keys(&pending), + vec!["incident_integrations"] + ); + assert!(!pending_migrations("0.0.62", "0.0.62").incident_integrations); + assert!(!pending_migrations("0.0.61", "0.0.60").incident_integrations); + } } diff --git a/src/core/src/users.rs b/src/core/src/users.rs index 42fde8a69f..ba1641cad1 100644 --- a/src/core/src/users.rs +++ b/src/core/src/users.rs @@ -75,6 +75,13 @@ impl RemovalTarget { } } +/// True when `requested` is not the same set of custom roles as `current`. +#[cfg(feature = "enterprise")] +fn custom_roles_differ(requested: &[String], current: &[String]) -> bool { + let requested: std::collections::HashSet<&String> = requested.iter().collect(); + requested != current.iter().collect() +} + fn redact_token(token: &str) -> String { if token.is_empty() { return String::new(); @@ -501,9 +508,20 @@ pub async fn update_user( is_org_updated |= local_user.role.ne(&new_user.role); #[cfg(feature = "enterprise")] if let Some(cr) = new_org_role.custom_role { - custom_roles_need_change = true; - custom_roles.extend(cr); - is_org_updated = true; + // Only an admin grants custom roles; a self-update may resend its own unchanged. + if update_mode.is_self_update() + && !matches!(local_user.role, UserRole::Admin | UserRole::Root) + && custom_roles_differ( + &cr, + &o2_openfga::authorizer::roles::get_roles_for_org_user(org_id, email).await, + ) + { + message = "Self custom roles cannot be changed"; + } else { + custom_roles_need_change = true; + custom_roles.extend(cr); + is_org_updated = true; + } } } } @@ -1577,6 +1595,20 @@ mod tests { // Mutex to ensure test setup is serialized to prevent race conditions static TEST_SETUP_LOCK: tokio::sync::OnceCell> = tokio::sync::OnceCell::const_new(); + #[cfg(feature = "enterprise")] + #[test] + fn a_self_update_may_resend_its_custom_roles_but_not_change_them() { + let current = ["a".to_string(), "b".to_string()]; + assert!(!custom_roles_differ(&["b".into(), "a".into()], ¤t)); + assert!(custom_roles_differ( + &["a".into(), "b".into(), "c".into()], + ¤t + )); + assert!(custom_roles_differ(&["a".into()], ¤t)); + assert!(custom_roles_differ(&["c".into()], &[])); + assert!(!custom_roles_differ(&[], &[])); + } + #[test] fn test_redact_token_exactly_four_chars() { // 4 visible chars + 8 stars = 12 total diff --git a/src/infra/src/table/alert_incidents.rs b/src/infra/src/table/alert_incidents.rs index ab7ea91f92..5882bbd769 100644 --- a/src/infra/src/table/alert_incidents.rs +++ b/src/infra/src/table/alert_incidents.rs @@ -327,7 +327,7 @@ pub async fn update_status( update_status_with(get_orm_client_rw().await, org_id, id, status).await } -/// [`update_status`] on a given connection; a resolve ends the downtime mute. +/// [`update_status`] on a given connection; resolve ends the mute, reopen clears resolve and ack. pub async fn update_status_with( conn: &C, org_id: &str, @@ -343,6 +343,10 @@ pub async fn update_status_with( .map_err(|e| Error::DbError(DbError::SeaORMError(e.to_string())))? .ok_or_else(|| Error::DbError(DbError::SeaORMError("Incident not found".to_string())))?; + // Like `acknowledge()`, an ack only takes an open incident, so a replica cannot undo a resolve. + if status == "acknowledged" && incident.status != "open" { + return Ok(incident); + } let mut active: alert_incidents::ActiveModel = incident.into(); active.status = Set(status.to_string()); active.updated_at = Set(now); @@ -351,6 +355,10 @@ pub async fn update_status_with( active.resolved_at = Set(Some(now)); // A manual reopen must run as a live incident, not stay muted forever. active.muted_by_downtime_id = Set(None); + } else if status == "open" { + active.resolved_at = Set(None); + active.acknowledged_by = Set(None); + active.acknowledged_at = Set(None); } active @@ -1459,6 +1467,45 @@ mod tests { ); } + #[tokio::test] + async fn a_reopen_clears_the_resolve_and_the_acknowledgement() { + let db = incidents_db().await; + let mut incident = new_incident( + "acme", + "P2", + serde_json::json!({"service": "payments"}), + "service", + 1, + None, + None, + ); + incident.status = Set("acknowledged".to_string()); + incident.acknowledged_by = Set(Some("ana@example.com".to_string())); + incident.acknowledged_at = Set(Some(2)); + let incident = incident.insert(&db).await.unwrap(); + + let resolved = update_status_with(&db, "acme", &incident.id, "resolved") + .await + .unwrap(); + assert!(resolved.resolved_at.is_some()); + assert_eq!(resolved.acknowledged_by.as_deref(), Some("ana@example.com")); + + // A replicated ack never clears the resolve; only a reopen does. + let acked = update_status_with(&db, "acme", &incident.id, "acknowledged") + .await + .unwrap(); + assert_eq!(acked.status, "resolved"); + assert_eq!(acked.resolved_at, resolved.resolved_at); + + let reopened = update_status_with(&db, "acme", &incident.id, "open") + .await + .unwrap(); + assert_eq!(reopened.status, "open"); + assert_eq!(reopened.resolved_at, None); + assert_eq!(reopened.acknowledged_by, None); + assert_eq!(reopened.acknowledged_at, None); + } + #[tokio::test] async fn the_list_carries_the_muting_downtime_for_the_muted_chip() { let db = incidents_db().await; diff --git a/tests/ui-testing/pages/alertsPages/alertsPage.js b/tests/ui-testing/pages/alertsPages/alertsPage.js index 724bc4f7a7..4489cc1a76 100644 --- a/tests/ui-testing/pages/alertsPages/alertsPage.js +++ b/tests/ui-testing/pages/alertsPages/alertsPage.js @@ -207,6 +207,7 @@ export class AlertsPage { alertCloneButton: '[data-test="alert-list-{alertName}-clone-alert"]', alertMoreOptions: '[data-test="alert-list-{alertName}-more-options"]', confirmButton: '[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]', + confirmDialogProviderPrimary: '[data-test="confirm-dialog-provider"] [data-test="o-dialog-primary-btn"]', // Alert movement locators selectAllCheckboxRowName: '# Name Owner Period Frequency', @@ -2388,6 +2389,7 @@ export class AlertsPage { testLogger.info(`Target lifecycle row index: ${targetIndex}`); await allRows.nth(targetIndex).locator(this.locators.incidentAckButton).click(); + await this.page.locator(this.locators.confirmDialogProviderPrimary).click(); await this.page.waitForTimeout(1000); testLogger.info('Clicked acknowledge button'); } @@ -2409,6 +2411,7 @@ export class AlertsPage { const resolveButton = row.locator(this.locators.incidentResolveButton); await resolveButton.waitFor({ state: 'visible', timeout: 10000 }); await resolveButton.click(); + await this.page.locator(this.locators.confirmDialogProviderPrimary).click(); await this.page.waitForTimeout(1000); testLogger.info('Clicked resolve button'); } @@ -2422,6 +2425,7 @@ export class AlertsPage { const reopenButton = row.locator(this.locators.incidentReopenButton); await reopenButton.waitFor({ state: 'visible', timeout: 10000 }); await reopenButton.click(); + await this.page.locator(this.locators.confirmDialogProviderPrimary).click(); await this.page.waitForTimeout(1000); testLogger.info('Clicked reopen button'); } diff --git a/web/src/components/alerts/IncidentDetailDrawer.spec.ts b/web/src/components/alerts/IncidentDetailDrawer.spec.ts index 5679f210dc..2999c151e5 100644 --- a/web/src/components/alerts/IncidentDetailDrawer.spec.ts +++ b/web/src/components/alerts/IncidentDetailDrawer.spec.ts @@ -487,6 +487,49 @@ describe("IncidentDetailDrawer.vue", () => { expect(incidentsService.updateStatus).toHaveBeenCalledWith("default", "1", "open"); }); + it.each([ + ["resolveIncident", "Resolve incident?"], + ["reopenIncident", "Reopen incident?"], + ])("%s asks first and changes nothing when cancelled", async (action, title) => { + mockConfirm.mockResolvedValueOnce(false); + + await wrapper.vm[action](); + + expect(mockConfirm).toHaveBeenLastCalledWith(expect.objectContaining({ title })); + expect(incidentsService.updateStatus).not.toHaveBeenCalled(); + }); + + it.each([ + ["resolved", "Resolve incident?"], + ["open", "Reopen incident?"], + ])( + "the Manage chip for %s asks first and changes nothing when cancelled", + async (status, title) => { + mockConfirm.mockResolvedValueOnce(false); + + await wrapper.vm.handleStatusChange(status); + + expect(mockConfirm).toHaveBeenLastCalledWith(expect.objectContaining({ title })); + expect(incidentsService.updateStatus).not.toHaveBeenCalled(); + }, + ); + + it("a reopen from the Manage chip drops the cleared resolve and acknowledgement", async () => { + wrapper.vm.incidentDetails.status = "resolved"; + wrapper.vm.incidentDetails.resolved_at = 1700000000000000; + wrapper.vm.incidentDetails.acknowledged_by = "ana@example.com"; + vi.mocked(incidentsService.updateStatus).mockResolvedValueOnce({ + data: { status: "open", resolved_at: null, acknowledged_by: null, acknowledged_at: null }, + } as never); + + await wrapper.vm.handleStatusChange("open"); + + expect(incidentsService.updateStatus).toHaveBeenCalledWith("default", "1", "open"); + expect(wrapper.vm.incidentDetails.status).toBe("open"); + expect(wrapper.vm.incidentDetails.resolved_at).toBeUndefined(); + expect(wrapper.vm.incidentDetails.acknowledged_by).toBeNull(); + }); + it("should set updating state during status update", async () => { // acknowledgeIncident awaits the confirm dialog before it ever touches // `updating`, so a fixed number of microtask hops can't be assumed here — diff --git a/web/src/components/alerts/IncidentDetailDrawer.vue b/web/src/components/alerts/IncidentDetailDrawer.vue index 3068020cb5..ee4752d67d 100644 --- a/web/src/components/alerts/IncidentDetailDrawer.vue +++ b/web/src/components/alerts/IncidentDetailDrawer.vue @@ -2692,6 +2692,7 @@ export default defineComponent({ incidentDetails.value.status = response.data.status; incidentDetails.value.acknowledged_by = response.data.acknowledged_by; incidentDetails.value.acknowledged_at = response.data.acknowledged_at; + incidentDetails.value.resolved_at = response.data.resolved_at ?? undefined; incidentDetails.value.updated_at = response.data.updated_at || Date.now() * 1000; editableStatus.value = response.data.status; toast({ @@ -2734,8 +2735,29 @@ export default defineComponent({ // The caller's promise must cover the update, not just the confirm dialog. return updateStatus("acknowledged"); }; - const resolveIncident = () => updateStatus("resolved"); - const reopenIncident = () => updateStatus("open"); + // Resolving closes the incident, so it confirms like Acknowledge; Reopen too. + const resolveIncident = async () => { + const ok = await confirm({ + title: t("alerts.incidents.resolveConfirmTitle"), + message: t("alerts.incidents.resolveConfirmMessage"), + confirmLabel: t("alerts.incidents.resolve"), + cancelLabel: t("common.cancel"), + persistent: false, + }); + if (!ok) return; + return updateStatus("resolved"); + }; + const reopenIncident = async () => { + const ok = await confirm({ + title: t("alerts.incidents.reopenConfirmTitle"), + message: t("alerts.incidents.reopenConfirmMessage"), + confirmLabel: t("alerts.incidents.reopen"), + cancelLabel: t("common.cancel"), + persistent: false, + }); + if (!ok) return; + return updateStatus("open"); + }; // Title editing. OInlineEdit owns the open/close and the Escape-restores // behaviour; this only has to persist a committed value. `isEditingTitle` @@ -2883,45 +2905,15 @@ export default defineComponent({ return typeof conditionData === "string" ? conditionData : "--"; }; - // Handle status change from dropdown - const handleStatusChange = async (newStatus: "open" | "acknowledged" | "resolved") => { - if (!incidentDetails.value || updating.value) return; - - updating.value = true; - try { - const response = await updateIncidentStatus.mutateAsync({ - id: incidentDetails.value.id, - status: newStatus, - }); - - // Update local state with the actual status from the API response - incidentDetails.value.status = response.data.status; - editableStatus.value = response.data.status; - - toast({ - variant: "success", - message: t("toastMessages.alerts.incidentStatusUpdatedTo", { - status: response.data.status, - }), - }); - // Mark data as stale so incident list will refresh - store.dispatch("incidents/setShouldRefresh", true); - - // Refresh events in timeline if Activity tab is active - if (activeTab.value === "activity") { - timelineRefreshTrigger.value++; - } - } catch (error: any) { - console.error("Failed to update status:", error); - toast({ - variant: "error", - message: error?.response?.data?.message || t("alerts.incidents.statusUpdateFailed"), - }); - // Revert on error - editableStatus.value = incidentDetails.value.status; - } finally { - updating.value = false; - } + // The Manage panel chips take the same confirmed path as the header buttons. + const handleStatusChange = (newStatus: "open" | "acknowledged" | "resolved") => { + if (updating.value) return; + const actions = { + open: reopenIncident, + acknowledged: acknowledgeIncident, + resolved: resolveIncident, + }; + return actions[newStatus](); }; // Handle trigger row click - find the alert index by name diff --git a/web/src/components/alerts/IncidentList.spec.ts b/web/src/components/alerts/IncidentList.spec.ts index 005bf778ff..390d0a3dc2 100644 --- a/web/src/components/alerts/IncidentList.spec.ts +++ b/web/src/components/alerts/IncidentList.spec.ts @@ -460,6 +460,22 @@ describe("IncidentList.vue", () => { expect(incidentsService.updateStatus).toHaveBeenCalledWith("default", incident.id, "open"); }); + it.each([ + ["resolveIncident", "Resolve incident?"], + ["reopenIncident", "Reopen incident?"], + ] as const)("%s asks first and changes nothing when cancelled", async (action, title) => { + // The instance exposes setup() bindings that the mounted type does not declare. + const vm = wrapper.vm as unknown as Record< + typeof action, + (incident: Incident) => Promise + > & { allIncidents: Incident[] }; + mockConfirm.mockResolvedValueOnce(false); + await vm[action](vm.allIncidents[0]); + await flushPromises(); + expect(mockConfirm).toHaveBeenLastCalledWith(expect.objectContaining({ title })); + expect(incidentsService.updateStatus).not.toHaveBeenCalled(); + }); + it("reloads incidents after successful status update", async () => { const incident = (wrapper.vm as any).allIncidents[0]; vi.clearAllMocks(); diff --git a/web/src/components/alerts/IncidentList.vue b/web/src/components/alerts/IncidentList.vue index 916bec4a2e..16d9d719f7 100644 --- a/web/src/components/alerts/IncidentList.vue +++ b/web/src/components/alerts/IncidentList.vue @@ -914,11 +914,27 @@ export default defineComponent({ updateStatus(incident, "acknowledged"); }; - const resolveIncident = (incident: Incident) => { + const resolveIncident = async (incident: Incident) => { + const ok = await confirm({ + title: t("alerts.incidents.resolveConfirmTitle"), + message: t("alerts.incidents.resolveConfirmMessage"), + confirmLabel: t("alerts.incidents.resolve"), + cancelLabel: t("common.cancel"), + persistent: false, + }); + if (!ok) return; updateStatus(incident, "resolved"); }; - const reopenIncident = (incident: Incident) => { + const reopenIncident = async (incident: Incident) => { + const ok = await confirm({ + title: t("alerts.incidents.reopenConfirmTitle"), + message: t("alerts.incidents.reopenConfirmMessage"), + confirmLabel: t("alerts.incidents.reopen"), + cancelLabel: t("common.cancel"), + persistent: false, + }); + if (!ok) return; updateStatus(incident, "open"); }; diff --git a/web/src/components/iam/groups/GroupServiceAccounts.vue b/web/src/components/iam/groups/GroupServiceAccounts.vue index 24631d319e..091b272c6a 100644 --- a/web/src/components/iam/groups/GroupServiceAccounts.vue +++ b/web/src/components/iam/groups/GroupServiceAccounts.vue @@ -60,6 +60,7 @@ along with this program. If not, see . :columns="columns" row-key="email" :loading="props.loading" + :forbidden="forbidden" :global-filter="userSearchKey" pagination="client" :page-size="100" @@ -158,6 +159,9 @@ const userSearchKey = ref(""); const hasFetchedOrgServiceAccounts = ref(false); +// A viewer cannot list service accounts; show the no-access state instead of failing. +const forbidden = ref(false); + const groupUsersMap = ref(new Set()); const { serviceAccountsState } = usePermissions(); @@ -220,10 +224,18 @@ const updateUserTable = async (value: string) => { const fetchOrgServiceAccounts = async () => { // fetch group users hasFetchedOrgServiceAccounts.value = true; - const data: any = await serviceAccountsState.getServiceAccounts( - store.state.selectedOrganization.identifier, - ); - + forbidden.value = false; + let data: Array<{ email: string; is_system?: boolean }>; + try { + data = (await serviceAccountsState.getServiceAccounts( + store.state.selectedOrganization.identifier, + )) as typeof data; + } catch (e: unknown) { + const status = (e as { response?: { status?: number } } | undefined)?.response?.status; + if (status !== 403) throw e; + forbidden.value = true; + data = []; + } serviceAccountsState.service_accounts_users = cloneDeep( data .filter((user: any) => user.is_system !== true) // Filter out system accounts diff --git a/web/src/components/iam/roles/roleModules.ts b/web/src/components/iam/roles/roleModules.ts index 7e785fefa0..55c9c0cdce 100644 --- a/web/src/components/iam/roles/roleModules.ts +++ b/web/src/components/iam/roles/roleModules.ts @@ -112,6 +112,7 @@ const GROUP_OF: Record = { template: "alerting", destination: "alerting", incidents: "alerting", + incident_integrations: "alerting", oncall: "alerting", oncall_response: "alerting", pipeline: "pipelines", @@ -181,6 +182,7 @@ const ICON_OF: Record = { template: "article", destination: "webhook", incidents: "shield-alert-outline", + incident_integrations: "webhook", oncall: "person-pin-circle", oncall_response: "forum", pipeline: "lan", diff --git a/web/src/locales/languages/ar-SA.json b/web/src/locales/languages/ar-SA.json index fae86e6b4a..7acd9612bc 100644 --- a/web/src/locales/languages/ar-SA.json +++ b/web/src/locales/languages/ar-SA.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "يؤدي هذا إلى وضع الحادث كقيد العمل. يمكنك إعادة فتحه لاحقًا إذا لزم الأمر.", "acknowledgeConfirmLabel": "إقرار", "acknowledgeConfirmCancelLabel": "إلغاء", + "resolveConfirmTitle": "حل الحادث؟", + "resolveConfirmMessage": "يؤدي هذا إلى إغلاق الحادث وإزالته من القائمة النشطة. يمكنك إعادة فتحه لاحقًا إذا لزم الأمر.", + "reopenConfirmTitle": "إعادة فتح الحادث؟", + "reopenConfirmMessage": "يؤدي هذا إلى إعادة الحادث إلى حالة مفتوح ومسح حله والإقرار به.", "acknowledgeAriaLabel": "الإقرار بالحادث", "acknowledgedBy": "تم الإقرار بواسطة", "resolve": "العزم", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "تم استيراد {count} قالب بنجاح | تم استيراد {count} من القوالب بنجاح", "aiAnalysisIsAlreadyRunningFor": "تحليل الذكاء الاصطناعي جار بالفعل لهذا الحادث", "incidentSeverityUpdatedTo": "تم تحديث خطورة الحادث إلى {severity}", - "incidentStatusUpdatedTo": "تم تحديث حالة الحادث إلى {status}", "failedToLoadIncidentDetails": "فشل في تحميل تفاصيل الحادث", "incidentsRefreshed": "الحوادث التي تم تحديثها", "failedToPostComment": "فشل في نشر التعليق", diff --git a/web/src/locales/languages/de-DE.json b/web/src/locales/languages/de-DE.json index 39496ff525..bda350d219 100644 --- a/web/src/locales/languages/de-DE.json +++ b/web/src/locales/languages/de-DE.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Damit wird der Vorfall als in Bearbeitung markiert. Sie können ihn später bei Bedarf wieder öffnen.", "acknowledgeConfirmLabel": "Bestätigen", "acknowledgeConfirmCancelLabel": "Abbrechen", + "resolveConfirmTitle": "Vorfall auflösen?", + "resolveConfirmMessage": "Dadurch wird der Vorfall geschlossen und aus der aktiven Liste entfernt. Sie können ihn bei Bedarf später wieder öffnen.", + "reopenConfirmTitle": "Vorfall wieder öffnen?", + "reopenConfirmMessage": "Dadurch wird der Vorfall wieder auf offen gesetzt und seine Auflösung und Bestätigung werden entfernt.", "acknowledgeAriaLabel": "Vorfall bestätigen", "acknowledgedBy": "Bestätigt von", "resolve": "Auflösen", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "Erfolgreich {count} Vorlage importiert | Erfolgreich {count} Vorlagen importiert", "aiAnalysisIsAlreadyRunningFor": "Für diesen Vorfall läuft bereits eine KI-Analyse", "incidentSeverityUpdatedTo": "Schweregrad des Vorfalls auf {severity} aktualisiert", - "incidentStatusUpdatedTo": "Status des Vorfalls auf {status} aktualisiert", "failedToLoadIncidentDetails": "Fehler beim Laden der Vorfall-Details", "incidentsRefreshed": "Vorfälle aktualisiert", "failedToPostComment": "Fehler beim Posten des Kommentars", diff --git a/web/src/locales/languages/en-US.json b/web/src/locales/languages/en-US.json index f15433c074..15af3c6c71 100644 --- a/web/src/locales/languages/en-US.json +++ b/web/src/locales/languages/en-US.json @@ -10268,6 +10268,10 @@ "acknowledgeConfirmMessage": "This marks the incident as being worked on. You can reopen it later if needed.", "acknowledgeConfirmLabel": "Acknowledge", "acknowledgeConfirmCancelLabel": "Cancel", + "resolveConfirmTitle": "Resolve incident?", + "resolveConfirmMessage": "This closes the incident and removes it from the active list. You can reopen it later if needed.", + "reopenConfirmTitle": "Reopen incident?", + "reopenConfirmMessage": "This sets the incident back to open and clears its resolution and acknowledgement.", "acknowledgeAriaLabel": "Acknowledge incident", "acknowledgedBy": "Acknowledged by", "resolve": "Resolve", @@ -20995,7 +20999,6 @@ "successfullyImportedTemplates": "Successfully imported {count} template | Successfully imported {count} templates", "aiAnalysisIsAlreadyRunningFor": "AI analysis is already running for this incident", "incidentSeverityUpdatedTo": "Incident severity updated to {severity}", - "incidentStatusUpdatedTo": "Incident status updated to {status}", "failedToLoadIncidentDetails": "Failed to load incident details", "incidentsRefreshed": "Incidents refreshed", "failedToPostComment": "Failed to post comment", diff --git a/web/src/locales/languages/es-ES.json b/web/src/locales/languages/es-ES.json index e2c49313e0..985fe3cb45 100644 --- a/web/src/locales/languages/es-ES.json +++ b/web/src/locales/languages/es-ES.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Esto marca el incidente como en curso. Puedes reabrirlo más tarde si es necesario.", "acknowledgeConfirmLabel": "Reconocer", "acknowledgeConfirmCancelLabel": "Cancelar", + "resolveConfirmTitle": "¿Resolver incidente?", + "resolveConfirmMessage": "Esto cierra el incidente y lo quita de la lista activa. Puedes reabrirlo más tarde si es necesario.", + "reopenConfirmTitle": "¿Reabrir incidente?", + "reopenConfirmMessage": "Esto devuelve el incidente al estado abierto y borra su resolución y reconocimiento.", "acknowledgeAriaLabel": "Reconocer incidente", "acknowledgedBy": "Reconocido por", "resolve": "Resolver", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "Se importó correctamente {count} plantilla | Se importaron correctamente {count} plantillas", "aiAnalysisIsAlreadyRunningFor": "El análisis de IA ya se está ejecutando para este incidente", "incidentSeverityUpdatedTo": "Gravedad del incidente actualizada a {severity}", - "incidentStatusUpdatedTo": "Estado del incidente actualizado a {status}", "failedToLoadIncidentDetails": "No se pudieron cargar los detalles del incidente", "incidentsRefreshed": "Incidentes actualizados", "failedToPostComment": "No se pudo publicar el comentario", diff --git a/web/src/locales/languages/fr-FR.json b/web/src/locales/languages/fr-FR.json index 4ff9b28e05..7b875ac499 100644 --- a/web/src/locales/languages/fr-FR.json +++ b/web/src/locales/languages/fr-FR.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Cela marque l'incident comme étant en cours de traitement. Vous pourrez le rouvrir plus tard si nécessaire.", "acknowledgeConfirmLabel": "Acquitter", "acknowledgeConfirmCancelLabel": "Annuler", + "resolveConfirmTitle": "Résoudre l'incident ?", + "resolveConfirmMessage": "Cela ferme l'incident et le retire de la liste active. Vous pourrez le rouvrir plus tard si nécessaire.", + "reopenConfirmTitle": "Rouvrir l'incident ?", + "reopenConfirmMessage": "Cela remet l'incident à l'état ouvert et efface sa résolution et son acquittement.", "acknowledgeAriaLabel": "Acquitter l'incident", "acknowledgedBy": "Acquitté par", "resolve": "Résoudre", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "{count} modèle importé avec succès | {count} modèles importés avec succès", "aiAnalysisIsAlreadyRunningFor": "Une analyse IA est déjà en cours pour cet incident", "incidentSeverityUpdatedTo": "La gravité de l'incident a été mise à jour vers {severity}", - "incidentStatusUpdatedTo": "Le statut de l'incident a été mis à jour vers {status}", "failedToLoadIncidentDetails": "Échec du chargement des détails de l'incident", "incidentsRefreshed": "Incidents actualisés", "failedToPostComment": "Échec de l'envoi du commentaire", diff --git a/web/src/locales/languages/it-IT.json b/web/src/locales/languages/it-IT.json index 1bef232d25..02f9e8690c 100644 --- a/web/src/locales/languages/it-IT.json +++ b/web/src/locales/languages/it-IT.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Questa azione contrassegna l'incidente come preso in carico. Puoi riaprirlo in seguito, se necessario.", "acknowledgeConfirmLabel": "Prendi in carico", "acknowledgeConfirmCancelLabel": "Annulla", + "resolveConfirmTitle": "Risolvere l'incidente?", + "resolveConfirmMessage": "Questo chiude l'incidente e lo rimuove dall'elenco attivo. Puoi riaprirlo più tardi se necessario.", + "reopenConfirmTitle": "Riaprire l'incidente?", + "reopenConfirmMessage": "Questo riporta l'incidente allo stato aperto e cancella la risoluzione e la presa in carico.", "acknowledgeAriaLabel": "Prendi in carico l'incidente", "acknowledgedBy": "Preso in carico da", "resolve": "Risolvi", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "{count} template importato con successo | {count} template importati con successo", "aiAnalysisIsAlreadyRunningFor": "L'analisi AI è già in esecuzione per questo incidente", "incidentSeverityUpdatedTo": "Gravità dell'incidente aggiornata a {severity}", - "incidentStatusUpdatedTo": "Stato dell'incidente aggiornato a {status}", "failedToLoadIncidentDetails": "Impossibile caricare i dettagli dell'incidente", "incidentsRefreshed": "Incidenti aggiornati", "failedToPostComment": "Impossibile pubblicare il commento", diff --git a/web/src/locales/languages/ja-JP.json b/web/src/locales/languages/ja-JP.json index 443b158b43..1c5a49a9f8 100644 --- a/web/src/locales/languages/ja-JP.json +++ b/web/src/locales/languages/ja-JP.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "これによりインシデントは対応中としてマークされます。必要に応じて後で再度オープンできます。", "acknowledgeConfirmLabel": "確認済みにする", "acknowledgeConfirmCancelLabel": "キャンセル", + "resolveConfirmTitle": "インシデントを解決しますか?", + "resolveConfirmMessage": "インシデントをクローズし、アクティブな一覧から削除します。必要に応じて後で再オープンできます。", + "reopenConfirmTitle": "インシデントを再オープンしますか?", + "reopenConfirmMessage": "インシデントをオープン状態に戻し、解決と確認済みの情報をクリアします。", "acknowledgeAriaLabel": "インシデントを確認済みにする", "acknowledgedBy": "確認者", "resolve": "解決", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "テンプレートを{count}件正常にインポートしました | テンプレートを{count}件正常にインポートしました", "aiAnalysisIsAlreadyRunningFor": "このインシデントに対してAI分析が既に実行されています", "incidentSeverityUpdatedTo": "インシデントの重要度が{severity}に更新されました", - "incidentStatusUpdatedTo": "インシデントのステータスが{status}に更新されました", "failedToLoadIncidentDetails": "インシデント詳細の読み込みに失敗しました", "incidentsRefreshed": "インシデントを更新しました", "failedToPostComment": "コメントの投稿に失敗しました", diff --git a/web/src/locales/languages/ko-KR.json b/web/src/locales/languages/ko-KR.json index c59c8a74c3..90f1bb6aec 100644 --- a/web/src/locales/languages/ko-KR.json +++ b/web/src/locales/languages/ko-KR.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "이 작업은 인시던트가 작업 중임을 표시합니다. 필요한 경우 나중에 다시 열 수 있습니다.", "acknowledgeConfirmLabel": "확인", "acknowledgeConfirmCancelLabel": "취소", + "resolveConfirmTitle": "인시던트를 해결하시겠습니까?", + "resolveConfirmMessage": "인시던트를 종료하고 활성 목록에서 제거합니다. 필요하면 나중에 다시 열 수 있습니다.", + "reopenConfirmTitle": "인시던트를 다시 여시겠습니까?", + "reopenConfirmMessage": "인시던트를 열린 상태로 되돌리고 해결 및 확인 정보를 지웁니다.", "acknowledgeAriaLabel": "인시던트 확인", "acknowledgedBy": "확인자", "resolve": "해결", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "{count}개 템플릿을 성공적으로 가져왔습니다. | {count}개 템플릿을 성공적으로 가져왔습니다.", "aiAnalysisIsAlreadyRunningFor": "이 인시던트에 대해 AI 분석이 이미 실행 중입니다.", "incidentSeverityUpdatedTo": "인시던트 심각도가 {severity}(으)로 업데이트되었습니다.", - "incidentStatusUpdatedTo": "인시던트 상태가 {status}(으)로 업데이트되었습니다.", "failedToLoadIncidentDetails": "인시던트 세부 정보를 불러오지 못했습니다.", "incidentsRefreshed": "인시던트가 새로 고쳐졌습니다.", "failedToPostComment": "댓글을 게시하지 못했습니다.", diff --git a/web/src/locales/languages/nl-NL.json b/web/src/locales/languages/nl-NL.json index 6bc267cbc3..5c85d1cf3d 100644 --- a/web/src/locales/languages/nl-NL.json +++ b/web/src/locales/languages/nl-NL.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Hiermee wordt het incident gemarkeerd als in behandeling. Je kunt het later opnieuw openen als dat nodig is.", "acknowledgeConfirmLabel": "Bevestigen", "acknowledgeConfirmCancelLabel": "Annuleren", + "resolveConfirmTitle": "Incident oplossen?", + "resolveConfirmMessage": "Hiermee wordt het incident gesloten en uit de actieve lijst verwijderd. U kunt het later zo nodig heropenen.", + "reopenConfirmTitle": "Incident heropenen?", + "reopenConfirmMessage": "Hiermee wordt het incident weer op open gezet en worden de oplossing en bevestiging gewist.", "acknowledgeAriaLabel": "Incident bevestigen", "acknowledgedBy": "Bevestigd door", "resolve": "Oplossen", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "{count} sjabloon succesvol geïmporteerd | {count} sjablonen succesvol geïmporteerd", "aiAnalysisIsAlreadyRunningFor": "AI-analyse draait al voor dit incident", "incidentSeverityUpdatedTo": "Ernst van incident bijgewerkt naar {severity}", - "incidentStatusUpdatedTo": "Incidentstatus bijgewerkt naar {status}", "failedToLoadIncidentDetails": "Kan incidentdetails niet laden", "incidentsRefreshed": "Incidenten vernieuwd", "failedToPostComment": "Kan reactie niet plaatsen", diff --git a/web/src/locales/languages/pl-PL.json b/web/src/locales/languages/pl-PL.json index a40ec33609..e07e04511e 100644 --- a/web/src/locales/languages/pl-PL.json +++ b/web/src/locales/languages/pl-PL.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "To oznacza incydent jako będący w trakcie obsługi. W razie potrzeby możesz go później ponownie otworzyć.", "acknowledgeConfirmLabel": "Potwierdź", "acknowledgeConfirmCancelLabel": "Anuluj", + "resolveConfirmTitle": "Rozwiązać incydent?", + "resolveConfirmMessage": "Spowoduje to zamknięcie incydentu i usunięcie go z listy aktywnych. W razie potrzeby możesz otworzyć go ponownie później.", + "reopenConfirmTitle": "Ponownie otworzyć incydent?", + "reopenConfirmMessage": "Spowoduje to przywrócenie incydentu do stanu otwartego i wyczyszczenie jego rozwiązania i potwierdzenia.", "acknowledgeAriaLabel": "Potwierdź incydent", "acknowledgedBy": "Potwierdzone przez", "resolve": "Rozwiąż", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "Pomyślnie zaimportowano {count} szablon | Pomyślnie zaimportowano szablony: {count}", "aiAnalysisIsAlreadyRunningFor": "Analiza AI już trwa dla tego incydentu", "incidentSeverityUpdatedTo": "Poziom ważności incydentu zaktualizowano na {severity}", - "incidentStatusUpdatedTo": "Status incydentu zaktualizowano na {status}", "failedToLoadIncidentDetails": "Nie udało się wczytać szczegółów incydentu", "incidentsRefreshed": "Odświeżono incydenty", "failedToPostComment": "Nie udało się dodać komentarza", diff --git a/web/src/locales/languages/pt-PT.json b/web/src/locales/languages/pt-PT.json index e35892e138..4f9c0ed2ba 100644 --- a/web/src/locales/languages/pt-PT.json +++ b/web/src/locales/languages/pt-PT.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Isto marca o incidente como em tratamento. Pode reabri-lo depois, se necessário.", "acknowledgeConfirmLabel": "Reconhecer", "acknowledgeConfirmCancelLabel": "Cancelar", + "resolveConfirmTitle": "Resolver incidente?", + "resolveConfirmMessage": "Isto fecha o incidente e remove-o da lista ativa. Pode reabri-lo mais tarde, se necessário.", + "reopenConfirmTitle": "Reabrir incidente?", + "reopenConfirmMessage": "Isto volta a colocar o incidente como aberto e limpa a resolução e o reconhecimento.", "acknowledgeAriaLabel": "Reconhecer incidente", "acknowledgedBy": "Reconhecido por", "resolve": "Resolver", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "Importado com sucesso {count} modelo | Importados com sucesso {count} modelos", "aiAnalysisIsAlreadyRunningFor": "A análise de IA já está em curso para este incidente", "incidentSeverityUpdatedTo": "Severidade do incidente atualizada para {severity}", - "incidentStatusUpdatedTo": "Estado do incidente atualizado para {status}", "failedToLoadIncidentDetails": "Falha ao carregar detalhes do incidente", "incidentsRefreshed": "Incidentes atualizados", "failedToPostComment": "Falha ao publicar comentário", diff --git a/web/src/locales/languages/ru-RU.json b/web/src/locales/languages/ru-RU.json index 9856e6f6e5..c989df0dd2 100644 --- a/web/src/locales/languages/ru-RU.json +++ b/web/src/locales/languages/ru-RU.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Инцидент будет помечен как взятый в работу. При необходимости его можно будет переоткрыть позже.", "acknowledgeConfirmLabel": "Подтвердить", "acknowledgeConfirmCancelLabel": "Отмена", + "resolveConfirmTitle": "Разрешить инцидент?", + "resolveConfirmMessage": "Инцидент будет закрыт и удалён из списка активных. При необходимости его можно открыть повторно позже.", + "reopenConfirmTitle": "Открыть инцидент повторно?", + "reopenConfirmMessage": "Инцидент вернётся в состояние «открыт», а его разрешение и подтверждение будут сброшены.", "acknowledgeAriaLabel": "Подтвердить инцидент", "acknowledgedBy": "Кем подтверждено", "resolve": "Разрешить", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "Успешно импортирован {count} шаблон | Успешно импортировано шаблонов: {count}", "aiAnalysisIsAlreadyRunningFor": "Анализ ИИ уже выполняется для этого инцидента", "incidentSeverityUpdatedTo": "Критичность инцидента обновлена до {severity}", - "incidentStatusUpdatedTo": "Статус инцидента обновлен до {status}", "failedToLoadIncidentDetails": "Не удалось загрузить сведения об инциденте", "incidentsRefreshed": "Инциденты обновлены", "failedToPostComment": "Не удалось опубликовать комментарий", diff --git a/web/src/locales/languages/tr-TR.json b/web/src/locales/languages/tr-TR.json index 0f06e8637a..24ce1ae718 100644 --- a/web/src/locales/languages/tr-TR.json +++ b/web/src/locales/languages/tr-TR.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Bu, vakanın üzerinde çalışıldığını gösterir. Gerekirse daha sonra yeniden açabilirsiniz.", "acknowledgeConfirmLabel": "Onayla", "acknowledgeConfirmCancelLabel": "İptal", + "resolveConfirmTitle": "Vaka çözülsün mü?", + "resolveConfirmMessage": "Bu işlem vakayı kapatır ve etkin listeden kaldırır. Gerekirse daha sonra yeniden açabilirsiniz.", + "reopenConfirmTitle": "Vaka yeniden açılsın mı?", + "reopenConfirmMessage": "Bu işlem vakayı yeniden açık durumuna getirir ve çözüm ile onay bilgilerini temizler.", "acknowledgeAriaLabel": "Vakayı onayla", "acknowledgedBy": "Onaylayan", "resolve": "Çöz", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "{count} şablon başarıyla içe aktarıldı | {count} şablon başarıyla içe aktarıldı", "aiAnalysisIsAlreadyRunningFor": "Bu vaka için AI analizi zaten çalışıyor", "incidentSeverityUpdatedTo": "Vaka önem derecesi {severity} olarak güncellendi", - "incidentStatusUpdatedTo": "Vaka durumu {status} olarak güncellendi", "failedToLoadIncidentDetails": "Vaka ayrıntıları yüklenemedi", "incidentsRefreshed": "Vakalar yenilendi", "failedToPostComment": "Yorum gönderilemedi", diff --git a/web/src/locales/languages/vi-VN.json b/web/src/locales/languages/vi-VN.json index 35c8492538..606f12752c 100644 --- a/web/src/locales/languages/vi-VN.json +++ b/web/src/locales/languages/vi-VN.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "Thao tác này đánh dấu sự cố là đang được xử lý. Bạn có thể mở lại sau nếu cần.", "acknowledgeConfirmLabel": "Xác nhận", "acknowledgeConfirmCancelLabel": "Hủy", + "resolveConfirmTitle": "Giải quyết sự cố?", + "resolveConfirmMessage": "Thao tác này đóng sự cố và xóa nó khỏi danh sách đang hoạt động. Bạn có thể mở lại sau nếu cần.", + "reopenConfirmTitle": "Mở lại sự cố?", + "reopenConfirmMessage": "Thao tác này đưa sự cố về trạng thái mở và xóa thông tin giải quyết và xác nhận.", "acknowledgeAriaLabel": "Xác nhận sự cố", "acknowledgedBy": "Được xác nhận bởi", "resolve": "Giải quyết", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "Đã nhập thành công {count} mẫu | Đã nhập thành công {count} mẫu", "aiAnalysisIsAlreadyRunningFor": "Đã có phân tích AI đang chạy cho sự cố này", "incidentSeverityUpdatedTo": "Mức độ nghiêm trọng của sự cố đã được cập nhật thành {severity}", - "incidentStatusUpdatedTo": "Trạng thái sự cố đã được cập nhật thành {status}", "failedToLoadIncidentDetails": "Không thể tải chi tiết sự cố", "incidentsRefreshed": "Đã làm mới sự cố", "failedToPostComment": "Không thể đăng bình luận", diff --git a/web/src/locales/languages/zh-CN.json b/web/src/locales/languages/zh-CN.json index 422b937665..a500ca6b8b 100644 --- a/web/src/locales/languages/zh-CN.json +++ b/web/src/locales/languages/zh-CN.json @@ -10270,6 +10270,10 @@ "acknowledgeConfirmMessage": "此操作会将事件标记为正在处理。如有需要,之后可重新打开。", "acknowledgeConfirmLabel": "确认", "acknowledgeConfirmCancelLabel": "取消", + "resolveConfirmTitle": "解决事件?", + "resolveConfirmMessage": "这将关闭事件并将其从活动列表中移除。如有需要,您可以稍后重新打开。", + "reopenConfirmTitle": "重新打开事件?", + "reopenConfirmMessage": "这会将事件恢复为打开状态,并清除其解决和确认信息。", "acknowledgeAriaLabel": "确认事件", "acknowledgedBy": "确认人", "resolve": "解决", @@ -20997,7 +21001,6 @@ "successfullyImportedTemplates": "成功导入 {count} 个模板 | 成功导入 {count} 个模板", "aiAnalysisIsAlreadyRunningFor": "此事件已在进行 AI 分析", "incidentSeverityUpdatedTo": "事件严重级别已更新为 {severity}", - "incidentStatusUpdatedTo": "事件状态已更新为 {status}", "failedToLoadIncidentDetails": "加载事件详情失败", "incidentsRefreshed": "事件已刷新", "failedToPostComment": "发布评论失败", diff --git a/web/src/locales/languages/zh-TW.json b/web/src/locales/languages/zh-TW.json index e3fb9c98bc..df7b8bbad6 100644 --- a/web/src/locales/languages/zh-TW.json +++ b/web/src/locales/languages/zh-TW.json @@ -10265,6 +10265,10 @@ "acknowledgeConfirmMessage": "此操作會將事件標記為處理中。之後如有需要,可以重新開啟。", "acknowledgeConfirmLabel": "確認", "acknowledgeConfirmCancelLabel": "取消", + "resolveConfirmTitle": "解決事件?", + "resolveConfirmMessage": "這將關閉事件並將其從作用中清單移除。如有需要,您可以稍後重新開啟。", + "reopenConfirmTitle": "重新開啟事件?", + "reopenConfirmMessage": "這會將事件恢復為開啟狀態,並清除其解決與確認資訊。", "acknowledgeAriaLabel": "確認事件", "acknowledgedBy": "確認者", "resolve": "解決", @@ -20992,7 +20996,6 @@ "successfullyImportedTemplates": "已成功匯入 {count} 個範本 | 已成功匯入 {count} 個範本", "aiAnalysisIsAlreadyRunningFor": "此事件的 AI 分析已在執行中", "incidentSeverityUpdatedTo": "事件嚴重性已更新為 {severity}", - "incidentStatusUpdatedTo": "事件狀態已更新為 {status}", "failedToLoadIncidentDetails": "無法載入事件詳細資訊", "incidentsRefreshed": "事件已重新整理", "failedToPostComment": "張貼留言失敗",