data-primals-engine 1.4.3 → 1.5.1
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/README.md +913 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +3 -7
- package/client/src/App.scss +25 -3
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +30 -12
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +104 -19
- package/client/src/DataEditor.jsx +12 -5
- package/client/src/DataLayout.jsx +805 -762
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +63 -77
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +591 -322
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +6 -6
- package/client/src/ModelCreatorField.jsx +74 -31
- package/client/src/ModelList.jsx +93 -54
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/constants.js +1 -1
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +26 -24
- package/package.json +3 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +3 -28
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +63 -7
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +151 -19
- package/src/modules/bucket.js +14 -16
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +9 -56
- package/src/modules/data/data.operations.js +3282 -2999
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +118 -24
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +85 -3
- package/src/modules/file.js +247 -236
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +5482 -5461
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +136 -2
- package/test/import_export.integration.test.js +1 -1
|
@@ -4,7 +4,7 @@ import { useUI } from '../contexts/UIContext.jsx';
|
|
|
4
4
|
import { useNotificationContext } from '../NotificationProvider.jsx';
|
|
5
5
|
import { tutorialsConfig } from '../tutorials.js';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import useLocalStorage from "./useLocalStorage.js";
|
|
9
9
|
import {Event} from "../../../src/events.js";
|
|
10
10
|
|
|
@@ -20,9 +20,9 @@ export const useTutorials = () => {
|
|
|
20
20
|
const { allTourSteps, setCurrentTourSteps, setIsTourOpen } = useUI();
|
|
21
21
|
const { addNotification } = useNotificationContext();
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
const
|
|
23
|
+
// Ref pour "verrouiller" la vérification d'une étape spécifique et éviter les notifications en double
|
|
24
|
+
// à cause des re-renders rapides (race condition).
|
|
25
|
+
const completingStepRef = useRef(null);
|
|
26
26
|
|
|
27
27
|
const { data: tutorials, isLoading: isLoadingTutorials } = useQuery('tutorials', async () => {
|
|
28
28
|
return tutorialsConfig.map(t => ({ ...t, _id: t.id }));
|
|
@@ -119,102 +119,99 @@ export const useTutorials = () => {
|
|
|
119
119
|
updateActiveTutorial(newActiveState);
|
|
120
120
|
launchTour(firstStage.tourName);
|
|
121
121
|
};
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
const triggerTutorialCheck = useCallback(async () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
// S'il n'y a pas de tutoriel actif, on ne fait rien.
|
|
125
|
+
if (!me?.activeTutorial || !tutorials) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { id: currentTutorialId, stage: currentStageNumber } = me.activeTutorial;
|
|
130
|
+
const stepIdentifier = `${currentTutorialId}-${currentStageNumber}`;
|
|
131
|
+
|
|
132
|
+
// --- VERROUILLAGE (LOCK) ---
|
|
133
|
+
// Si on est déjà en train de traiter cette étape exacte, on ignore les appels suivants
|
|
134
|
+
// pour éviter les notifications multiples.
|
|
135
|
+
if (completingStepRef.current === stepIdentifier) {
|
|
136
|
+
console.log(`[Tutoriels] Vérification pour l'étape ${stepIdentifier} déjà en cours. On ignore.`);
|
|
127
137
|
return;
|
|
128
138
|
}
|
|
129
|
-
if (!me?.activeTutorial || !tutorials) return;
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
const tutorial = tutorials.find(t => t._id === currentTutorialId);
|
|
141
|
+
const stageConfig = tutorial?.stages.find(s => s.stage === currentStageNumber);
|
|
142
|
+
|
|
143
|
+
if (!stageConfig) return; // Pas d'étape active à vérifier
|
|
133
144
|
|
|
134
145
|
try {
|
|
135
|
-
|
|
146
|
+
// On pose le verrou pour cette étape
|
|
147
|
+
completingStepRef.current = stepIdentifier;
|
|
148
|
+
console.log(`[Tutoriels] Verrouillage et vérification de l'étape ${stepIdentifier}`);
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
const tutorial = tutorials.find(t => t._id === currentActiveState.id);
|
|
139
|
-
if (!tutorial) break;
|
|
150
|
+
const { isCompleted } = await checkCompletionCondition(stageConfig.completionCondition);
|
|
140
151
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
console.log('[Tutoriels] Fin du tutoriel, attribution des récompenses.');
|
|
144
|
-
claimRewards(tutorial._id);
|
|
145
|
-
updateActiveTutorial(null);
|
|
146
|
-
setIsTourOpen(false);
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
152
|
+
if (isCompleted) {
|
|
153
|
+
console.log(`[Tutoriels] Étape ${currentStageNumber} complétée.`);
|
|
149
154
|
|
|
150
|
-
const
|
|
155
|
+
const titleKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.name`;
|
|
156
|
+
const descriptionKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.description`;
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
addNotification({
|
|
159
|
+
title: t(titleKey, stageConfig.name),
|
|
160
|
+
message: t(descriptionKey, stageConfig.description),
|
|
161
|
+
status: 'success',
|
|
162
|
+
id: `tuto-step-${stepIdentifier}` // ID unique pour la notification
|
|
163
|
+
});
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
// On passe à l'étape suivante (ou on termine le tutoriel)
|
|
166
|
+
const nextStageNumber = currentStageNumber + 1;
|
|
167
|
+
const nextStage = tutorial.stages.find(s => s.stage === nextStageNumber);
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
status: 'success'
|
|
163
|
-
});
|
|
164
|
-
currentActiveState = { id: currentActiveState.id, stage: currentActiveState.stage + 1 };
|
|
169
|
+
if (nextStage) {
|
|
170
|
+
updateActiveTutorial({ id: currentTutorialId, stage: nextStageNumber });
|
|
171
|
+
launchTour(nextStage.tourName);
|
|
165
172
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
launchTour(stageConfig.tourName);
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
173
|
+
claimRewards(tutorial._id);
|
|
174
|
+
updateActiveTutorial(null);
|
|
175
|
+
setIsTourOpen(false);
|
|
172
176
|
}
|
|
177
|
+
} else {
|
|
178
|
+
console.log(`[Tutoriels] Étape ${currentStageNumber} non complétée.`);
|
|
173
179
|
}
|
|
174
180
|
} catch (error) {
|
|
175
|
-
console.error("[Tutoriels] Erreur lors de la vérification
|
|
181
|
+
console.error("[Tutoriels] Erreur lors de la vérification de l'étape :", error);
|
|
176
182
|
} finally {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Si une vérification a été mise en attente, on la lance maintenant.
|
|
181
|
-
if (isCheckQueuedRef.current) {
|
|
182
|
-
console.log('[Tutoriels] Lancement de la vérification mise en file d\'attente.');
|
|
183
|
-
isCheckQueuedRef.current = false;
|
|
184
|
-
triggerTutorialCheck();
|
|
185
|
-
}
|
|
183
|
+
// On libère le verrou
|
|
184
|
+
completingStepRef.current = null;
|
|
185
|
+
console.log(`[Tutoriels] Libération du verrou pour l'étape ${stepIdentifier}`);
|
|
186
186
|
}
|
|
187
|
-
}, [
|
|
188
|
-
tutorials, checkCompletionCondition, claimRewards, updateActiveTutorial,
|
|
189
|
-
addNotification, launchTour, t, setIsTourOpen, me
|
|
190
|
-
]);
|
|
187
|
+
}, [me, tutorials, checkCompletionCondition, updateActiveTutorial, claimRewards, addNotification, launchTour, t, setIsTourOpen]);
|
|
191
188
|
|
|
192
189
|
useEffect(() => {
|
|
193
190
|
setMe(prevMe => ({ ...prevMe, activeTutorial: activeTuto }));
|
|
194
191
|
}, [activeTuto]);
|
|
195
192
|
|
|
193
|
+
const handleDataChange = useCallback(async (payload) => {
|
|
194
|
+
if (me?.activeTutorial) {
|
|
195
|
+
console.log(`[Tutoriels] Événement de données reçu, déclenchement de la vérification.`, payload);
|
|
196
|
+
triggerTutorialCheck();
|
|
197
|
+
}
|
|
198
|
+
}, [me?.activeTutorial, triggerTutorialCheck]);
|
|
199
|
+
|
|
196
200
|
// Écoute les événements de modification de données pour vérifier la progression en arrière-plan.
|
|
197
201
|
useEffect(() => {
|
|
198
|
-
const handleDataChange = async (payload) => {
|
|
199
|
-
console.log(`[Tutoriels] Événement de données reçu.`, payload);
|
|
200
|
-
if (me?.activeTutorial) {
|
|
201
|
-
triggerTutorialCheck();
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
console.log(me.activeTutorial + new Date().getMilliseconds());
|
|
206
202
|
const eventTypes = ['API_ADD_DATA', 'API_EDIT_DATA', 'API_DELETE_DATA'];
|
|
207
203
|
eventTypes.forEach(type => Event.Listen(type, handleDataChange, "custom", "data"));
|
|
208
204
|
|
|
209
205
|
return () => {
|
|
210
206
|
eventTypes.forEach(type => Event.RemoveCallback(type, handleDataChange, "custom", "data"));
|
|
211
207
|
};
|
|
212
|
-
}, [
|
|
208
|
+
}, [handleDataChange]);
|
|
213
209
|
|
|
214
210
|
// Vérifie la progression lorsqu'un tutoriel devient actif ou que son étape change.
|
|
215
211
|
useEffect(() => {
|
|
216
212
|
triggerTutorialCheck();
|
|
217
|
-
},[]);
|
|
213
|
+
},[me?.activeTutorial]); // Se déclenche quand le tutoriel/étape change
|
|
214
|
+
|
|
218
215
|
return {
|
|
219
216
|
tutorials,
|
|
220
217
|
isLoadingTutorials,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {useMutation, useQuery} from 'react-query';
|
|
2
|
+
import { debounce } from '../../../src/core.js';
|
|
3
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates a field on the server.
|
|
7
|
+
* @param {object} api - The API client instance.
|
|
8
|
+
* @param {object} payload - The validation payload.
|
|
9
|
+
* @param {string} payload.model - The model name.
|
|
10
|
+
* @param {object} payload.data - The field and value to validate, e.g., { email: '...' }.
|
|
11
|
+
* @param {string} [payload.contextId] - The ID of the document being edited.
|
|
12
|
+
* @returns {Promise<any>}
|
|
13
|
+
*/
|
|
14
|
+
const validateFieldOnServer = async (api, payload) => {
|
|
15
|
+
return fetch('/api/data/validate', { method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const useRealtimeValidation = (modelName, docId) => {
|
|
23
|
+
|
|
24
|
+
const [validationState, setValidationState] = useState({}); // { fieldName: { status, error } }
|
|
25
|
+
|
|
26
|
+
const mutation = useMutation(
|
|
27
|
+
(payload) => validateFieldOnServer(api, payload),
|
|
28
|
+
{
|
|
29
|
+
onSuccess: (data, variables) => {
|
|
30
|
+
const fieldName = Object.keys(variables.data)[0];
|
|
31
|
+
setValidationState(prev => ({
|
|
32
|
+
...prev,
|
|
33
|
+
[fieldName]: { status: 'valid', error: null }
|
|
34
|
+
}));
|
|
35
|
+
},
|
|
36
|
+
onError: (error, variables) => {
|
|
37
|
+
const fieldName = Object.keys(variables.data)[0];
|
|
38
|
+
setValidationState(prev => ({
|
|
39
|
+
...prev,
|
|
40
|
+
[fieldName]: { status: 'invalid', error: error.response?.data?.error || 'Validation failed' }
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const debouncedValidate = useMemo(
|
|
47
|
+
() => debounce((field, value) => {
|
|
48
|
+
if (value === '' || value === null || value === undefined) {
|
|
49
|
+
setValidationState(prev => ({ ...prev, [field]: { status: 'idle', error: null } }));
|
|
50
|
+
mutation.reset();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setValidationState(prev => ({ ...prev, [field]: { status: 'validating', error: null } }));
|
|
55
|
+
mutation.mutate({
|
|
56
|
+
model: modelName,
|
|
57
|
+
contextId: docId,
|
|
58
|
+
data: { [field]: value }
|
|
59
|
+
});
|
|
60
|
+
}, 500),
|
|
61
|
+
[modelName, docId, mutation]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const validate = useCallback((field, value) => {
|
|
65
|
+
if (!modelName) return; // Don't validate if not configured
|
|
66
|
+
debouncedValidate(field, value);
|
|
67
|
+
}, [debouncedValidate, modelName]);
|
|
68
|
+
|
|
69
|
+
const resetValidation = useCallback(() => {
|
|
70
|
+
setValidationState({});
|
|
71
|
+
mutation.reset();
|
|
72
|
+
}, [mutation]);
|
|
73
|
+
|
|
74
|
+
return { validate, validationState, resetValidation };
|
|
75
|
+
};
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export const websiteTranslations = {
|
|
3
3
|
fr: {
|
|
4
4
|
translation: {
|
|
5
|
+
"dashboards.addKpi": "Ajouter un indicateur KPI",
|
|
6
|
+
|
|
5
7
|
"field.geolocation": "Géolocalisation",
|
|
6
8
|
"time.minute": "Minute",
|
|
7
9
|
"time.hour": "Heure",
|
|
@@ -459,8 +461,8 @@ export const websiteTranslations = {
|
|
|
459
461
|
"email.backup.restoreRequest.content": `<p>Vous avez demandé une restauration de sauvegarde. Veuillez choisir une option :</p>
|
|
460
462
|
<p><strong>ATTENTION : Toute restauration écrasera vos données actuelles (modèles et/ou données).</strong></p>
|
|
461
463
|
<ul>
|
|
462
|
-
<li><a href="https://
|
|
463
|
-
<li><a href="https://
|
|
464
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=full">Restauration Complète (Modèles ET Données)</a></li>
|
|
465
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Restaurer UNIQUEMENT les Modèles</a></li>
|
|
464
466
|
</ul>
|
|
465
467
|
<p>Ces liens expireront dans 30 minutes.</p>`,
|
|
466
468
|
"btns.confirm": "Confirmer",
|
|
@@ -1572,8 +1574,8 @@ export const websiteTranslations = {
|
|
|
1572
1574
|
"email.backup.restoreRequest.content": `<p>You have requested a backup restoration. Please choose an option:</p>
|
|
1573
1575
|
<p><strong>WARNING: Any restoration will overwrite your current data (models and/or data).</strong></p>
|
|
1574
1576
|
<ul>
|
|
1575
|
-
<li><a href="https://
|
|
1576
|
-
<li><a href="https://
|
|
1577
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Full Restoration (Models AND Data)</a></li>
|
|
1578
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Restore ONLY the Models</a></li>
|
|
1577
1579
|
</ul>
|
|
1578
1580
|
<p>These links will expire in 30 minutes.</p>`,
|
|
1579
1581
|
"btns.confirm": "Confirm",
|
|
@@ -3048,8 +3050,8 @@ export const websiteTranslations = {
|
|
|
3048
3050
|
"email.backup.restoreRequest.content": `<p>Has solicitado una restauración de copia de seguridad. Por favor, elige una opción:</p>
|
|
3049
3051
|
<p><strong>ADVERTENCIA: Cualquier restauración sobrescribirá tus datos actuales (modelos y/o datos).</strong></p>
|
|
3050
3052
|
<ul>
|
|
3051
|
-
<li><a href="https://
|
|
3052
|
-
<li><a href="https://
|
|
3053
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Restauración completa (Modelos Y Datos)</a></li>
|
|
3054
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Restaurar SOLO los Modelos</a></li>
|
|
3053
3055
|
</ul>
|
|
3054
3056
|
<p>Estos enlaces expirarán en 30 minutos.</p>`,
|
|
3055
3057
|
"modelimporter.requires": "Requiere:",
|
|
@@ -4530,8 +4532,8 @@ export const websiteTranslations = {
|
|
|
4530
4532
|
"email.backup.restoreRequest.content": `<p>Solicitou a restauração de uma cópia de segurança. Por favor, escolha uma opção:</p>
|
|
4531
4533
|
<p><strong>ATENÇÃO: Qualquer restauração irá substituir os seus dados atuais (modelos e/ou dados).</strong></p>
|
|
4532
4534
|
<ul>
|
|
4533
|
-
<li><a href="https://
|
|
4534
|
-
<li><a href="https://
|
|
4535
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Restauração Completa (Modelos E Dados)</a></li>
|
|
4536
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Restaurar APENAS os Modelos</a></li>
|
|
4535
4537
|
</ul>
|
|
4536
4538
|
<p>Estes links expiram em 30 minutos.</p>`,
|
|
4537
4539
|
"modelimporter.requires": "Requer:",
|
|
@@ -5986,8 +5988,8 @@ export const websiteTranslations = {
|
|
|
5986
5988
|
"email.backup.restoreRequest.content": `<p>Sie haben die Wiederherstellung eines Backups angefordert. Bitte wählen Sie eine Option:</p>
|
|
5987
5989
|
<p><strong>ACHTUNG: Jede Wiederherstellung überschreibt Ihre aktuellen Daten (Modelle und/oder Daten).</strong></p>
|
|
5988
5990
|
<ul>
|
|
5989
|
-
<li><a href="https://
|
|
5990
|
-
<li><a href="https://
|
|
5991
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Vollständige Wiederherstellung (Modelle UND Daten)</a></li>
|
|
5992
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">NUR Modelle wiederherstellen</a></li>
|
|
5991
5993
|
</ul>
|
|
5992
5994
|
<p>Diese Links laufen in 30 Minuten ab.</p>`,
|
|
5993
5995
|
"modelimporter.requires": "Erfordert:",
|
|
@@ -7465,8 +7467,8 @@ export const websiteTranslations = {
|
|
|
7465
7467
|
"email.backup.restoreRequest.content": `<p>Hai richiesto il ripristino di un backup. Scegli un'opzione:</p>
|
|
7466
7468
|
<p><strong>ATTENZIONE: Qualsiasi ripristino sovrascriverà i tuoi dati attuali (modelli e/o dati).</strong></p>
|
|
7467
7469
|
<ul>
|
|
7468
|
-
<li><a href="https://
|
|
7469
|
-
<li><a href="https://
|
|
7470
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Ripristino Completo (Modelli E Dati)</a></li>
|
|
7471
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Ripristina SOLO i Modelli</a></li>
|
|
7470
7472
|
</ul>
|
|
7471
7473
|
<p>Questi link scadranno tra 30 minuti.</p>`,
|
|
7472
7474
|
"modelimporter.requires": "Richiede:",
|
|
@@ -8939,8 +8941,8 @@ export const websiteTranslations = {
|
|
|
8939
8941
|
"email.backup.restoreRequest.content": `<p>Požádali jste o obnovení ze zálohy. Vyberte prosím možnost:</p>
|
|
8940
8942
|
<p><strong>VAROVÁNÍ: Jakákoli obnova přepíše vaše aktuální data (modely a/nebo data).</strong></p>
|
|
8941
8943
|
<ul>
|
|
8942
|
-
<li><a href="https://
|
|
8943
|
-
<li><a href="https://
|
|
8944
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Kompletní obnova (Modely I Data)</a></li>
|
|
8945
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Obnovit POUZE Modely</a></li>
|
|
8944
8946
|
</ul>
|
|
8945
8947
|
<p>Tyto odkazy expirují za 30 minut.</p>`,
|
|
8946
8948
|
"modelimporter.requires": "Vyžaduje:",
|
|
@@ -10411,8 +10413,8 @@ export const websiteTranslations = {
|
|
|
10411
10413
|
"email.backup.restoreRequest.content": `<p>Вы запросили восстановление из резервной копии. Пожалуйста, выберите вариант:</p>
|
|
10412
10414
|
<p><strong>ВНИМАНИЕ: Любое восстановление перезапишет ваши текущие данные (модели и/или данные).</strong></p>
|
|
10413
10415
|
<ul>
|
|
10414
|
-
<li><a href="https://
|
|
10415
|
-
<li><a href="https://
|
|
10416
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Полное восстановление (Модели И Данные)</a></li>
|
|
10417
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Восстановить ТОЛЬКО Модели</a></li>
|
|
10416
10418
|
</ul>
|
|
10417
10419
|
<p>Эти ссылки истекают через 30 минут.</p>`,
|
|
10418
10420
|
"modelimporter.requires": "Требуется:",
|
|
@@ -11902,8 +11904,8 @@ export const websiteTranslations = {
|
|
|
11902
11904
|
"email.backup.restoreRequest.content": `<p>لقد طلبت استعادة نسخة احتياطية. يرجى اختيار خيار:</p>
|
|
11903
11905
|
<p><strong>تحذير: أي عملية استعادة ستستبدل بياناتك الحالية (النماذج و/أو البيانات).</strong></p>
|
|
11904
11906
|
<ul>
|
|
11905
|
-
<li><a href="https://
|
|
11906
|
-
<li><a href="https://
|
|
11907
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">استعادة كاملة (النماذج والبيانات)</a></li>
|
|
11908
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">استعادة النماذج فقط</a></li>
|
|
11907
11909
|
</ul>
|
|
11908
11910
|
<p>ستنتهي صلاحية هذه الروابط بعد 30 دقيقة.</p>`,
|
|
11909
11911
|
"modelimporter.requires": "يتطلب:",
|
|
@@ -13382,8 +13384,8 @@ export const websiteTranslations = {
|
|
|
13382
13384
|
"email.backup.restoreRequest.content": `<p>Du har begärt en återställning från backup. Vänligen välj ett alternativ:</p>
|
|
13383
13385
|
<p><strong>OBS: All återställning kommer att skriva över din nuvarande data (modeller och/eller data).</strong></p>
|
|
13384
13386
|
<ul>
|
|
13385
|
-
<li><a href="https://
|
|
13386
|
-
<li><a href="https://
|
|
13387
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Fullständig återställning (Modeller OCH Data)</a></li>
|
|
13388
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Återställ ENDAST Modeller</a></li>
|
|
13387
13389
|
</ul>
|
|
13388
13390
|
<p>Dessa länkar upphör att gälla om 30 minuter.</p>`,
|
|
13389
13391
|
"modelimporter.requires": "Kräver:",
|
|
@@ -14856,8 +14858,8 @@ export const websiteTranslations = {
|
|
|
14856
14858
|
"email.backup.restoreRequest.content": `<p>Έχετε αιτηθεί επαναφορά από αντίγραφο ασφαλείας. Παρακαλώ επιλέξτε μια επιλογή:</p>
|
|
14857
14859
|
<p><strong>ΠΡΟΣΟΧΗ: Οποιαδήποτε επαναφορά θα αντικαταστήσει τα τρέχοντα δεδομένα σας (μοντέλα και/ή δεδομένα).</strong></p>
|
|
14858
14860
|
<ul>
|
|
14859
|
-
<li><a href="https://
|
|
14860
|
-
<li><a href="https://
|
|
14861
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">Πλήρης Επαναφορά (Μοντέλα ΚΑΙ Δεδομένα)</a></li>
|
|
14862
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">Επαναφορά ΜΟΝΟ Μοντέλων</a></li>
|
|
14861
14863
|
</ul>
|
|
14862
14864
|
<p>Οι σύνδεσμοι θα λήξουν σε 30 λεπτά.</p>`,
|
|
14863
14865
|
"modelimporter.requires": "Απαιτείται:",
|
|
@@ -16285,8 +16287,8 @@ export const websiteTranslations = {
|
|
|
16285
16287
|
"email.backup.restoreRequest.content": `<p>درخواست بازیابی نسخه پشتیبان را دادهاید. لطفاً یک گزینه انتخاب کنید:</p>
|
|
16286
16288
|
<p><strong>هشدار: هرگونه بازیابی، دادههای فعلی شما (مدلها و/یا دادهها) را بازنویسی خواهد کرد.</strong></p>
|
|
16287
16289
|
<ul>
|
|
16288
|
-
<li><a href="https://
|
|
16289
|
-
<li><a href="https://
|
|
16290
|
+
<li><a href="https://{{host}}/backup/restore?token={{fullToken}}&username={{user}}&type=full">بازیابی کامل (هم مدلها و هم دادهها)</a></li>
|
|
16291
|
+
<li><a href="https://{{host}}/backup/restore?token={{modelsToken}}&username={{user}}&type=models">فقط بازیابی مدلها</a></li>
|
|
16290
16292
|
</ul>
|
|
16291
16293
|
<p>این لینکها پس از 30 دقیقه منقضی خواهند شد.</p>`,
|
|
16292
16294
|
"btns.confirm": "تأیید",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "data-primals-engine is a package responsible from handling large amount of data using MongoDB in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation. It also has integrated AI assistant.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"express-mongo-sanitize": "^2.2.0",
|
|
79
79
|
"express-rate-limit": "^8.0.1",
|
|
80
80
|
"express-session": "^1.18.2",
|
|
81
|
+
"handlebars": "^4.7.8",
|
|
81
82
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
82
83
|
"isolated-vm": "^4.7.2",
|
|
83
84
|
"juice": "^11.0.1",
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
"stripe": "^18.4.0",
|
|
102
103
|
"swagger-ui-express": "^5.0.1",
|
|
103
104
|
"tar": "^7.4.3",
|
|
105
|
+
"tinycolor2": "^1.6.0",
|
|
104
106
|
"uniqid": "^5.4.0",
|
|
105
107
|
"vitest": "^3.2.4",
|
|
106
108
|
"yaml": "^2.8.0"
|
package/perf/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
## Performance Test Report 1
|
|
2
|
+
|
|
3
|
+
### Test Environment
|
|
4
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
5
|
+
- **Concurrency**: Ramp-up to 50 virtual users/sec
|
|
6
|
+
- Runs on `perf-shot-search.yml` ,that simulates user basic activity on API
|
|
7
|
+
|
|
8
|
+
### Summary of Results
|
|
9
|
+
|
|
10
|
+
| Metric | Value |
|
|
11
|
+
|----------------------------|------------|
|
|
12
|
+
| Total HTTP Requests | 1,475 |
|
|
13
|
+
| Successful Responses (2xx) | 1,475 |
|
|
14
|
+
| Average Request Rate | 19/sec |
|
|
15
|
+
| Data Downloaded | 13.5 MB |
|
|
16
|
+
| Failed Scenarios | 0 |
|
|
17
|
+
|
|
18
|
+
### Response Time Analysis (in milliseconds)
|
|
19
|
+
|
|
20
|
+
| Percentile | Response Time (ms) |
|
|
21
|
+
|------------|--------------------|
|
|
22
|
+
| min | 8 |
|
|
23
|
+
| median | **36.2** |
|
|
24
|
+
| mean | 253.9 |
|
|
25
|
+
| p95 | 561.2 |
|
|
26
|
+
| p99 | 608 |
|
|
27
|
+
| max | 805 |
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Analysis & Recommendations
|
|
31
|
+
|
|
32
|
+
* **Outstanding Performance & Stability**: The final test results are excellent. The server handled **1,475 requests with 0 failures**, demonstrating exceptional stability under sustained load.
|
|
33
|
+
|
|
34
|
+
* **Bottleneck Resolved**:
|
|
35
|
+
* The **median response time is now just 36.2 milliseconds**. This is a dramatic improvement and indicates that the core API operations are extremely fast for the majority of users.
|
|
36
|
+
* The **p95 (561.2 ms)** and **p99 (608 ms)** response times remain well under one second. This shows that the application performs consistently, with very few outliers, even during peak load phases.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Performance Test Report 2
|
|
41
|
+
|
|
42
|
+
### Test Environment
|
|
43
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
44
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey including model creation, data import, and pack installation)
|
|
45
|
+
- **Concurrency**: Sustained load of 1 virtual user/sec for 60 seconds.
|
|
46
|
+
|
|
47
|
+
### Summary of Results
|
|
48
|
+
|
|
49
|
+
| Metric | Value |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Total HTTP Requests | 480 |
|
|
52
|
+
| Successful Responses (2xx) | 480 |
|
|
53
|
+
| Average Request Rate | 4/sec |
|
|
54
|
+
| Data Downloaded | 0.88 MB |
|
|
55
|
+
| **Failed Scenarios** | **0** |
|
|
56
|
+
|
|
57
|
+
### Response Time Analysis (in milliseconds)
|
|
58
|
+
|
|
59
|
+
| Percentile | Response Time (ms) |
|
|
60
|
+
|------------|--------------------|
|
|
61
|
+
| min | 7 |
|
|
62
|
+
| median | **15** |
|
|
63
|
+
| mean | 96.1 |
|
|
64
|
+
| p95 | 368.8 |
|
|
65
|
+
| p99 | 407.5 |
|
|
66
|
+
| max | 474 |
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### Analysis & Recommendations
|
|
70
|
+
|
|
71
|
+
* **Total Success & Stability**: The test is a complete success. With a 100% success rate (**0 failed scenarios** and **0 request errors**), it confirms that the previous fixes for race conditions and file upload issues are effective and robust.
|
|
72
|
+
|
|
73
|
+
* **Outstanding Performance**: The API's responsiveness is excellent.
|
|
74
|
+
* A **median response time of just 15 milliseconds** shows that the core operations are extremely fast for the majority of users.
|
|
75
|
+
* The **p99 is 407.5 ms**, meaning 99% of requests, even within a complex multi-step scenario, completed in well under half a second. This demonstrates consistent and reliable performance.
|
|
76
|
+
|
|
77
|
+
* **Conclusion**: The application is perfectly stable and highly performant under the tested load. The entire user journey, including complex operations, is handled efficiently. This provides a solid baseline for future, more intensive load tests.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Performance Test Report 3 (Scaling)
|
|
81
|
+
|
|
82
|
+
### Test Environment
|
|
83
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
84
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey)
|
|
85
|
+
- **Concurrency**: Ramped load from 1 to 2 virtual users/sec for 60 seconds.
|
|
86
|
+
|
|
87
|
+
### Summary of Results
|
|
88
|
+
|
|
89
|
+
| Metric | Value |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Total HTTP Requests | 720 |
|
|
92
|
+
| Successful Responses (2xx) | 720 |
|
|
93
|
+
| Average Request Rate | 9/sec |
|
|
94
|
+
| **Failed Scenarios** | **0** |
|
|
95
|
+
|
|
96
|
+
### Response Time Analysis (in milliseconds)
|
|
97
|
+
|
|
98
|
+
| Percentile | Response Time (ms) |
|
|
99
|
+
|------------|--------------------|
|
|
100
|
+
| min | 8 |
|
|
101
|
+
| median | **32.1** |
|
|
102
|
+
| mean | 141.4 |
|
|
103
|
+
| p95 | 772.9 |
|
|
104
|
+
| p99 | 854.2 |
|
|
105
|
+
| max | 922 |
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Analysis & Recommendations
|
|
109
|
+
|
|
110
|
+
* **Excellent Linear Scaling**: This test confirms the application's ability to scale gracefully. When doubling the load from the previous test (1 user/sec to 2 users/sec), the median and p95 response times also roughly doubled. This linear behavior is ideal and indicates a robust architecture without major bottlenecks.
|
|
111
|
+
* **Continued Stability**: With **0 failed scenarios**, the application proves its stability even as concurrency increases.
|
|
112
|
+
* **Conclusion**: The application is ready for higher loads. The performance degradation is predictable and linear, which is a sign of a very healthy system.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Performance Test Report 4 (Bottleneck Identification)
|
|
116
|
+
|
|
117
|
+
### Test Environment
|
|
118
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
119
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey)
|
|
120
|
+
- **Concurrency**: Ramped load from 1 to 4 virtual users/sec for 60 seconds.
|
|
121
|
+
|
|
122
|
+
### Summary of Results
|
|
123
|
+
|
|
124
|
+
| Metric | Value |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Total HTTP Requests | 1,200 |
|
|
127
|
+
| Successful Responses (2xx) | 1,200 |
|
|
128
|
+
| Average Request Rate | 12/sec |
|
|
129
|
+
| **Failed Scenarios** | **0** |
|
|
130
|
+
|
|
131
|
+
### Response Time Analysis (in milliseconds)
|
|
132
|
+
|
|
133
|
+
| Percentile | Response Time (ms) |
|
|
134
|
+
|------------|--------------------|
|
|
135
|
+
| min | 3 |
|
|
136
|
+
| median | **70.1** |
|
|
137
|
+
| mean | 483.9 |
|
|
138
|
+
| p95 | **2893.5** |
|
|
139
|
+
| p99 | **8024.5** |
|
|
140
|
+
| max | 8348 |
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Analysis & Recommendations
|
|
144
|
+
|
|
145
|
+
* **Bottleneck Identified**: This test successfully identified the application's first major performance bottleneck. While stability remains perfect (**0 failures**), the response times show non-linear degradation. The **p99 jumping to over 8 seconds** indicates that the system is struggling under this write-heavy load.
|
|
146
|
+
* **Database Contention**: The cause is almost certainly database contention (MongoDB). The scenario involves concurrent model creation, data import, and pack installation, which are highly resource-intensive operations.
|
|
147
|
+
* **Next Steps**: The immediate next step is to run a detailed analysis to pinpoint the exact slow queries. Monitoring server CPU/Disk I/O during the test and using MongoDB's profiler will be essential for optimization.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {uuidv4} from "../src/core.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export function setScenarioVariables(context, events, done) {
|
|
5
|
+
// context.vars is where we store variables for the virtual user's session
|
|
6
|
+
context.vars.vuId = uuidv4().replace(/-/g, '');
|
|
7
|
+
context.vars.demoUser = `perf${Math.floor(Math.random() * 9000000) + 1000000}`;
|
|
8
|
+
return done();
|
|
9
|
+
}
|
|
10
|
+
// Hook to name requests to aggregate metrics by endpoint
|
|
11
|
+
export function metricsByEndpoint_beforeRequest(requestParams, context, ee, next) {
|
|
12
|
+
// Remove query parameters for the metric name
|
|
13
|
+
const urlPath = requestParams.url.split('?')[0];
|
|
14
|
+
|
|
15
|
+
// Replace unique IDs (ObjectId or UUID) in the URL with a placeholder for aggregation
|
|
16
|
+
// Example: /api/data/60d0fe4f5311236168a109ca -> /api/data/:id
|
|
17
|
+
const name = urlPath.replace(/([a-fA-F0-9]{24}|[a-fA-F0-9-]{36})/g, ':id');
|
|
18
|
+
|
|
19
|
+
// Emit an internal Artillery event to name the request
|
|
20
|
+
ee.emit('request:set_name', name);
|
|
21
|
+
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Hook pour logger les erreurs de manière détaillée
|
|
26
|
+
export function metricsByEndpoint_afterResponse(requestParams, response, context, ee, next) {
|
|
27
|
+
// On vérifie si le statut est un code d'erreur (4xx ou 5xx)
|
|
28
|
+
if (response.statusCode >= 400) {
|
|
29
|
+
console.error(`\n[ERREUR] Requête ${requestParams.method} ${requestParams.url} a échoué avec le statut ${response.statusCode}`);
|
|
30
|
+
// On log le corps de la réponse, qui contient souvent le message d'erreur du serveur
|
|
31
|
+
if (response.body) {
|
|
32
|
+
console.error(` -> Réponse: ${response.body}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// On passe à la suite du scénario
|
|
36
|
+
return next();
|
|
37
|
+
}
|