Merge branch 'master' into mob-phase-1

This commit is contained in:
Nawaz Dhandala
2026-02-10 15:26:41 +00:00
35 changed files with 4031 additions and 1147 deletions

View File

@@ -32,6 +32,7 @@ import Reseller from "Common/Models/DatabaseModels/Reseller";
import User from "Common/Models/DatabaseModels/User";
import React, { useState } from "react";
import useAsyncEffect from "use-async-effect";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
const RegisterPage: () => JSX.Element = () => {
const apiUrl: URL = SIGNUP_API_URL;
@@ -172,6 +173,36 @@ const RegisterPage: () => JSX.Element = () => {
}
}
if (!BILLING_ENABLED) {
formFields = formFields.concat([
{
overrideField: {
selfHostedCompanyName: true,
},
overrideFieldKey: "selfHostedCompanyName",
fieldType: FormFieldSchemaType.Text,
placeholder: "Acme, Inc.",
required: false,
title: "Company Name",
dataTestId: "selfHostedCompanyName",
showEvenIfPermissionDoesNotExist: true,
disableSpellCheck: true,
},
{
overrideField: {
selfHostedPhoneNumber: true,
},
overrideFieldKey: "selfHostedPhoneNumber",
fieldType: FormFieldSchemaType.Phone,
required: false,
placeholder: "+11234567890",
title: "Phone Number",
dataTestId: "selfHostedPhoneNumber",
showEvenIfPermissionDoesNotExist: true,
},
]);
}
formFields = formFields.concat([
{
field: {
@@ -206,6 +237,25 @@ const RegisterPage: () => JSX.Element = () => {
},
]);
if (!IsBillingEnabled) {
formFields = formFields.concat([
{
overrideField: {
notifySelfHosted: true,
},
overrideFieldKey: "notifySelfHosted",
fieldType: FormFieldSchemaType.Checkbox,
required: false,
defaultValue: true,
title: "Notify me about security patches and new releases",
dataTestId: "notifySelfHosted",
showEvenIfPermissionDoesNotExist: true,
spanFullRow: true,
},
]);
}
if (isCaptchaEnabled) {
formFields = formFields.concat([
{

View File

@@ -23,6 +23,7 @@ import WhatsAppLogAPI from "./WhatsAppLogAPI";
// Import API
import ResellerPlanAPI from "Common/Server/API/ResellerPlanAPI";
import EnterpriseLicenseAPI from "Common/Server/API/EnterpriseLicenseAPI";
import OpenSourceDeploymentAPI from "Common/Server/API/OpenSourceDeploymentAPI";
import MonitorAPI from "Common/Server/API/MonitorAPI";
import ShortLinkAPI from "Common/Server/API/ShortLinkAPI";
import StatusPageAPI from "Common/Server/API/StatusPageAPI";
@@ -2007,6 +2008,10 @@ const BaseAPIFeatureSet: FeatureSet = {
`/${APP_NAME.toLocaleLowerCase()}`,
new EnterpriseLicenseAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new OpenSourceDeploymentAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,

View File

@@ -17,9 +17,11 @@ import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from "Common/Types/PositiveNumber";
import DatabaseConfig from "Common/Server/DatabaseConfig";
import {
AppVersion,
EncryptionSecret,
IsBillingEnabled,
} from "Common/Server/EnvironmentConfig";
import API from "Common/Utils/API";
import AccessTokenService from "Common/Server/Services/AccessTokenService";
import EmailVerificationTokenService from "Common/Server/Services/EmailVerificationTokenService";
import MailService from "Common/Server/Services/MailService";
@@ -271,6 +273,28 @@ router.post(
logger.info("User signed up: " + savedUser.email?.toString());
if (!IsBillingEnabled && miscDataProps["notifySelfHosted"] === true) {
const instanceUrl: string = new URL(httpProtocol, host).toString();
API.post({
url: URL.fromString(
"https://oneuptime.com/api/open-source-deployment/register",
),
data: {
email: savedUser.email?.toString() || "",
name: savedUser.name?.toString() || "",
companyName:
(miscDataProps["selfHostedCompanyName"] as string) || undefined,
companyPhoneNumber:
(miscDataProps["selfHostedPhoneNumber"] as string) || undefined,
oneuptimeVersion: AppVersion,
instanceUrl: instanceUrl,
},
}).catch((err: Error) => {
logger.error(err);
});
}
return Response.sendEntityResponse(req, res, savedUser, User);
}

16
App/package-lock.json generated
View File

@@ -1609,13 +1609,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -2388,9 +2388,9 @@
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",

View File

@@ -543,6 +543,31 @@ export default class AlertEpisode extends BaseModel {
})
public resolvedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.Date,
title: "All Alerts Resolved At",
description:
"When all alerts in this episode were first detected as resolved. Used for resolve delay calculation.",
})
@Column({
type: ColumnType.Date,
nullable: true,
unique: false,
})
public allAlertsResolvedAt?: Date = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -542,6 +542,31 @@ export default class IncidentEpisode extends BaseModel {
})
public resolvedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.Date,
title: "All Incidents Resolved At",
description:
"When all incidents in this episode were first detected as resolved. Used for resolve delay calculation.",
})
@Column({
type: ColumnType.Date,
nullable: true,
unique: false,
})
public allIncidentsResolvedAt?: Date = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -98,6 +98,7 @@ import ProjectSmtpConfig from "./ProjectSmtpConfig";
import ProjectSSO from "./ProjectSso";
import PromoCode from "./PromoCode";
import EnterpriseLicense from "./EnterpriseLicense";
import OpenSourceDeployment from "./OpenSourceDeployment";
import Reseller from "./Reseller";
import ResellerPlan from "./ResellerPlan";
// ScheduledMaintenances
@@ -411,6 +412,7 @@ const AllModelTypes: Array<{
PromoCode,
EnterpriseLicense,
OpenSourceDeployment,
GlobalConfig,

View File

@@ -0,0 +1,140 @@
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import IconProp from "../../Types/Icon/IconProp";
import { Column, Entity } from "typeorm";
@TableAccessControl({
create: [],
read: [],
update: [],
delete: [],
})
@CrudApiEndpoint(new Route("/open-source-deployment"))
@TableMetadata({
tableName: "OpenSourceDeployment",
singularName: "Open Source Deployment",
pluralName: "Open Source Deployments",
icon: IconProp.Globe,
tableDescription:
"Open source deployment registrations from self-hosted instances.",
})
@Entity({
name: "OpenSourceDeployment",
})
export default class OpenSourceDeployment extends BaseModel {
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.Email,
title: "Email",
description: "Email address of the user who registered.",
})
@Column({
nullable: false,
type: ColumnType.Email,
length: ColumnLength.Email,
})
public email?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Name",
description: "Full name of the user who registered.",
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public name?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.ShortText,
title: "Company Name",
description: "Company name of the user who registered.",
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public companyName?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.Phone,
title: "Company Phone Number",
description: "Phone number of the user who registered.",
})
@Column({
nullable: true,
type: ColumnType.Phone,
length: ColumnLength.Phone,
})
public companyPhoneNumber?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "Version",
description: "OneUptime version of the self-hosted instance.",
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public oneuptimeVersion?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.ShortText,
title: "Instance URL",
description: "URL of the self-hosted instance.",
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public instanceUrl?: string = undefined;
}

