mirror of
				https://github.com/abhinavxd/libredesk.git
				synced 2025-11-03 21:43:35 +00:00 
			
		
		
		
	feat: allow setting metric per SLA notification, so admins can set SLA alert per metric or just set to all if they want a notification to be sent for all metrics
- Make sla time fields (first response, next response, resolution) optional, only 1 field is required.
This commit is contained in:
		@@ -195,7 +195,7 @@ func main() {
 | 
			
		||||
	go conversation.Run(ctx, messageIncomingQWorkers, messageOutgoingQWorkers, messageOutgoingScanInterval)
 | 
			
		||||
	go conversation.RunUnsnoozer(ctx, unsnoozeInterval)
 | 
			
		||||
	go notifier.Run(ctx)
 | 
			
		||||
	go sla.Start(ctx, slaEvaluationInterval)
 | 
			
		||||
	go sla.Run(ctx, slaEvaluationInterval)
 | 
			
		||||
	go sla.SendNotifications(ctx)
 | 
			
		||||
	go media.DeleteUnlinkedMedia(ctx)
 | 
			
		||||
	go user.MonitorAgentAvailability(ctx)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										84
									
								
								cmd/sla.go
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								cmd/sla.go
									
									
									
									
									
								
							@@ -29,7 +29,7 @@ func handleGetSLA(r *fastglue.Request) error {
 | 
			
		||||
	)
 | 
			
		||||
	id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
 | 
			
		||||
	if err != nil || id == 0 {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "SLA `id`"), nil, envelope.InputError)
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sla, err := app.sla.Get(id)
 | 
			
		||||
