@stamhoofd/backend 2.114.0 → 2.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +1 -0
- package/package.json +11 -10
- package/src/boot.ts +30 -19
- package/src/email-recipient-loaders/documents.ts +1 -14
- package/src/email-recipient-loaders/payments.ts +620 -0
- package/src/email-recipient-loaders/receivable-balances.ts +2 -11
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +1 -1
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -5
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +15 -0
- package/src/endpoints/system/HealthEndpoint.ts +77 -0
- package/src/helpers/email-html-helpers.ts +144 -0
- package/src/seeds/data/default-email-templates.sql +2 -1
- package/src/services/CpuService.ts +123 -0
- package/src/sql-filters/balance-item-payments.ts +5 -0
- package/src/sql-filters/member-responsibility-records.ts +16 -0
- package/src/sql-filters/payments.ts +5 -0
- package/src/sql-sorters/document-templates.ts +1 -1
- package/src/sql-sorters/documents.ts +1 -1
- package/src/sql-sorters/orders.ts +0 -84
- package/src/sql-sorters/organizations.ts +3 -3
- /package/src/{seeds → seeds-temporary}/1769088653-uitpas-status.ts +0 -0
|
@@ -4,11 +4,11 @@ import { assertSort, CountFilteredRequest, Document as DocumentStruct, getSortFi
|
|
|
4
4
|
|
|
5
5
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { applySQLSorter, compileToSQLFilter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
7
|
-
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
|
-
import { Context } from '../../../../helpers/Context';
|
|
9
|
-
import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper';
|
|
10
|
-
import { documentFilterCompilers } from '../../../../sql-filters/documents';
|
|
11
|
-
import { documentSorters } from '../../../../sql-sorters/documents';
|
|
7
|
+
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
|
|
8
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
9
|
+
import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper.js';
|
|
10
|
+
import { documentFilterCompilers } from '../../../../sql-filters/documents.js';
|
|
11
|
+
import { documentSorters } from '../../../../sql-sorters/documents.js';
|
|
12
12
|
|
|
13
13
|
type Params = Record<string, never>;
|
|
14
14
|
type Query = LimitedFilteredRequest;
|
|
@@ -45,6 +45,7 @@ export class GetDocumentsEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
45
45
|
|
|
46
46
|
const query = SQL
|
|
47
47
|
.select(SQL.wildcard(documentTable))
|
|
48
|
+
.setMaxExecutionTime(15 * 1000)
|
|
48
49
|
.from(SQL.table(documentTable))
|
|
49
50
|
.where('organizationId', organization.id);
|
|
50
51
|
|
|
@@ -124,8 +124,23 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
124
124
|
shouldUpdateSetupSteps = true;
|
|
125
125
|
}
|
|
126
126
|
organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles);
|
|
127
|
+
if (request.body.privateMeta.roles) {
|
|
128
|
+
for (const role of organization.privateMeta.roles) {
|
|
129
|
+
role.compress();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
127
132
|
organization.privateMeta.responsibilities = request.body.privateMeta.responsibilities.applyTo(organization.privateMeta.responsibilities);
|
|
133
|
+
if (request.body.privateMeta.responsibilities) {
|
|
134
|
+
for (const responsibility of organization.privateMeta.responsibilities) {
|
|
135
|
+
responsibility.compress();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
128
138
|
organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles);
|
|
139
|
+
if (request.body.privateMeta.inheritedResponsibilityRoles) {
|
|
140
|
+
for (const role of organization.privateMeta.inheritedResponsibilityRoles) {
|
|
141
|
+
role.compress();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
129
144
|
organization.privateMeta.privateKey = request.body.privateMeta.privateKey ?? organization.privateMeta.privateKey;
|
|
130
145
|
organization.privateMeta.featureFlags = patchObject(organization.privateMeta.featureFlags, request.body.privateMeta.featureFlags);
|
|
131
146
|
organization.privateMeta.balanceNotificationSettings = patchObject(organization.privateMeta.balanceNotificationSettings, request.body.privateMeta.balanceNotificationSettings);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AutoEncoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { CpuService } from '../../services/CpuService.js';
|
|
5
|
+
|
|
6
|
+
type Params = Record<string, never>;
|
|
7
|
+
type Body = undefined;
|
|
8
|
+
|
|
9
|
+
class Query extends AutoEncoder {
|
|
10
|
+
@field({ decoder: StringDecoder, optional: true })
|
|
11
|
+
key?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ResponseBody extends AutoEncoder {
|
|
15
|
+
@field({ decoder: StringDecoder })
|
|
16
|
+
status: 'ok' | 'error';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export class HealthEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
24
|
+
queryDecoder = Query;
|
|
25
|
+
|
|
26
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
27
|
+
if (request.method !== 'GET') {
|
|
28
|
+
return [false];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!STAMHOOFD.HEALTH_ACCESS_KEY) {
|
|
32
|
+
return [false];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const params = Endpoint.parseParameters(request.url, '/health', {});
|
|
36
|
+
|
|
37
|
+
if (params) {
|
|
38
|
+
return [true, params as Params];
|
|
39
|
+
}
|
|
40
|
+
return [false];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
44
|
+
if (!STAMHOOFD.HEALTH_ACCESS_KEY) {
|
|
45
|
+
throw new SimpleError({
|
|
46
|
+
code: 'unauthorized',
|
|
47
|
+
message: 'Unauthorized',
|
|
48
|
+
statusCode: 401,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (STAMHOOFD.HEALTH_ACCESS_KEY && request.query.key !== STAMHOOFD.HEALTH_ACCESS_KEY) {
|
|
53
|
+
throw new SimpleError({
|
|
54
|
+
code: 'unauthorized',
|
|
55
|
+
message: 'Unauthorized',
|
|
56
|
+
statusCode: 401,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const health = ResponseBody.create({
|
|
61
|
+
status: 'ok',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (CpuService.getAverage(60) > 80) {
|
|
65
|
+
health.status = 'error';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = new Response(
|
|
69
|
+
health,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (health.status === 'error') {
|
|
73
|
+
response.status = 503;
|
|
74
|
+
}
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Order, Webshop } from '@stamhoofd/models';
|
|
2
|
+
import { CheckoutMethodType, PaymentGeneral, PaymentMethod, PaymentMethodHelper, RecordCategory, RecordCheckboxAnswer, WebshopTakeoutMethod } from '@stamhoofd/structures';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
+
|
|
5
|
+
export function createOrderDataHTMLTable(order: Order, webshop: Webshop): string {
|
|
6
|
+
let str = `<table width="100%" cellspacing="0" cellpadding="0" class="email-data-table"><tbody>`;
|
|
7
|
+
|
|
8
|
+
const data = [
|
|
9
|
+
{
|
|
10
|
+
title: $t('4d496edf-0203-4df3-a6e9-3e58d226d6c5'),
|
|
11
|
+
value: '' + (order.number ?? '?'),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
title: ((order) => {
|
|
15
|
+
if (order.data.checkoutMethod?.type === CheckoutMethodType.Takeout) {
|
|
16
|
+
return $t(`8113733b-00ea-42ae-8829-6056774a8be0`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (order.data.checkoutMethod?.type === CheckoutMethodType.OnSite) {
|
|
20
|
+
return $t(`7eec15d0-4d60-423f-b860-4f3824271578`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return $t(`8a910c54-1b2d-4963-9128-2cab93b0151b`);
|
|
24
|
+
})(order),
|
|
25
|
+
value: ((order) => {
|
|
26
|
+
if (order.data.checkoutMethod?.type === CheckoutMethodType.Takeout) {
|
|
27
|
+
return order.data.checkoutMethod.name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (order.data.checkoutMethod?.type === CheckoutMethodType.OnSite) {
|
|
31
|
+
return order.data.checkoutMethod.name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return order.data.address?.shortString() ?? '';
|
|
35
|
+
})(order),
|
|
36
|
+
},
|
|
37
|
+
...(
|
|
38
|
+
(order.data.checkoutMethod?.type === CheckoutMethodType.Takeout || order.data.checkoutMethod?.type === CheckoutMethodType.OnSite) && ((order.data.checkoutMethod as any)?.address)
|
|
39
|
+
? [
|
|
40
|
+
{
|
|
41
|
+
title: $t(`f7e792ed-2265-41e9-845f-e3ce0bc5da7c`),
|
|
42
|
+
value: ((order) => {
|
|
43
|
+
return (order.data.checkoutMethod as WebshopTakeoutMethod)?.address?.shortString() ?? '';
|
|
44
|
+
})(order),
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
: []
|
|
48
|
+
),
|
|
49
|
+
{
|
|
50
|
+
title: $t(`40aabd99-0331-4267-9b6a-a87c06b3f7fe`),
|
|
51
|
+
value: Formatter.capitalizeFirstLetter(order.data.timeSlot?.dateString() ?? ''),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: $t(`7853cca1-c41a-4687-9502-190849405f76`),
|
|
55
|
+
value: order.data.timeSlot?.timeRangeString() ?? '',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: $t(`17edcdd6-4fb2-4882-adec-d3a4f43a1926`),
|
|
59
|
+
value: order.data.customer.name,
|
|
60
|
+
},
|
|
61
|
+
...(order.data.customer.phone
|
|
62
|
+
? [
|
|
63
|
+
{
|
|
64
|
+
title: $t(`feea3664-9353-4bd4-b17d-aff005d3e265`),
|
|
65
|
+
value: order.data.customer.phone,
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
: []),
|
|
69
|
+
...order.data.fieldAnswers.filter(a => a.answer).map(a => ({
|
|
70
|
+
title: a.field.name,
|
|
71
|
+
value: a.answer,
|
|
72
|
+
})),
|
|
73
|
+
...RecordCategory.sortAnswers(order.data.recordAnswers, webshop.meta.recordCategories).filter(a => !a.isEmpty || a instanceof RecordCheckboxAnswer).map(a => ({
|
|
74
|
+
title: a.settings.name.toString(),
|
|
75
|
+
value: a.stringValue,
|
|
76
|
+
})),
|
|
77
|
+
...(
|
|
78
|
+
(order.data.paymentMethod !== PaymentMethod.Unknown)
|
|
79
|
+
? [
|
|
80
|
+
{
|
|
81
|
+
title: $t(`07e7025c-0bfb-41be-87bc-1023d297a1a2`),
|
|
82
|
+
value: Formatter.capitalizeFirstLetter(PaymentMethodHelper.getName(order.data.paymentMethod)),
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
: []
|
|
86
|
+
),
|
|
87
|
+
...order.data.priceBreakown.map((p) => {
|
|
88
|
+
return {
|
|
89
|
+
title: p.name,
|
|
90
|
+
value: Formatter.price(p.price),
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const replacement of data) {
|
|
96
|
+
if (replacement.value.length === 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
str += `<tr><td><h4>${Formatter.escapeHtml(replacement.title)}</h4></td><td>${Formatter.escapeHtml(replacement.value)}</td></tr>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return str + '</tbody></table>';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createPaymentDataHTMLTable(payment: PaymentGeneral): string {
|
|
106
|
+
let str = `<table width="100%" cellspacing="0" cellpadding="0" class="email-data-table"><tbody>`;
|
|
107
|
+
|
|
108
|
+
const customer = payment.customer;
|
|
109
|
+
|
|
110
|
+
const replacements: { title: string; value: string }[] = [];
|
|
111
|
+
|
|
112
|
+
if (customer) {
|
|
113
|
+
replacements.push({
|
|
114
|
+
title: $t(`17edcdd6-4fb2-4882-adec-d3a4f43a1926`),
|
|
115
|
+
value: customer.dynamicName,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (customer.phone) {
|
|
119
|
+
replacements.push({
|
|
120
|
+
title: $t(`feea3664-9353-4bd4-b17d-aff005d3e265`),
|
|
121
|
+
value: customer.phone,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
replacements.push({
|
|
127
|
+
title: $t(`07e7025c-0bfb-41be-87bc-1023d297a1a2`),
|
|
128
|
+
value: Formatter.capitalizeFirstLetter(PaymentMethodHelper.getName(payment.method ?? PaymentMethod.Unknown)),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
replacements.push({
|
|
132
|
+
title: $t(`Totaal`),
|
|
133
|
+
value: Formatter.price(payment.price),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
for (const replacement of replacements) {
|
|
137
|
+
if (replacement.value.length === 0) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
str += `<tr><td><h4>${Formatter.escapeHtml(replacement.title)}</h4></td><td>${Formatter.escapeHtml(replacement.value)}</td></tr>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return str + '</tbody></table>';
|
|
144
|
+
}
|
|
@@ -53,4 +53,5 @@ INSERT INTO `email_templates` (`id`, `subject`, `groupId`, `webshopId`, `organiz
|
|
|
53
53
|
INSERT INTO `email_templates` (`id`, `subject`, `groupId`, `webshopId`, `organizationId`, `type`, `text`, `html`, `json`, `updatedAt`, `createdAt`) VALUES
|
|
54
54
|
('f3d1c594-7510-4f12-9899-ef6e0d3a64c0', 'Kampmelding voor {{eventName}} ({{dateRange}}) werd goedgekeurd', NULL, NULL, NULL, 'EventNotificationAccepted', '{{greeting}}Goed nieuws! Jullie kampmelding voor {{eventName}} ({{dateRange}}) van {{organizationName}} werd goedgekeurd. Deze werd ingediend door {{submitterName}}.\n\n \n \n \n \n Kampmelding bekijken ({{reviewUrl}})\n \n \n \n \n\n', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>Kampmelding voor {{eventName}} ({{dateRange}}) werd goedgekeurd</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Goed nieuws! Jullie kampmelding voor <strong>{{eventName}}</strong> ({{dateRange}}) van <strong>{{organizationName}}</strong> werd goedgekeurd. Deze werd ingediend door {{submitterName}}.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><div data-type=\"smartButton\" data-id=\"reviewUrl\"><table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"margin: 5px 0;\">\n<tbody><tr>\n <td>\n <table cellspacing=\"0\" cellpadding=\"0\">\n <tbody><tr>\n <td style=\"border-radius: 7px;\" bgcolor=\"{{primaryColor}}\">\n <a class=\"button primary\" href=\"{{reviewUrl}}\" target=\"\" style=\"margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity;\">Kampmelding bekijken</a>\n </td>\n </tr>\n </tbody></table>\n </td>\n</tr>\n</tbody></table></div><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Goed nieuws! Jullie kampmelding voor \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"eventName\"}, \"marks\": [{\"type\": \"bold\"}]}, {\"text\": \" (\", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"dateRange\"}}, {\"text\": \") van \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}, \"marks\": [{\"type\": \"bold\"}]}, {\"text\": \" werd goedgekeurd. Deze werd ingediend door \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"submitterName\"}}, {\"text\": \".\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"smartButton\", \"attrs\": {\"id\": \"reviewUrl\"}, \"content\": [{\"text\": \"Kampmelding bekijken\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}]}, \"version\": 377}', '2025-07-15 09:35:18', '2025-07-15 09:35:18'),
|
|
55
55
|
('f5bf2c0a-0ac2-4d1d-a2d2-5c1c0139d399', 'Wachtwoord van {{organizationName}} vergeten', NULL, NULL, NULL, 'ForgotPasswordButNoAccount', '{{greeting}}Je gaf aan dat je jouw wachtwoord bent vergeten van {{organizationName}}, maar er bestaat geen account op het e-mailadres dat je hebt ingegeven ({{email}}). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarmee je een account hebt. Lukt dat niet? Dan kan je via ons ledenportaal een nieuw account aanmaken met een e-mailadres naar keuze.{{organizationName}}', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>Wachtwoord van {{organizationName}} vergeten</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Je gaf aan dat je jouw wachtwoord bent vergeten van {{organizationName}}, maar er bestaat geen account op het e-mailadres dat je hebt ingegeven ({{email}}). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarmee je een account hebt. Lukt dat niet? Dan kan je via ons ledenportaal een nieuw account aanmaken met een e-mailadres naar keuze.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{organizationName}}</p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Je gaf aan dat je jouw wachtwoord bent vergeten van \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}, {\"text\": \", maar er bestaat geen account op het e-mailadres dat je hebt ingegeven (\", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"email\"}}, {\"text\": \"). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarmee je een account hebt. Lukt dat niet? Dan kan je via ons ledenportaal een nieuw account aanmaken met een e-mailadres naar keuze.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}]}]}, \"version\": 377}', '2025-07-15 09:18:37', '2025-07-15 09:18:37'),
|
|
56
|
-
('fdf5ff55-5dd3-45e5-82d3-b5b4f511582d', 'DNS-records van {{mailDomain}} zijn onstabiel', NULL, NULL, NULL, 'OrganizationUnstableDNS', '{{greeting}}Dit is een technische e-mail bestemd voor jullie webmaster.Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.Stamhoofd', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>DNS-records van {{mailDomain}} zijn onstabiel</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Dit is een technische e-mail bestemd voor jullie webmaster.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd</p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Dit is een technische e-mail bestemd voor jullie webmaster.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd controleert elke dag automatisch of jullie domeinnaam \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"mailDomain\"}}, {\"text\": \", die gekoppeld is aan \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}, {\"text\": \" op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd\", \"type\": \"text\"}]}]}, \"version\": 377}', '2025-07-15 09:24:17', '2025-07-15 09:24:17')
|
|
56
|
+
('fdf5ff55-5dd3-45e5-82d3-b5b4f511582d', 'DNS-records van {{mailDomain}} zijn onstabiel', NULL, NULL, NULL, 'OrganizationUnstableDNS', '{{greeting}}Dit is een technische e-mail bestemd voor jullie webmaster.Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.Stamhoofd', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>DNS-records van {{mailDomain}} zijn onstabiel</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Dit is een technische e-mail bestemd voor jullie webmaster.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd</p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Dit is een technische e-mail bestemd voor jullie webmaster.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd controleert elke dag automatisch of jullie domeinnaam \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"mailDomain\"}}, {\"text\": \", die gekoppeld is aan \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}, {\"text\": \" op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd\", \"type\": \"text\"}]}]}, \"version\": 377}', '2025-07-15 09:24:17', '2025-07-15 09:24:17'),
|
|
57
|
+
('61199699-492f-4fd3-a966-ca75179c3e71', '', NULL, NULL, NULL, 'DefaultPaymentsEmail', '{{greeting}}\n\nBetaalinstructies\n{{paymentTable}}\n\n{{overviewContext}}\n\n{{balanceItemPaymentsTable}}\nTotaal: {{paymentPrice}}\n\nKomt deze e-mail bij jou terecht, maar weet je niet waarover dit gaat en denk je dat dit aan een andere persoon is gericht (bv. een typefout)? Dan schrijf je best uit voor onze e-mails via deze knop ({{unsubscribeUrl}}) — hierna zal je nooit meer een e-mail van ons ontvangen.\nUitschrijven ({{unsubscribeUrl}})', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title></title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table td.price {\n white-space: nowrap;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><hr style=\"height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;\"><h2 style=\"margin: 0; padding: 0;\">Betaalinstructies</h2><div data-type=\"smartVariableBlock\" data-id=\"paymentTable\">{{paymentTable}}</div><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><h2 style=\"margin: 0; padding: 0;\">{{overviewContext}}</h2><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><div data-type=\"smartVariableBlock\" data-id=\"balanceItemPaymentsTable\">{{balanceItemPaymentsTable}}</div><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><h3 style=\"margin: 0; padding: 0;\">Totaal: {{paymentPrice}}</h3><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p class=\"description\" style=\"color: #5e5e5e;\"><em>Komt deze e-mail bij jou terecht, maar weet je niet waarover dit gaat en denk je dat dit aan een andere persoon is gericht (bv. een typefout)? Dan schrijf je best uit voor onze e-mails via </em><a class=\"inline-link\" href=\"{{unsubscribeUrl}}\" target=\"\" style=\"margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation;\"><em>deze knop</em></a><em> — hierna zal je nooit meer een e-mail van ons ontvangen.</em></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><a class=\"inline-link\" href=\"{{unsubscribeUrl}}\" target=\"\" style=\"margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation;\">Uitschrijven</a></p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"horizontalRule\"}, {\"type\": \"heading\", \"attrs\": {\"level\": 2}, \"content\": [{\"text\": \"Betaalinstructies\", \"type\": \"text\"}]}, {\"type\": \"smartVariableBlock\", \"attrs\": {\"id\": \"paymentTable\"}}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\"}, {\"type\": \"heading\", \"attrs\": {\"level\": 2}, \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"overviewContext\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"smartVariableBlock\", \"attrs\": {\"id\": \"balanceItemPaymentsTable\"}}, {\"type\": \"paragraph\"}, {\"type\": \"heading\", \"attrs\": {\"level\": 3}, \"content\": [{\"text\": \"Totaal: \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"paymentPrice\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\"}, {\"type\": \"descriptiveText\", \"content\": [{\"text\": \"Komt deze e-mail bij jou terecht, maar weet je niet waarover dit gaat en denk je dat dit aan een andere persoon is gericht (bv. een typefout)? Dan schrijf je best uit voor onze e-mails via \", \"type\": \"text\", \"marks\": [{\"type\": \"italic\"}]}, {\"type\": \"smartButtonInline\", \"attrs\": {\"id\": \"unsubscribeUrl\"}, \"content\": [{\"text\": \"deze knop\", \"type\": \"text\", \"marks\": [{\"type\": \"italic\"}]}]}, {\"text\": \" — hierna zal je nooit meer een e-mail van ons ontvangen.\", \"type\": \"text\", \"marks\": [{\"type\": \"italic\"}]}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"smartButtonInline\", \"attrs\": {\"id\": \"unsubscribeUrl\"}, \"content\": [{\"text\": \"Uitschrijven\", \"type\": \"text\"}]}]}]}, \"version\": 394}', '2026-02-10 17:44:20', '2026-02-10 17:44:20');
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { SQLLogger } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
class StaticCpuService {
|
|
4
|
+
samples: number[];
|
|
5
|
+
private maxSamples: number;
|
|
6
|
+
private interval?: NodeJS.Timeout;
|
|
7
|
+
// Current index = the last saved
|
|
8
|
+
private currentIndex = 0;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.maxSamples = 5 * 60; // 5 minutes of data
|
|
12
|
+
this.samples = new Array(this.maxSamples).fill(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get a live sample of CPU usage
|
|
17
|
+
* @param samplingInterval
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
async takeSample(samplingInterval = 500): Promise<number> {
|
|
21
|
+
const startUsage = process.cpuUsage();
|
|
22
|
+
const startTime = process.hrtime.bigint();
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
const elapsedUsage = process.cpuUsage(startUsage);
|
|
27
|
+
const elapsedTime = process.hrtime.bigint() - startTime;
|
|
28
|
+
|
|
29
|
+
// Convert to microseconds
|
|
30
|
+
const elapsedTimeMs = Number(elapsedTime) / 1000;
|
|
31
|
+
|
|
32
|
+
// CPU time in microseconds
|
|
33
|
+
const totalCPUTime = elapsedUsage.user + elapsedUsage.system;
|
|
34
|
+
|
|
35
|
+
// Calculate percentage
|
|
36
|
+
const cpuPercent = (totalCPUTime / elapsedTimeMs) * 100;
|
|
37
|
+
|
|
38
|
+
resolve(cpuPercent);
|
|
39
|
+
}, samplingInterval);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getCpuUsage(): number | undefined {
|
|
44
|
+
if (this.currentIndex === 0) {
|
|
45
|
+
return this.samples[this.samples.length - 1];
|
|
46
|
+
}
|
|
47
|
+
return this.samples[this.currentIndex - 1];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getAverage(size = this.maxSamples): number {
|
|
51
|
+
if (size > this.maxSamples) {
|
|
52
|
+
size = this.maxSamples;
|
|
53
|
+
}
|
|
54
|
+
if (size <= 0) {
|
|
55
|
+
return NaN;
|
|
56
|
+
}
|
|
57
|
+
if (size === this.maxSamples) {
|
|
58
|
+
return this.samples.reduce((a, b) => a + b, 0) / size;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// To read before current index
|
|
62
|
+
const toReadBeforeIndex = Math.min(size, this.currentIndex);
|
|
63
|
+
const toReadFromEnd = size - toReadBeforeIndex;
|
|
64
|
+
|
|
65
|
+
// Sum performantly
|
|
66
|
+
let sum = 0;
|
|
67
|
+
for (let i = 0; i < toReadBeforeIndex; i++) {
|
|
68
|
+
sum += this.samples[this.currentIndex - 1 - i];
|
|
69
|
+
}
|
|
70
|
+
for (let i = 0; i < toReadFromEnd; i++) {
|
|
71
|
+
sum += this.samples[this.samples.length - 1 - i];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return sum / size;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async saveSample() {
|
|
79
|
+
const sample = await this.takeSample(1000);
|
|
80
|
+
this.samples[this.currentIndex] = sample;
|
|
81
|
+
this.currentIndex = (this.currentIndex + 1) % this.maxSamples;
|
|
82
|
+
const five = this.getAverage(5);
|
|
83
|
+
|
|
84
|
+
if (this.currentIndex % 5 === 0 || five > 80) {
|
|
85
|
+
const min = this.getAverage(60);
|
|
86
|
+
console.log(`[CPU] 5s: ${five.toFixed(2)}%\n[CPU] 1 min: ${min.toFixed(2)}%\n[CPU] 5 min: ${this.getAverage(60 * 5).toFixed(2)}%`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (five > 80) {
|
|
90
|
+
// Danger zone, in this case we don't want to log all slow queries any longer because the information won't be trustworthy.
|
|
91
|
+
SQLLogger.slowQueryThresholdMs = null;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
if (five < 20) {
|
|
95
|
+
// No load, safe to log all slow queries
|
|
96
|
+
SQLLogger.slowQueryThresholdMs = 300;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
SQLLogger.slowQueryThresholdMs = 500;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
startMonitoring() {
|
|
105
|
+
if (this.interval) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.interval = setInterval(() => {
|
|
109
|
+
this.saveSample().catch((error) => {
|
|
110
|
+
console.error('Failed to take CPU sample:', error);
|
|
111
|
+
});
|
|
112
|
+
}, 1000); // Sample every second
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stopMonitoring() {
|
|
116
|
+
if (this.interval) {
|
|
117
|
+
clearInterval(this.interval);
|
|
118
|
+
this.interval = undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const CpuService = new StaticCpuService();
|
|
@@ -24,5 +24,10 @@ export const balanceItemPaymentsCompilers: SQLFilterDefinitions = {
|
|
|
24
24
|
type: SQLValueType.String,
|
|
25
25
|
nullable: false,
|
|
26
26
|
}),
|
|
27
|
+
payingOrganizationId: createColumnFilter({
|
|
28
|
+
expression: SQL.column('balance_items', 'payingOrganizationId'),
|
|
29
|
+
type: SQLValueType.String,
|
|
30
|
+
nullable: true,
|
|
31
|
+
}),
|
|
27
32
|
},
|
|
28
33
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MemberResponsibilityRecord } from '@stamhoofd/models';
|
|
2
|
+
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
|
|
3
|
+
|
|
4
|
+
const baseTable = SQL.table(MemberResponsibilityRecord.table);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Defines how to filter member responsibility records in the database from StamhoofdFilter objects
|
|
8
|
+
*/
|
|
9
|
+
export const memberResponsibilityRecordFilterCompilers: SQLFilterDefinitions = {
|
|
10
|
+
...baseSQLFilterCompilers,
|
|
11
|
+
responsibilityId: createColumnFilter({
|
|
12
|
+
expression: SQL.column(baseTable, 'responsibilityId'),
|
|
13
|
+
type: SQLValueType.String,
|
|
14
|
+
nullable: false,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
@@ -31,6 +31,11 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
31
31
|
type: SQLValueType.String,
|
|
32
32
|
nullable: true,
|
|
33
33
|
}),
|
|
34
|
+
payingOrganizationId: createColumnFilter({
|
|
35
|
+
expression: SQL.column('payingOrganizationId'),
|
|
36
|
+
type: SQLValueType.String,
|
|
37
|
+
nullable: true,
|
|
38
|
+
}),
|
|
34
39
|
createdAt: createColumnFilter({
|
|
35
40
|
expression: SQL.column('createdAt'),
|
|
36
41
|
type: SQLValueType.Datetime,
|
|
@@ -27,7 +27,7 @@ export const documentTemplateSorters: SQLSortDefinitions<DocumentTemplate> = {
|
|
|
27
27
|
},
|
|
28
28
|
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
29
29
|
return new SQLOrderBy({
|
|
30
|
-
column: SQL.
|
|
30
|
+
column: SQL.jsonValue(SQL.column('settings'), '$.value.name', 'CHAR'),
|
|
31
31
|
direction,
|
|
32
32
|
});
|
|
33
33
|
},
|
|
@@ -27,7 +27,7 @@ export const documentSorters: SQLSortDefinitions<Document> = {
|
|
|
27
27
|
},
|
|
28
28
|
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
29
29
|
return new SQLOrderBy({
|
|
30
|
-
column: SQL.
|
|
30
|
+
column: SQL.jsonValue(SQL.column('data'), '$.value.description', 'CHAR'),
|
|
31
31
|
direction,
|
|
32
32
|
});
|
|
33
33
|
},
|