View File

@@ -0,0 +1,73 @@
import OpenSourceDeployment from "../../Models/DatabaseModels/OpenSourceDeployment";
import { JSONObject } from "../../Types/JSON";
import URL from "../../Types/API/URL";
import API from "../../Utils/API";
import OpenSourceDeploymentService, {
Service as OpenSourceDeploymentServiceType,
} from "../Services/OpenSourceDeploymentService";
import { OpenSourceDeploymentWebhookUrl } from "../EnvironmentConfig";
import logger from "../Utils/Logger";
import Response from "../Utils/Response";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import BaseAPI from "./BaseAPI";
export default class OpenSourceDeploymentAPI extends BaseAPI<
OpenSourceDeployment,
OpenSourceDeploymentServiceType
> {
public constructor() {
super(OpenSourceDeployment, OpenSourceDeploymentService);
this.router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/register`,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body;
const deployment: OpenSourceDeployment = new OpenSourceDeployment();
deployment.email = (body["email"] as string) || "";
deployment.name = (body["name"] as string) || "";
deployment.companyName = (body["companyName"] as string) || "";
deployment.companyPhoneNumber =
(body["companyPhoneNumber"] as string) || "";
deployment.oneuptimeVersion =
(body["oneuptimeVersion"] as string) || "unknown";
deployment.instanceUrl = (body["instanceUrl"] as string) || "";
await OpenSourceDeploymentService.create({
data: deployment,
props: {
isRoot: true,
},
});
if (OpenSourceDeploymentWebhookUrl) {
API.post({
url: URL.fromString(OpenSourceDeploymentWebhookUrl),
data: {
email: deployment.email?.toString() || "",
name: deployment.name?.toString() || "",
companyName: deployment.companyName?.toString() || "",
companyPhoneNumber:
deployment.companyPhoneNumber?.toString() || "",
oneuptimeVersion: deployment.oneuptimeVersion?.toString() || "",
instanceUrl: deployment.instanceUrl?.toString() || "",
},
}).catch((err: Error) => {
logger.error(err);
});
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
next(err);
}
},
);
}
}

View File

@@ -148,6 +148,9 @@ export const EncryptionSecret: ObjectID = new ObjectID(
process.env["ENCRYPTION_SECRET"] || "secret",
);
export const OpenSourceDeploymentWebhookUrl: string =
process.env["OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL"] || "";
export const AirtableApiKey: string = process.env["AIRTABLE_API_KEY"] || "";
export const AirtableBaseId: string = process.env["AIRTABLE_BASE_ID"] || "";

View File

@@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1770728946893 implements MigrationInterface {
public name = "MigrationName1770728946893";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "IncidentEpisode" ADD "allIncidentsResolvedAt" TIMESTAMP WITH TIME ZONE`,
);
await queryRunner.query(
`ALTER TABLE "AlertEpisode" ADD "allAlertsResolvedAt" TIMESTAMP WITH TIME ZONE`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
await queryRunner.query(
`CREATE INDEX "IDX_0610406e5c436c20a5068b1006" ON "IncidentEpisode" ("allIncidentsResolvedAt") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_ea5d1f899fe52445dd6e0d0d55" ON "AlertEpisode" ("allAlertsResolvedAt") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_ea5d1f899fe52445dd6e0d0d55"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_0610406e5c436c20a5068b1006"`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "AlertEpisode" DROP COLUMN "allAlertsResolvedAt"`,
);
await queryRunner.query(
`ALTER TABLE "IncidentEpisode" DROP COLUMN "allIncidentsResolvedAt"`,
);
}
}

View File

@@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1770732721195 implements MigrationInterface {
public name = "MigrationName1770732721195";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "OpenSourceDeployment" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "email" character varying(100) NOT NULL, "name" character varying(100) NOT NULL, "companyName" character varying(100), "companyPhoneNumber" character varying(30), "oneuptimeVersion" character varying(100) NOT NULL, "instanceUrl" character varying(100), CONSTRAINT "PK_cf6728c16db1c0c1b89f8ffc6dd" PRIMARY KEY ("_id"))`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(`DROP TABLE "OpenSourceDeployment"`);
}
}

View File

@@ -254,6 +254,8 @@ import { MigrationName1770237245069 } from "./1770237245069-MigrationName";
import { MigrationName1770237245070 } from "./1770237245070-MigrationName";
import { MigrationName1770407024682 } from "./1770407024682-MigrationName";
import { MigrationName1770668054908 } from "./1770668054908-MigrationName";
import { MigrationName1770728946893 } from "./1770728946893-MigrationName";
import { MigrationName1770732721195 } from "./1770732721195-MigrationName";
export default [
InitialMigration,
@@ -512,4 +514,6 @@ export default [
MigrationName1770237245070,
MigrationName1770407024682,
MigrationName1770668054908,
MigrationName1770728946893,
MigrationName1770732721195,
];

View File

@@ -94,26 +94,16 @@ export class Service extends DatabaseService<Model> {
});
// Update episode's alertCount and lastAlertAddedAt
Promise.resolve()
.then(async () => {
try {
await AlertEpisodeService.updateAlertCount(
createdItem.alertEpisodeId!,
);
await AlertEpisodeService.updateLastAlertAddedAt(
createdItem.alertEpisodeId!,
);
} catch (error) {
logger.error(
`Error updating episode counts in AlertEpisodeMemberService.onCreateSuccess: ${error}`,
);
}
})
.catch((error: Error) => {
logger.error(
`Critical error in AlertEpisodeMemberService.onCreateSuccess: ${error}`,
);
});
try {
await AlertEpisodeService.updateAlertCount(createdItem.alertEpisodeId!);
await AlertEpisodeService.updateLastAlertAddedAt(
createdItem.alertEpisodeId!,
);
} catch (error) {
logger.error(
`Error updating episode counts in AlertEpisodeMemberService.onCreateSuccess: ${error}`,
);
}
// Get alert details for feed
const alert: Alert | null = await AlertService.findOneById({

View File

@@ -815,11 +815,12 @@ export class Service extends DatabaseService<Model> {
},
});
// Clear resolved timestamp
// Clear resolved timestamp and allAlertsResolvedAt
await this.updateOneById({
id: episodeId,
data: {
resolvedAt: undefined as any,
resolvedAt: null,
allAlertsResolvedAt: null,
},
props: {
isRoot: true,

View File

@@ -688,6 +688,7 @@ class AlertGroupingEngineServiceClass {
newEpisode.alertGroupingRuleId = rule.id!;
newEpisode.groupingKey = groupingKey;
newEpisode.isManuallyCreated = false;
newEpisode.lastAlertAddedAt = OneUptimeDate.getCurrentDate();
// Set severity from alert
if (alert.alertSeverityId) {

View File

@@ -96,26 +96,18 @@ export class Service extends DatabaseService<Model> {
});
// Update episode's incidentCount and lastIncidentAddedAt
Promise.resolve()
.then(async () => {
try {
await IncidentEpisodeService.updateIncidentCount(
createdItem.incidentEpisodeId!,
);
await IncidentEpisodeService.updateLastIncidentAddedAt(
createdItem.incidentEpisodeId!,
);
} catch (error) {
logger.error(
`Error updating episode counts in IncidentEpisodeMemberService.onCreateSuccess: ${error}`,
);
}
})
.catch((error: Error) => {
logger.error(
`Critical error in IncidentEpisodeMemberService.onCreateSuccess: ${error}`,
);
});
try {
await IncidentEpisodeService.updateIncidentCount(
createdItem.incidentEpisodeId!,
);
await IncidentEpisodeService.updateLastIncidentAddedAt(
createdItem.incidentEpisodeId!,
);
} catch (error) {
logger.error(
`Error updating episode counts in IncidentEpisodeMemberService.onCreateSuccess: ${error}`,
);
}
// Get incident details for feed
const incident: Incident | null = await IncidentService.findOneById({

View File

@@ -609,6 +609,18 @@ export class Service extends DatabaseService<Model> {
},
cascadeToIncidents: cascadeToIncidents,
});
// Clear resolved timestamp and allIncidentsResolvedAt when episode is reopened
await this.updateOneById({
id: episodeId,
data: {
resolvedAt: null,
allIncidentsResolvedAt: null,
},
props: {
isRoot: true,
},
});
}
@CaptureSpan()

View File

@@ -757,6 +757,7 @@ class IncidentGroupingEngineServiceClass {
newEpisode.incidentGroupingRuleId = rule.id!;
newEpisode.groupingKey = groupingKey;
newEpisode.isManuallyCreated = false;
newEpisode.lastIncidentAddedAt = OneUptimeDate.getCurrentDate();
// Set severity from incident
if (incident.incidentSeverityId) {

View File

@@ -79,6 +79,7 @@ import ProjectSmtpConfigService from "./ProjectSmtpConfigService";
import ProjectSsoService from "./ProjectSsoService";
import PromoCodeService from "./PromoCodeService";
import EnterpriseLicenseService from "./EnterpriseLicenseService";
import OpenSourceDeploymentService from "./OpenSourceDeploymentService";
import ResellerPlanService from "./ResellerPlanService";
import ResellerService from "./ResellerService";
import ScheduledMaintenanceCustomFieldService from "./ScheduledMaintenanceCustomFieldService";
@@ -210,6 +211,7 @@ const services: Array<BaseService> = [
AcmeCertificateService,
PromoCodeService,
EnterpriseLicenseService,
OpenSourceDeploymentService,
ResellerService,
ResellerPlanService,

View File

@@ -0,0 +1,10 @@
import DatabaseService from "./DatabaseService";
import OpenSourceDeployment from "../../Models/DatabaseModels/OpenSourceDeployment";
export class Service extends DatabaseService<OpenSourceDeployment> {
public constructor() {
super(OpenSourceDeployment);
}
}
export default new Service();

View File

@@ -646,7 +646,14 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
})
.map((field: Field<T>, i: number) => {
return (
<div key={getFieldName(field)}>
<div
key={getFieldName(field)}
className={
field.spanFullRow
? `md:col-span-${props.showAsColumns || 1}`
: undefined
}
>
{
<FormField<T>
field={field}

View File

@@ -124,4 +124,7 @@ export default interface Field<TEntity> {
disableSpellCheck?: boolean | undefined;
getSummaryElement?: (item: FormValues<TEntity>) => ReactElement | undefined;
// If true, this field will span the full row in multi-column layouts.
spanFullRow?: boolean | undefined;
}

View File

@@ -423,7 +423,6 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0",
@@ -926,7 +925,6 @@
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-5.23.0.tgz",
"integrity": "sha512-iI/Ssl8T5ZEn9s899Qz67m92M6RU8thf/aqD7cUHB2yHmkCjqbw7s7NaODTsyArAsnyu7DGJMWm7EhbfFXDNgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@bull-board/api": "5.23.0"
}
@@ -2564,7 +2562,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -5728,7 +5725,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -5935,7 +5931,8 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/unist": {
"version": "2.0.11",
@@ -6105,7 +6102,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6508,13 +6504,13 @@
}
},
"node_modules/axios": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -7027,7 +7023,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
@@ -7865,7 +7860,6 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
@@ -8287,7 +8281,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -9755,9 +9748,9 @@
"license": "Apache-2.0"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
@@ -9818,9 +9811,9 @@
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -11421,7 +11414,6 @@
"integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^28.1.3",
"@jest/types": "^28.1.3",
@@ -15230,7 +15222,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@@ -15585,7 +15576,6 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -15902,7 +15892,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -15987,7 +15976,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -16508,8 +16496,7 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0",
"peer": true
"license": "Apache-2.0"
},
"node_modules/refractor": {
"version": "5.0.0",
@@ -19922,7 +19909,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -19931,8 +19917,7 @@
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
"integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/zustand": {
"version": "4.5.5",

File diff suppressed because it is too large Load Diff

View File

@@ -225,6 +225,9 @@ Usage:
value: {{ default "" $.Values.captcha.secretKey | quote }}
- name: OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL
value: {{ default "" $.Values.openSourceDeployment.webhookUrl | quote }}
- name: NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER
value: {{ $.Values.notifications.webhooks.slack.onCreateUser }}

View File

@@ -524,6 +524,10 @@ externalClickhouse:
cert:
key:
openSourceDeployment:
# This webhook is called when a new self-hosted open source deployment registers.
webhookUrl:
# Notification webhooks when certain events happen in the system. (usually they are slack webhooks)
notifications:
webhooks:

View File

@@ -1429,13 +1429,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -2236,15 +2236,16 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -2255,9 +2256,9 @@
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
9.5.6
9.5.7

View File

@@ -38,6 +38,7 @@ RunCron(
projectId: true,
alertGroupingRuleId: true,
lastAlertAddedAt: true,
allAlertsResolvedAt: true,
},
props: {
isRoot: true,
@@ -134,7 +135,6 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
// Check if all alerts are in resolved state or higher
let allResolved: boolean = true;
let lastResolvedAt: Date | null = null;
for (const alertId of alertIds) {
const alert: Alert | null = await AlertService.findOneById({
@@ -143,7 +143,6 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
currentAlertState: {
order: true,
},
updatedAt: true,
},
props: {
isRoot: true,
@@ -160,31 +159,61 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
allResolved = false;
break;
}
// Track the latest resolved time among alerts
if (alert.updatedAt) {
if (!lastResolvedAt || alert.updatedAt > lastResolvedAt) {
lastResolvedAt = alert.updatedAt;
}
}
}
if (!allResolved) {
// If any alert is unresolved, clear allAlertsResolvedAt
if (episode.allAlertsResolvedAt) {
await AlertEpisodeService.updateOneById({
id: episode.id,
data: {
allAlertsResolvedAt: null,
},
props: {
isRoot: true,
},
});
}
logger.debug(
`AlertEpisode:AutoResolve - Episode ${episode.id} has unresolved alerts`,
);
return;
}
// All alerts are resolved. Check if resolve delay has passed (only if enabled)
if (enableResolveDelay && resolveDelayMinutes > 0 && lastResolvedAt) {
const timeSinceLastResolved: number =
OneUptimeDate.getDifferenceInMinutes(
lastResolvedAt,
OneUptimeDate.getCurrentDate(),
);
// All alerts are resolved. Set allAlertsResolvedAt if not already set.
if (!episode.allAlertsResolvedAt) {
await AlertEpisodeService.updateOneById({
id: episode.id,
data: {
allAlertsResolvedAt: OneUptimeDate.getCurrentDate(),
},
props: {
isRoot: true,
},
});
if (timeSinceLastResolved < resolveDelayMinutes) {
// If resolve delay is enabled, return and wait for the delay
if (enableResolveDelay && resolveDelayMinutes > 0) {
logger.debug(
`AlertEpisode:AutoResolve - Episode ${episode.id} all alerts resolved, starting resolve delay (${resolveDelayMinutes} minutes)`,
);
return;
}
}
// Check if resolve delay has passed (only if enabled)
if (
enableResolveDelay &&
resolveDelayMinutes > 0 &&
episode.allAlertsResolvedAt
) {
const timeSinceAllResolved: number = OneUptimeDate.getDifferenceInMinutes(
episode.allAlertsResolvedAt,
OneUptimeDate.getCurrentDate(),
);
if (timeSinceAllResolved < resolveDelayMinutes) {
logger.debug(
`AlertEpisode:AutoResolve - Episode ${episode.id} waiting for resolve delay (${resolveDelayMinutes} minutes)`,
);

View File

@@ -1,11 +1,14 @@
import RunCron from "../../Utils/Cron";
import OneUptimeDate from "Common/Types/Date";
import { EVERY_MINUTE } from "Common/Utils/CronTime";
import IncidentEpisodeService from "Common/Server/Services/IncidentEpisodeService";
import IncidentEpisodeMemberService from "Common/Server/Services/IncidentEpisodeMemberService";
import IncidentStateService from "Common/Server/Services/IncidentStateService";
import IncidentGroupingRuleService from "Common/Server/Services/IncidentGroupingRuleService";
import IncidentService from "Common/Server/Services/IncidentService";
import logger from "Common/Server/Utils/Logger";
import IncidentEpisode from "Common/Models/DatabaseModels/IncidentEpisode";
import IncidentGroupingRule from "Common/Models/DatabaseModels/IncidentGroupingRule";
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
import Incident from "Common/Models/DatabaseModels/Incident";
import ObjectID from "Common/Types/ObjectID";
@@ -33,7 +36,9 @@ RunCron(
select: {
_id: true,
projectId: true,
incidentGroupingRuleId: true,
lastIncidentAddedAt: true,
allIncidentsResolvedAt: true,
},
props: {
isRoot: true,
@@ -71,6 +76,31 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
return;
}
// Get resolve delay from the grouping rule if exists and enabled
let resolveDelayMinutes: number = 0;
let enableResolveDelay: boolean = false;
if (episode.incidentGroupingRuleId) {
const rule: IncidentGroupingRule | null =
await IncidentGroupingRuleService.findOneById({
id: episode.incidentGroupingRuleId,
select: {
enableResolveDelay: true,
resolveDelayMinutes: true,
},
props: {
isRoot: true,
},
});
if (rule) {
enableResolveDelay = rule.enableResolveDelay || false;
if (enableResolveDelay && rule.resolveDelayMinutes) {
resolveDelayMinutes = rule.resolveDelayMinutes;
}
}
}
// Get all incidents in this episode
const incidentIds: ObjectID[] =
await IncidentEpisodeMemberService.getIncidentsInEpisode(episode.id);
@@ -116,7 +146,6 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
currentIncidentState: {
order: true,
},
updatedAt: true,
},
props: {
isRoot: true,
@@ -136,12 +165,65 @@ const checkAndResolveEpisode: CheckAndResolveEpisodeFunction = async (
}
if (!allResolved) {
// If any incident is unresolved, clear allIncidentsResolvedAt
if (episode.allIncidentsResolvedAt) {
await IncidentEpisodeService.updateOneById({
id: episode.id,
data: {
allIncidentsResolvedAt: null,
},
props: {
isRoot: true,
},
});
}
logger.debug(
`IncidentEpisode:AutoResolve - Episode ${episode.id} has unresolved incidents`,
);
return;
}
// All incidents are resolved. Set allIncidentsResolvedAt if not already set.
if (!episode.allIncidentsResolvedAt) {
await IncidentEpisodeService.updateOneById({
id: episode.id,
data: {
allIncidentsResolvedAt: OneUptimeDate.getCurrentDate(),
},
props: {
isRoot: true,
},
});
// If resolve delay is enabled, return and wait for the delay
if (enableResolveDelay && resolveDelayMinutes > 0) {
logger.debug(
`IncidentEpisode:AutoResolve - Episode ${episode.id} all incidents resolved, starting resolve delay (${resolveDelayMinutes} minutes)`,
);
return;
}
}
// Check if resolve delay has passed (only if enabled)
if (
enableResolveDelay &&
resolveDelayMinutes > 0 &&
episode.allIncidentsResolvedAt
) {
const timeSinceAllResolved: number = OneUptimeDate.getDifferenceInMinutes(
episode.allIncidentsResolvedAt,
OneUptimeDate.getCurrentDate(),
);
if (timeSinceAllResolved < resolveDelayMinutes) {
logger.debug(
`IncidentEpisode:AutoResolve - Episode ${episode.id} waiting for resolve delay (${resolveDelayMinutes} minutes)`,
);
return;
}
}
// Resolve the episode
logger.info(
`IncidentEpisode:AutoResolve - Resolving episode ${episode.id} as all incidents are resolved`,

View File

@@ -2,13 +2,12 @@ import RunCron from "../../Utils/Cron";
import OneUptimeDate from "Common/Types/Date";
import { EVERY_FIVE_MINUTE } from "Common/Utils/CronTime";
import IncidentEpisodeService from "Common/Server/Services/IncidentEpisodeService";
import IncidentGroupingRuleService from "Common/Server/Services/IncidentGroupingRuleService";
import logger from "Common/Server/Utils/Logger";
import IncidentEpisode from "Common/Models/DatabaseModels/IncidentEpisode";
import IncidentGroupingRule from "Common/Models/DatabaseModels/IncidentGroupingRule";
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
// Default inactivity timeout in minutes (24 hours)
const DEFAULT_INACTIVITY_TIMEOUT_MINUTES: number = 1440;
RunCron(
"IncidentEpisode:ResolveInactiveEpisodes",
{
@@ -31,6 +30,7 @@ RunCron(
select: {
_id: true,
projectId: true,
incidentGroupingRuleId: true,
lastIncidentAddedAt: true,
},
props: {
@@ -68,9 +68,38 @@ const checkAndResolveInactiveEpisode: CheckAndResolveInactiveEpisodeFunction =
return;
}
// Use default inactivity timeout since there's no grouping rule for incidents
const inactivityTimeoutMinutes: number =
DEFAULT_INACTIVITY_TIMEOUT_MINUTES;
// Get inactivity timeout from the grouping rule (only if enabled)
let inactivityTimeoutMinutes: number = 0;
let enableInactivityTimeout: boolean = false;
if (episode.incidentGroupingRuleId) {
const rule: IncidentGroupingRule | null =
await IncidentGroupingRuleService.findOneById({
id: episode.incidentGroupingRuleId,
select: {
enableInactivityTimeout: true,
inactivityTimeoutMinutes: true,
},
props: {
isRoot: true,
},
});
if (rule) {
enableInactivityTimeout = rule.enableInactivityTimeout || false;
if (
enableInactivityTimeout &&
rule.inactivityTimeoutMinutes !== undefined
) {
inactivityTimeoutMinutes = rule.inactivityTimeoutMinutes;
}
}
}
// If inactivity timeout is not enabled or is 0, don't resolve inactive episodes
if (!enableInactivityTimeout || inactivityTimeoutMinutes <= 0) {
return;
}
// Check if episode has been inactive for too long
const lastIncidentAddedAt: Date =

View File

@@ -276,9 +276,13 @@ LETS_ENCRYPT_ACCOUNT_KEY=
# This is the number of active monitors allowed in the free plan.
ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN=10
# Open Source Deployment Webhook
# This webhook is called when a new self-hosted open source deployment registers.
OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL=
# Notifications Webhook (Slack)
# This webhook notifies slack when the new user signs up or is created.
# This webhook notifies slack when the new user signs up or is created.
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER=
# This webhook notifies slack when the new project is created.
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT=

View File

@@ -127,6 +127,9 @@ x-common-runtime-variables: &common-runtime-variables
DISABLE_AUTOMATIC_ALERT_CREATION: ${DISABLE_AUTOMATIC_ALERT_CREATION}
# Open Source Deployment Webhook
OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL: ${OPEN_SOURCE_DEPLOYMENT_WEBHOOK_URL}
# Notification Webhooks
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_USER}
NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT: ${NOTIFICATION_SLACK_WEBHOOK_ON_CREATED_PROJECT}