@@ -70,7 +70,7 @@ func handleUpdateSLA(r *fastglue.Request) error {
 | 
			
		||||
 | 
			
		||||
	id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
 | 
			
		||||
	if err != nil || id == 0 {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "SLA `id`"), nil, envelope.InputError)
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := r.Decode(&sla, "json"); err != nil {
 | 
			
		||||
@@ -95,7 +95,7 @@ func handleDeleteSLA(r *fastglue.Request) error {
 | 
			
		||||
	)
 | 
			
		||||
	id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
 | 
			
		||||
	if err != nil || id == 0 {
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "SLA `id`"), nil, envelope.InputError)
 | 
			
		||||
		return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = app.sla.Delete(id); err != nil {
 | 
			
		||||
@@ -108,59 +108,71 @@ func handleDeleteSLA(r *fastglue.Request) error {
 | 
			
		||||
// validateSLA validates the SLA policy and returns an envelope.Error if any validation fails.
 | 
			
		||||
func validateSLA(app *App, sla *smodels.SLAPolicy) error {
 | 
			
		||||
	if sla.Name == "" {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "SLA `name`"), nil)
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil)
 | 
			
		||||
	}
 | 
			
		||||
	if sla.FirstResponseTime == "" {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "SLA `first_response_time`"), nil)
 | 
			
		||||
	}
 | 
			
		||||
	if sla.ResolutionTime == "" {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "SLA `resolution_time`"), nil)
 | 
			
		||||
	if sla.FirstResponseTime == "" && sla.NextResponseTime == "" && sla.ResolutionTime == "" {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "At least one of `first_response_time`, `next_response_time`, or `resolution_time` must be provided."), nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Validate notifications if any
 | 
			
		||||
	// Validate notifications if any.
 | 
			
		||||
	for _, n := range sla.Notifications {
 | 
			
		||||
		if n.Type == "" {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "SLA notification `type`"), nil)
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`type`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if n.TimeDelayType == "" {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "SLA notification `time_delay_type`"), nil)
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`time_delay_type`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if n.Metric == "" {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`metric`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if n.TimeDelayType != "immediately" {
 | 
			
		||||
			if n.TimeDelay == "" {
 | 
			
		||||
				return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "SLA notification `time_delay`"), nil)
 | 
			
		||||
				return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`time_delay`"), nil)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if len(n.Recipients) == 0 {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "SLA notification `recipients`"), nil)
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`recipients`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Validate time duration strings
 | 
			
		||||
	frt, err := time.ParseDuration(sla.FirstResponseTime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`first_response_time`"), nil)
 | 
			
		||||
	}
 | 
			
		||||
	if frt.Minutes() < 1 {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`first_response_time`"), nil)
 | 
			
		||||
	// Validate first response time duration string if not empty.
 | 
			
		||||
	if sla.FirstResponseTime != "" {
 | 
			
		||||
		frt, err := time.ParseDuration(sla.FirstResponseTime)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`first_response_time`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if frt.Minutes() < 1 {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`first_response_time`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rt, err := time.ParseDuration(sla.ResolutionTime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`resolution_time`"), nil)
 | 
			
		||||
	}
 | 
			
		||||
	if rt.Minutes() < 1 {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`resolution_time`"), nil)
 | 
			
		||||
	}
 | 
			
		||||
	if frt > rt {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.T("sla.firstResponseTimeAfterResolution"), nil)
 | 
			
		||||
	// Validate resolution time duration string if not empty.
 | 
			
		||||
	if sla.ResolutionTime != "" {
 | 
			
		||||
		rt, err := time.ParseDuration(sla.ResolutionTime)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`resolution_time`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if rt.Minutes() < 1 {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`resolution_time`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
		// Compare with first response time if both are present.
 | 
			
		||||
		if sla.FirstResponseTime != "" {
 | 
			
		||||
			frt, _ := time.ParseDuration(sla.FirstResponseTime)
 | 
			
		||||
			if frt > rt {
 | 
			
		||||
				return envelope.NewError(envelope.InputError, app.i18n.T("sla.firstResponseTimeAfterResolution"), nil)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nrt, err := time.ParseDuration(sla.NextResponseTime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`next_response_time`"), nil)
 | 
			
		||||
	}
 | 
			
		||||
	if nrt.Seconds() < 1 {
 | 
			
		||||
		return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`next_response_time`"), nil)
 | 
			
		||||
	// Validate next response time duration string if not empty.
 | 
			
		||||
	if sla.NextResponseTime != "" {
 | 
			
		||||
		nrt, err := time.ParseDuration(sla.NextResponseTime)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`next_response_time`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if nrt.Seconds() < 1 {
 | 
			
		||||
			return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`next_response_time`"), nil)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -109,16 +109,16 @@
 | 
			
		||||
                  {{
 | 
			
		||||
                    notification.type === 'warning' ? t('admin.sla.warning') : t('admin.sla.breach')
 | 
			
		||||
                  }}
 | 
			
		||||
                  {{ t('admin.sla.notification') }}
 | 
			
		||||
                  {{ t('globals.terms.alert').toLowerCase() }}
 | 
			
		||||
                </div>
 | 
			
		||||
                <p class="text-xs text-muted-foreground">
 | 
			
		||||
                  {{ notification.type === 'warning' ? 'Pre-breach alert' : 'Post-breach action' }}
 | 
			
		||||
                  {{ notification.type === 'warning' ? 'Pre-breach alert' : 'Post-breach alert' }}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="ghost"
 | 
			
		||||
              size="sm"
 | 
			
		||||
              size="xs"
 | 
			
		||||
              @click.prevent="removeNotification(index)"
 | 
			
		||||
              class="opacity-70 hover:opacity-100 text-muted-foreground hover:text-foreground"
 | 
			
		||||
            >
 | 
			
		||||
@@ -142,16 +142,16 @@
 | 
			
		||||
                      {{ t('admin.sla.triggerTiming') }}
 | 
			
		||||
                    </FormLabel>
 | 
			
		||||
                    <FormControl>
 | 
			
		||||
                      <Select v-bind="componentField" class="hover:border-foreground/30">
 | 
			
		||||
                      <Select v-bind="componentField">
 | 
			
		||||
                        <SelectTrigger class="w-full">
 | 
			
		||||
                          <SelectValue />
 | 
			
		||||
                        </SelectTrigger>
 | 
			
		||||
                        <SelectContent>
 | 
			
		||||
                          <SelectGroup>
 | 
			
		||||
                            <SelectItem value="immediately" class="focus:bg-accent">
 | 
			
		||||
                            <SelectItem value="immediately">
 | 
			
		||||
                              {{ t('admin.sla.immediatelyOnBreach') }}
 | 
			
		||||
                            </SelectItem>
 | 
			
		||||
                            <SelectItem value="after" class="focus:bg-accent">
 | 
			
		||||
                            <SelectItem value="after">
 | 
			
		||||
                              {{ t('admin.sla.afterSpecificDuration') }}
 | 
			
		||||
                            </SelectItem>
 | 
			
		||||
                          </SelectGroup>
 | 
			
		||||
@@ -172,7 +172,7 @@
 | 
			
		||||
                      }}
 | 
			
		||||
                    </FormLabel>
 | 
			
		||||
                    <FormControl>
 | 
			
		||||
                      <Select v-bind="componentField" class="hover:border-foreground/30">
 | 
			
		||||
                      <Select v-bind="componentField">
 | 
			
		||||
                        <SelectTrigger class="w-full">
 | 
			
		||||
                          <SelectValue :placeholder="t('admin.sla.selectDuration')" />
 | 
			
		||||
                        </SelectTrigger>
 | 
			
		||||
@@ -182,7 +182,6 @@
 | 
			
		||||
                              v-for="duration in delayDurations"
 | 
			
		||||
                              :key="duration"
 | 
			
		||||
                              :value="duration"
 | 
			
		||||
                              class="focus:bg-accent"
 | 
			
		||||
                            >
 | 
			
		||||
                              {{ duration }}
 | 
			
		||||
                            </SelectItem>
 | 
			
		||||
@@ -205,7 +204,7 @@
 | 
			
		||||
                <FormItem>
 | 
			
		||||
                  <FormLabel class="flex items-center gap-1.5 text-sm font-medium">
 | 
			
		||||
                    <Users class="w-4 h-4 text-muted-foreground" />
 | 
			
		||||
                    {{ t('admin.sla.notificationRecipients') }}
 | 
			
		||||
                    {{ t('admin.sla.alertRecipients') }}
 | 
			
		||||
                  </FormLabel>
 | 
			
		||||
                  <FormControl>
 | 
			
		||||
                    <SelectTag
 | 
			
		||||
@@ -225,6 +224,39 @@
 | 
			
		||||
                </FormItem>
 | 
			
		||||
              </FormField>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <FormField :name="`notifications.${index}.metric`" v-slot="{ componentField }">
 | 
			
		||||
              <FormItem>
 | 
			
		||||
                <FormLabel class="flex items-center gap-1.5 text-sm font-medium">
 | 
			
		||||
                  <SlidersHorizontal class="w-4 h-4 text-muted-foreground" />
 | 
			
		||||
                  {{ t('admin.sla.metric') }}
 | 
			
		||||
                </FormLabel>
 | 
			
		||||
                <FormControl>
 | 
			
		||||
                  <Select v-bind="componentField">
 | 
			
		||||
                    <SelectTrigger class="w-full">
 | 
			
		||||
                      <SelectValue :placeholder="t('admin.sla.selectMetric')" />
 | 
			
		||||
                    </SelectTrigger>
 | 
			
		||||
                    <SelectContent>
 | 
			
		||||
                      <SelectGroup>
 | 
			
		||||
                        <SelectItem value="all">
 | 
			
		||||
                          {{ t('admin.sla.metrics.all') }}
 | 
			
		||||
                        </SelectItem>
 | 
			
		||||
                        <SelectItem value="first_response">
 | 
			
		||||
                          {{ t('admin.sla.firstResponseTime') }}
 | 
			
		||||
                        </SelectItem>
 | 
			
		||||
                        <SelectItem value="next_response">
 | 
			
		||||
                          {{ t('admin.sla.nextResponseTime') }}
 | 
			
		||||
                        </SelectItem>
 | 
			
		||||
                        <SelectItem value="resolution">
 | 
			
		||||
                          {{ t('admin.sla.resolutionTime') }}
 | 
			
		||||
                        </SelectItem>
 | 
			
		||||
                      </SelectGroup>
 | 
			
		||||
                    </SelectContent>
 | 
			
		||||
                  </Select>
 | 
			
		||||
                </FormControl>
 | 
			
		||||
                <FormMessage />
 | 
			
		||||
              </FormItem>
 | 
			
		||||
            </FormField>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -235,7 +267,7 @@
 | 
			
		||||
        class="flex flex-col items-center justify-center p-8 space-y-3 rounded-xl bg-muted/30 border border-dashed"
 | 
			
		||||
      >
 | 
			
		||||
        <Bell class="w-8 h-8 text-muted-foreground" />
 | 
			
		||||
        <p class="text-sm text-muted-foreground">{{ t('admin.sla.noNotificationsConfigured') }}</p>
 | 
			
		||||
        <p class="text-sm text-muted-foreground">{{ t('admin.sla.noAlertsConfigured') }}</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@@ -251,7 +283,17 @@ import { useForm } from 'vee-validate'
 | 
			
		||||
import { toTypedSchema } from '@vee-validate/zod'
 | 
			
		||||
import { createFormSchema } from './formSchema'
 | 
			
		||||
import { Button } from '@/components/ui/button'
 | 
			
		||||
import { X, Plus, Timer, CircleAlert, Users, Clock, Hourglass, Bell } from 'lucide-vue-next'
 | 
			
		||||
import {
 | 
			
		||||
  X,
 | 
			
		||||
  Plus,
 | 
			
		||||
  Timer,
 | 
			
		||||
  CircleAlert,
 | 
			
		||||
  Users,
 | 
			
		||||
  Clock,
 | 
			
		||||
  Hourglass,
 | 
			
		||||
  Bell,
 | 
			
		||||
  SlidersHorizontal
 | 
			
		||||
} from 'lucide-vue-next'
 | 
			
		||||
import { useUsersStore } from '@/stores/users'
 | 
			
		||||
import {
 | 
			
		||||
  FormControl,
 | 
			
		||||
@@ -343,7 +385,8 @@ const addNotification = (type) => {
 | 
			
		||||
    type: type,
 | 
			
		||||
    time_delay_type: type === 'warning' ? 'before' : 'immediately',
 | 
			
		||||
    time_delay: type === 'warning' ? '10m' : '',
 | 
			
		||||
    recipients: []
 | 
			
		||||
    recipients: [],
 | 
			
		||||
    metric: 'all'
 | 
			
		||||
  })
 | 
			
		||||
  form.setFieldValue('notifications', notifications)
 | 
			
		||||
}
 | 
			
		||||
@@ -364,6 +407,8 @@ watch(
 | 
			
		||||
 | 
			
		||||
    const transformedNotifications = (newValues.notifications || []).map((notification) => ({
 | 
			
		||||
      ...notification,
 | 
			
		||||
      // Default value, notification applies to all metrics unless specified.
 | 
			
		||||
      metric: notification.metric || 'all',
 | 
			
		||||
      time_delay_type:
 | 
			
		||||
        notification.type === 'warning'
 | 
			
		||||
          ? 'before'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +1,79 @@
 | 
			
		||||
import * as z from 'zod'
 | 
			
		||||
import { isGoHourMinuteDuration } from '@/utils/strings'
 | 
			
		||||
 | 
			
		||||
export const createFormSchema = (t) => z.object({
 | 
			
		||||
    name: z
 | 
			
		||||
        .string()
 | 
			
		||||
        .min(1, { message: t('admin.sla.name.valid') })
 | 
			
		||||
        .max(255, { message: t('admin.sla.name.valid') }),
 | 
			
		||||
    description: z
 | 
			
		||||
        .string()
 | 
			
		||||
        .min(1, { message: t('admin.sla.description.valid') })
 | 
			
		||||
        .max(255, { message: t('admin.sla.description.valid') }),
 | 
			
		||||
    first_response_time: z.string().refine(isGoHourMinuteDuration, {
 | 
			
		||||
        message:
 | 
			
		||||
            t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
    }),
 | 
			
		||||
    resolution_time: z.string().refine(isGoHourMinuteDuration, {
 | 
			
		||||
        message:
 | 
			
		||||
            t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
    }),
 | 
			
		||||
    next_response_time: z.string().refine(isGoHourMinuteDuration, {
 | 
			
		||||
        message:
 | 
			
		||||
            t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
    }),
 | 
			
		||||
    notifications: z
 | 
			
		||||
        .array(
 | 
			
		||||
            z
 | 
			
		||||
                .object({
 | 
			
		||||
                    type: z.enum(['breach', 'warning']),
 | 
			
		||||
                    time_delay_type: z.enum(['immediately', 'after', 'before']),
 | 
			
		||||
                    time_delay: z.string().optional(),
 | 
			
		||||
                    recipients: z
 | 
			
		||||
                        .array(z.string())
 | 
			
		||||
                        .min(1, { message: t('globals.messages.atleastOneRecipient') })
 | 
			
		||||
export const createFormSchema = (t) =>
 | 
			
		||||
    z
 | 
			
		||||
        .object({
 | 
			
		||||
            name: z
 | 
			
		||||
                .string()
 | 
			
		||||
                .min(1, { message: t('admin.sla.name.valid') })
 | 
			
		||||
                .max(255, { message: t('admin.sla.name.valid') }),
 | 
			
		||||
            description: z
 | 
			
		||||
                .string()
 | 
			
		||||
                .min(1, { message: t('admin.sla.description.valid') })
 | 
			
		||||
                .max(255, { message: t('admin.sla.description.valid') }),
 | 
			
		||||
            first_response_time: z.string().optional().refine(val => !val || isGoHourMinuteDuration(val), {
 | 
			
		||||
                message: t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
            }),
 | 
			
		||||
            resolution_time: z.string().optional().refine(val => !val || isGoHourMinuteDuration(val), {
 | 
			
		||||
                message: t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
            }),
 | 
			
		||||
            next_response_time: z.string().optional().refine(val => !val || isGoHourMinuteDuration(val), {
 | 
			
		||||
                message: t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
            }),
 | 
			
		||||
            notifications: z
 | 
			
		||||
                .array(
 | 
			
		||||
                    z
 | 
			
		||||
                        .object({
 | 
			
		||||
                            type: z.enum(['breach', 'warning']),
 | 
			
		||||
                            time_delay_type: z.enum(['immediately', 'after', 'before']),
 | 
			
		||||
                            time_delay: z.string().optional(),
 | 
			
		||||
                            metric: z.enum(['first_response', 'resolution', 'next_response', 'all']),
 | 
			
		||||
                            recipients: z
 | 
			
		||||
                                .array(z.string())
 | 
			
		||||
                                .min(1, { message: t('globals.messages.atleastOneRecipient') }),
 | 
			
		||||
                        })
 | 
			
		||||
                        .superRefine((obj, ctx) => {
 | 
			
		||||
                            if (obj.time_delay_type !== 'immediately') {
 | 
			
		||||
                                if (!obj.time_delay || obj.time_delay === '') {
 | 
			
		||||
                                    ctx.addIssue({
 | 
			
		||||
                                        code: z.ZodIssueCode.custom,
 | 
			
		||||
                                        message: t('admin.sla.delay.required'),
 | 
			
		||||
                                        path: ['time_delay'],
 | 
			
		||||
                                    });
 | 
			
		||||
                                } else if (!isGoHourMinuteDuration(obj.time_delay)) {
 | 
			
		||||
                                    ctx.addIssue({
 | 
			
		||||
                                        code: z.ZodIssueCode.custom,
 | 
			
		||||
                                        message: t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
                                        path: ['time_delay'],
 | 
			
		||||
                                    });
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                )
 | 
			
		||||
                .optional()
 | 
			
		||||
                .default([]),
 | 
			
		||||
        })
 | 
			
		||||
        .superRefine((data, ctx) => {
 | 
			
		||||
            const { first_response_time, resolution_time, next_response_time } = data
 | 
			
		||||
            const isEmpty = !first_response_time && !resolution_time && !next_response_time
 | 
			
		||||
 | 
			
		||||
            if (isEmpty) {
 | 
			
		||||
                const msg = t('admin.sla.atleastOneSLATimeRequired')
 | 
			
		||||
                ctx.addIssue({
 | 
			
		||||
                    code: z.ZodIssueCode.custom,
 | 
			
		||||
                    path: ['first_response_time'],
 | 
			
		||||
                    message: msg,
 | 
			
		||||
                })
 | 
			
		||||
                .superRefine((obj, ctx) => {
 | 
			
		||||
                    if (obj.time_delay_type !== 'immediately') {
 | 
			
		||||
                        if (!obj.time_delay || obj.time_delay === '') {
 | 
			
		||||
                            ctx.addIssue({
 | 
			
		||||
                                code: z.ZodIssueCode.custom,
 | 
			
		||||
                                message:
 | 
			
		||||
                                    t('admin.sla.delay.required'),
 | 
			
		||||
                                path: ['time_delay']
 | 
			
		||||
                            })
 | 
			
		||||
                        } else if (!isGoHourMinuteDuration(obj.time_delay)) {
 | 
			
		||||
                            ctx.addIssue({
 | 
			
		||||
                                code: z.ZodIssueCode.custom,
 | 
			
		||||
                                message:
 | 
			
		||||
                                    t('globals.messages.goHourMinuteDuration'),
 | 
			
		||||
                                path: ['time_delay']
 | 
			
		||||
                            })
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ctx.addIssue({
 | 
			
		||||
                    code: z.ZodIssueCode.custom,
 | 
			
		||||
                    path: ['resolution_time'],
 | 
			
		||||
                    message: msg,
 | 
			
		||||
                })
 | 
			
		||||
        )
 | 
			
		||||
        .optional()
 | 
			
		||||
        .default([])
 | 
			
		||||
})
 | 
			
		||||
                ctx.addIssue({
 | 
			
		||||
                    code: z.ZodIssueCode.custom,
 | 
			
		||||
                    path: ['next_response_time'],
 | 
			
		||||
                    message: msg,
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								i18n/en.json
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								i18n/en.json
									
									
									
									
									
								
							@@ -81,6 +81,7 @@
 | 
			
		||||
  "globals.terms.key": "Key | Keys",
 | 
			
		||||
  "globals.terms.note": "Note | Notes",
 | 
			
		||||
  "globals.terms.ipAddress": "IP Address | IP Addresses",
 | 
			
		||||
  "globals.terms.alert": "Alert | Alerts",
 | 
			
		||||
  "globals.messages.badRequest": "Bad request",
 | 
			
		||||
  "globals.messages.adjustFilters": "Try adjusting filters",
 | 
			
		||||
  "globals.messages.errorUpdating": "Error updating {name}",
 | 
			
		||||
@@ -381,27 +382,30 @@
 | 
			
		||||
  "admin.sla.name.valid": "SLA Policy name should be between 1 and 255 characters",
 | 
			
		||||
  "admin.sla.description.valid": "SLA Policy description should be between 1 and 255 characters",
 | 
			
		||||
  "admin.sla.delay.required": "Delay is required",
 | 
			
		||||
  "admin.sla.firstResponseTime": "First Response Time",
 | 
			
		||||
  "admin.sla.firstResponseTime": "First response time",
 | 
			
		||||
  "admin.sla.resolutionTime": "Resolution time",
 | 
			
		||||
  "admin.sla.nextResponseTime": "Next response time",
 | 
			
		||||
  "admin.sla.firstResponseTime.description": "Duration in hours or minutes. Example: 1h, 30m, 1h30m",
 | 
			
		||||
  "admin.sla.resolutionTime": "Resolution Time",
 | 
			
		||||
  "admin.sla.resolutionTime.description": "Duration in hours or minutes. Example: 1h, 30m, 1h30m",
 | 
			
		||||
  "admin.sla.nextResponseTime": "Next Response Time",
 | 
			
		||||
  "admin.sla.nextResponseTime.description": "Duration in hours or minutes. Example: 1h, 30m, 1h30m",
 | 
			
		||||
  "admin.sla.alertConfiguration": "Alert Configuration",
 | 
			
		||||
  "admin.sla.alertConfiguration.description": "Set up notification triggers and recipients",
 | 
			
		||||
  "admin.sla.addBreachAlert": "Add Breach Alert",
 | 
			
		||||
  "admin.sla.addWarningAlert": "Add Warning Alert",
 | 
			
		||||
  "admin.sla.alertConfiguration": "Alert configuration",
 | 
			
		||||
  "admin.sla.alertConfiguration.description": "Set up alert triggers and recipients",
 | 
			
		||||
  "admin.sla.addBreachAlert": "Add breach alert",
 | 
			
		||||
  "admin.sla.addWarningAlert": "Add warning alert",
 | 
			
		||||
  "admin.sla.warning": "Warning",
 | 
			
		||||
  "admin.sla.breach": "Breach",
 | 
			
		||||
  "admin.sla.notification": "Notification",
 | 
			
		||||
  "admin.sla.triggerTiming": "Trigger Timing",
 | 
			
		||||
  "admin.sla.triggerTiming": "Trigger timing",
 | 
			
		||||
  "admin.sla.immediatelyOnBreach": "Immediately on breach",
 | 
			
		||||
  "admin.sla.afterSpecificDuration": "After specific duration",
 | 
			
		||||
  "admin.sla.selectDuration": "Select duration...",
 | 
			
		||||
  "admin.sla.advanceWarning": "Advance Warning",
 | 
			
		||||
  "admin.sla.followUpDelay": "Follow Up Delay",
 | 
			
		||||
  "admin.sla.notificationRecipients": "Notification Recipients",
 | 
			
		||||
  "admin.sla.noNotificationsConfigured": "No notifications configured",
 | 
			
		||||
  "admin.sla.advanceWarning": "Advance warning",
 | 
			
		||||
  "admin.sla.followUpDelay": "Follow up delay",
 | 
			
		||||
  "admin.sla.alertRecipients": "Alert recipients",
 | 
			
		||||
  "admin.sla.noAlertsConfigured": "No alerts configured",
 | 
			
		||||
  "admin.sla.metric": "SLA Metric",
 | 
			
		||||
  "admin.sla.selectMetric": "Select SLA metric",
 | 
			
		||||
  "admin.sla.metrics.all": "All",
 | 
			
		||||
  "admin.sla.atleastOneSLATimeRequired": "At least one of First Response Time, Next Response Time, or Resolution Time must be provided.",
 | 
			
		||||
  "admin.conversationTags.edit.description": "Change the tag name. Click save when you're done.",
 | 
			
		||||
  "admin.conversationTags.new.description": "Set tag name. Click save when you're done.",
 | 
			
		||||
  "admin.conversationTags.updated": "Tag updated successfully",
 | 
			
		||||
 
 | 
			
		||||
@@ -361,15 +361,15 @@
 | 
			
		||||
    "admin.sla.addWarningAlert": "चेतावणी सूचना जोडा",
 | 
			
		||||
    "admin.sla.warning": "चेतावणी",
 | 
			
		||||
    "admin.sla.breach": "उल्लंघन",
 | 
			
		||||
    "admin.sla.notification": "सूचना",
 | 
			
		||||
    "globals.terms.alert": "सूचना",
 | 
			
		||||
    "admin.sla.triggerTiming": "ट्रिगर वेळ",
 | 
			
		||||
    "admin.sla.immediatelyOnBreach": "उल्लंघनावर लगेच",
 | 
			
		||||
    "admin.sla.afterSpecificDuration": "विशिष्ट कालावधीनंतर",
 | 
			
		||||
    "admin.sla.selectDuration": "कालावधी निवडा...",
 | 
			
		||||
    "admin.sla.advanceWarning": "अग्रिम चेतावणी",
 | 
			
		||||
    "admin.sla.followUpDelay": "फॉलो अप विलंब",
 | 
			
		||||
    "admin.sla.notificationRecipients": "सूचना प्राप्तकर्ते",
 | 
			
		||||
    "admin.sla.noNotificationsConfigured": "सूचना कॉन्फिगर केलेल्या नाहीत",
 | 
			
		||||
    "admin.sla.alertRecipients": "सूचना प्राप्तकर्ते",
 | 
			
		||||
    "admin.sla.noAlertsConfigured": "सूचना कॉन्फिगर केलेल्या नाहीत",
 | 
			
		||||
    "admin.conversationTags.edit.description": "टॅग नाव बदला. पूर्ण झाल्यावर जतन करा क्लिक करा.",
 | 
			
		||||
    "admin.conversationTags.new.description": "टॅग नाव सेट करा. पूर्ण झाल्यावर जतन करा क्लिक करा.",
 | 
			
		||||
    "admin.conversationTags.updated": "टॅग यशस्वीरित्या अद्ययावत केला",
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ type SlaNotification struct {
 | 
			
		||||
	Recipients    []string `db:"recipients" json:"recipients"`
 | 
			
		||||
	TimeDelay     string   `db:"time_delay" json:"time_delay"`
 | 
			
		||||
	TimeDelayType string   `db:"time_delay_type" json:"time_delay_type"`
 | 
			
		||||
	Metric        string   `db:"metric" json:"metric"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ScheduledSLANotification represents a scheduled SLA notification
 | 
			
		||||
 
 | 
			
		||||
@@ -195,8 +195,7 @@ RETURNING id;
 | 
			
		||||
 | 
			
		||||
-- name: set-latest-sla-event-met-at
 | 
			
		||||
UPDATE sla_events
 | 
			
		||||
SET met_at = NOW(),
 | 
			
		||||
status = CASE WHEN NOW() > deadline_at THEN 'breached'::sla_event_status ELSE 'met'::sla_event_status END
 | 
			
		||||
SET met_at = NOW()
 | 
			
		||||
WHERE id = (
 | 
			
		||||
  SELECT id FROM sla_events
 | 
			
		||||
  WHERE applied_sla_id = $1 AND type = $2 AND met_at IS NULL
 | 
			
		||||
@@ -208,7 +207,7 @@ RETURNING met_at;
 | 
			
		||||
-- name: mark-sla-event-as-breached
 | 
			
		||||
UPDATE sla_events
 | 
			
		||||
SET breached_at = NOW(),
 | 
			
		||||
status = 'breached'
 | 
			
		||||
    status = 'breached'
 | 
			
		||||
WHERE id = $1;
 | 
			
		||||
 | 
			
		||||
-- name: mark-sla-event-as-met
 | 
			
		||||
@@ -216,7 +215,7 @@ UPDATE sla_events
 | 
			
		||||
SET status = 'met'
 | 
			
		||||
WHERE id = $1;
 | 
			
		||||
 | 
			
		||||
-- name: get-sla-event
 | 
			
		||||
-- name: get-sla-event-by-id
 | 
			
		||||
SELECT id, created_at, updated_at, applied_sla_id, sla_policy_id, type, deadline_at, met_at, breached_at
 | 
			
		||||
FROM sla_events
 | 
			
		||||
WHERE id = $1;
 | 
			
		||||
@@ -224,4 +223,4 @@ WHERE id = $1;
 | 
			
		||||
-- name: get-pending-sla-events
 | 
			
		||||
SELECT id
 | 
			
		||||
FROM sla_events
 | 
			
		||||
WHERE status = 'pending' and deadline_at IS NOT NULL;
 | 
			
		||||
WHERE status = 'pending' AND deadline_at IS NOT NULL;
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ const (
 | 
			
		||||
	MetricFirstResponse = "first_response"
 | 
			
		||||
	MetricResolution    = "resolution"
 | 
			
		||||
	MetricNextResponse  = "next_response"
 | 
			
		||||
	MetricAll           = "all"
 | 
			
		||||
 | 
			
		||||
	NotificationTypeWarning = "warning"
 | 
			
		||||
	NotificationTypeBreach  = "breach"
 | 
			
		||||
@@ -103,11 +104,10 @@ type businessHrsStore interface {
 | 
			
		||||
 | 
			
		||||
// queries hold prepared SQL queries.
 | 
			
		||||
type queries struct {
 | 
			
		||||
	// TODO: name queries better.
 | 
			
		||||
	GetSLA                             *sqlx.Stmt `query:"get-sla-policy"`
 | 
			
		||||
	GetAllSLA                          *sqlx.Stmt `query:"get-all-sla-policies"`
 | 
			
		||||
	GetAppliedSLA                      *sqlx.Stmt `query:"get-applied-sla"`
 | 
			
		||||
	GetSLAEvent                        *sqlx.Stmt `query:"get-sla-event"`
 | 
			
		||||
	GetSLAEventByID                    *sqlx.Stmt `query:"get-sla-event-by-id"`
 | 
			
		||||
	GetScheduledSLANotifications       *sqlx.Stmt `query:"get-scheduled-sla-notifications"`
 | 
			
		||||
	GetLatestAppliedSLAForConversation *sqlx.Stmt `query:"get-latest-applied-sla-for-conversation"`
 | 
			
		||||
	InsertScheduledSLANotification     *sqlx.Stmt `query:"insert-scheduled-sla-notification"`
 | 
			
		||||
@@ -372,14 +372,18 @@ func (m *Manager) SetLatestSLAEventMetAt(conversationID int, metric string) (tim
 | 
			
		||||
	return metAt, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// evaluatePendingSLAEvents fetches pending SLA events and marks them as breached if the deadline has passed.
 | 
			
		||||
// evaluatePendingSLAEvents fetches pending SLA events, updates their status based on deadlines, and schedules notifications for breached SLAs.
 | 
			
		||||
func (m *Manager) evaluatePendingSLAEvents(ctx context.Context) error {
 | 
			
		||||
	var slaEvents []models.SLAEvent
 | 
			
		||||
	if err := m.q.GetPendingSLAEvents.SelectContext(ctx, &slaEvents); err != nil {
 | 
			
		||||
		m.lo.Error("error fetching pending SLA events", "error", err)
 | 
			
		||||
		return fmt.Errorf("fetching pending SLA events: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	m.lo.Info("found SLA events that have breached", "count", len(slaEvents))
 | 
			
		||||
	if len(slaEvents) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.lo.Info("found pending SLA events for evaluation", "count", len(slaEvents))
 | 
			
		||||
 | 
			
		||||
	// Cache for SLA policies.
 | 
			
		||||
	var slaPolicyCache = make(map[int]models.SLAPolicy)
 | 
			
		||||
@@ -390,13 +394,13 @@ func (m *Manager) evaluatePendingSLAEvents(ctx context.Context) error {
 | 
			
		||||
		default:
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := m.q.GetSLAEvent.GetContext(ctx, &event, event.ID); err != nil {
 | 
			
		||||
		if err := m.q.GetSLAEventByID.GetContext(ctx, &event, event.ID); err != nil {
 | 
			
		||||
			m.lo.Error("error fetching SLA event", "error", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if event.DeadlineAt.IsZero() {
 | 
			
		||||
			m.lo.Warn("SLA event deadline is zero, skipping marking as breached", "sla_event_id", event.ID)
 | 
			
		||||
			m.lo.Warn("SLA event deadline is zero, skipping evaluation", "sla_event_id", event.ID)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -418,9 +422,9 @@ func (m *Manager) evaluatePendingSLAEvents(ctx context.Context) error {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Schedule a breach notification if the event is not met at all.
 | 
			
		||||
		// Schedule a breach notification if the event is not met at all and SLA breached.
 | 
			
		||||
		if !event.MetAt.Valid && hasBreached {
 | 
			
		||||
			// Check if the SLA policy is already cached.
 | 
			
		||||
			// Get policy from cache.
 | 
			
		||||
			slaPolicy, ok := slaPolicyCache[event.SlaPolicyID]
 | 
			
		||||
			if !ok {
 | 
			
		||||
				var err error
 | 
			
		||||
@@ -439,8 +443,8 @@ func (m *Manager) evaluatePendingSLAEvents(ctx context.Context) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Start begins SLA and SLA event evaluation loops in separate goroutines.
 | 
			
		||||
func (m *Manager) Start(ctx context.Context, interval time.Duration) {
 | 
			
		||||
// Run starts Applied SLA and SLA event evaluation loops in separate goroutines.
 | 
			
		||||
func (m *Manager) Run(ctx context.Context, interval time.Duration) {
 | 
			
		||||
	m.wg.Add(2)
 | 
			
		||||
	go m.runSLAEvaluation(ctx, interval)
 | 
			
		||||
	go m.runSLAEventEvaluation(ctx, interval)
 | 
			
		||||
@@ -515,14 +519,14 @@ func (m *Manager) SendNotifications(ctx context.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendNotification sends a SLA notification to agents.
 | 
			
		||||
// SendNotification sends a SLA notification to agents, a schedule notification can be linked to a specific SLA event or applied SLA.
 | 
			
		||||
func (m *Manager) SendNotification(scheduledNotification models.ScheduledSLANotification) error {
 | 
			
		||||
	var (
 | 
			
		||||
		appliedSLA models.AppliedSLA
 | 
			
		||||
		slaEvent   models.SLAEvent
 | 
			
		||||
	)
 | 
			
		||||
	if scheduledNotification.SlaEventID.Int != 0 {
 | 
			
		||||
		if err := m.q.GetSLAEvent.Get(&slaEvent, scheduledNotification.SlaEventID.Int); err != nil {
 | 
			
		||||
		if err := m.q.GetSLAEventByID.Get(&slaEvent, scheduledNotification.SlaEventID.Int); err != nil {
 | 
			
		||||
			m.lo.Error("error fetching SLA event", "error", err)
 | 
			
		||||
			return fmt.Errorf("fetching SLA event for notification: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
@@ -760,14 +764,15 @@ func (m *Manager) getBusinessHoursAndTimezone(assignedTeamID int) (bmodels.Busin
 | 
			
		||||
	return bh, timezone, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// createNotificationSchedule creates a notification schedule in database for the applied SLA.
 | 
			
		||||
// createNotificationSchedule creates a notification schedule in database for the applied SLA to be sent later.
 | 
			
		||||
func (m *Manager) createNotificationSchedule(notifications models.SlaNotifications, appliedSLAID int, slaEventID null.Int, deadlines Deadlines, breaches Breaches) {
 | 
			
		||||
	scheduleNotification := func(sendAt time.Time, metric, notifType string, recipients []string) {
 | 
			
		||||
		// Make sure the sendAt time is in not too far in the past.
 | 
			
		||||
		if sendAt.Before(time.Now().Add(-5 * time.Minute)) {
 | 
			
		||||
			m.lo.Debug("skipping scheduling notification as it is in the past", "send_at", sendAt, "applied_sla_id", appliedSLAID, "metric", metric, "type", notifType)
 | 
			
		||||
			m.lo.Warn("skipping scheduling notification as it is in the past", "send_at", sendAt, "applied_sla_id", appliedSLAID, "metric", metric, "type", notifType)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		m.lo.Info("scheduling SLA notification", "send_at", sendAt, "applied_sla_id", appliedSLAID, "metric", metric, "type", notifType, "recipients", recipients)
 | 
			
		||||
		if _, err := m.q.InsertScheduledSLANotification.Exec(appliedSLAID, slaEventID, metric, notifType, pq.Array(recipients), sendAt); err != nil {
 | 
			
		||||
			m.lo.Error("error inserting scheduled SLA notification", "error", err)
 | 
			
		||||
		}
 | 
			
		||||
@@ -775,43 +780,42 @@ func (m *Manager) createNotificationSchedule(notifications models.SlaNotificatio
 | 
			
		||||
 | 
			
		||||
	// Insert scheduled entries for each notification.
 | 
			
		||||
	for _, notif := range notifications {
 | 
			
		||||
		var (
 | 
			
		||||
			delayDur time.Duration
 | 
			
		||||
			err      error
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		// No delay for immediate notifications.
 | 
			
		||||
		if notif.TimeDelayType == "immediately" {
 | 
			
		||||
			delayDur = 0
 | 
			
		||||
		} else {
 | 
			
		||||
			delayDur, err = time.ParseDuration(notif.TimeDelay)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
		delayDur := time.Duration(0)
 | 
			
		||||
		if notif.TimeDelayType != "immediately" && notif.TimeDelay != "" {
 | 
			
		||||
			if d, err := time.ParseDuration(notif.TimeDelay); err == nil {
 | 
			
		||||
				delayDur = d
 | 
			
		||||
			} else {
 | 
			
		||||
				m.lo.Error("error parsing sla notification delay", "error", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if notif.Type == NotificationTypeWarning {
 | 
			
		||||
			if !deadlines.FirstResponse.IsZero() {
 | 
			
		||||
				scheduleNotification(deadlines.FirstResponse.Add(-delayDur), MetricFirstResponse, notif.Type, notif.Recipients)
 | 
			
		||||
			}
 | 
			
		||||
			if !deadlines.Resolution.IsZero() {
 | 
			
		||||
				scheduleNotification(deadlines.Resolution.Add(-delayDur), MetricResolution, notif.Type, notif.Recipients)
 | 
			
		||||
			}
 | 
			
		||||
			if !deadlines.NextResponse.IsZero() {
 | 
			
		||||
				scheduleNotification(deadlines.NextResponse.Add(-delayDur), MetricNextResponse, notif.Type, notif.Recipients)
 | 
			
		||||
			}
 | 
			
		||||
		} else if notif.Type == NotificationTypeBreach {
 | 
			
		||||
			if !breaches.FirstResponse.IsZero() {
 | 
			
		||||
				scheduleNotification(breaches.FirstResponse.Add(delayDur), MetricFirstResponse, notif.Type, notif.Recipients)
 | 
			
		||||
			}
 | 
			
		||||
			if !breaches.Resolution.IsZero() {
 | 
			
		||||
				scheduleNotification(breaches.Resolution.Add(delayDur), MetricResolution, notif.Type, notif.Recipients)
 | 
			
		||||
			}
 | 
			
		||||
			if !breaches.NextResponse.IsZero() {
 | 
			
		||||
				scheduleNotification(breaches.NextResponse.Add(delayDur), MetricNextResponse, notif.Type, notif.Recipients)
 | 
			
		||||
		if notif.Metric == "" {
 | 
			
		||||
			notif.Metric = MetricAll
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		schedule := func(target time.Time, metricType string) {
 | 
			
		||||
			if !target.IsZero() && (notif.Metric == metricType || notif.Metric == MetricAll) {
 | 
			
		||||
				var sendAt time.Time
 | 
			
		||||
				if notif.Type == NotificationTypeWarning {
 | 
			
		||||
					sendAt = target.Add(-delayDur)
 | 
			
		||||
				} else {
 | 
			
		||||
					sendAt = target.Add(delayDur)
 | 
			
		||||
				}
 | 
			
		||||
				scheduleNotification(sendAt, metricType, notif.Type, notif.Recipients)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch notif.Type {
 | 
			
		||||
		case NotificationTypeWarning:
 | 
			
		||||
			schedule(deadlines.FirstResponse, MetricFirstResponse)
 | 
			
		||||
			schedule(deadlines.Resolution, MetricResolution)
 | 
			
		||||
			schedule(deadlines.NextResponse, MetricNextResponse)
 | 
			
		||||
		case NotificationTypeBreach:
 | 
			
		||||
			schedule(breaches.FirstResponse, MetricFirstResponse)
 | 
			
		||||
			schedule(breaches.Resolution, MetricResolution)
 | 
			
		||||
			schedule(breaches.NextResponse, MetricNextResponse)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user