Référence

Chaque outil, en un seul endroit

Cette page est la référence pratique pour chaque outil dans la barre latérale de votre environnement Archestack. Chaque section couvre ce que fait l'outil, quand y recourir, les étapes pour les workflows les plus courants et les pièges qui attrapent les nouveaux utilisateurs. La dernière section (APIs appelables depuis un script) documente la surface exacte que les scripts peuvent appeler.

Schema Designer

Un éditeur visuel pour le schéma de base de données, tables, colonnes, types, relations, indexes, qui s'enregistre automatiquement dans un document JSON. Le schéma est la source de vérité pour votre application ; aucun autre outil ne voit une colonne tant que vous ne l'ajoutez pas ici et ne déployez pas.

Workflow courant : dans la barre d'outils, cliquez sur Add Table (bouton-icône). Une boîte de dialogue intitulée "Create New Table" s'ouvre ; tapez un nom (le texte d'aide rappelle "Lowercase with underscores recommended"), cliquez sur Create. La table apparaît sur le canevas avec une clé primaire id SERIAL auto-générée. Sélectionnez la table, le panneau de droite s'ouvre sur l'onglet Columns. Ajoutez des colonnes depuis la boîte "Add column" à bordure pointillée en bas (champ nom, liste déroulante Type, cliquez sur Add Column). Cliquez sur chaque colonne pour la développer et ajuster les commutateurs (PK, NULL, UQ), Length et Default value.

Types de colonne disponibles dans la liste déroulante Type : SERIAL, BIGSERIAL, INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, REAL, DOUBLE PRECISION, VARCHAR, CHAR, TEXT, BOOLEAN, DATE, TIME, TIMESTAMP, TIMESTAMPTZ, UUID, JSON, JSONB, BYTEA, INET, CIDR, MACADDR, MONEY, INTERVAL, plus les types géométriques et tableau. Length n'est pertinent que pour VARCHAR et CHAR.

Les clés étrangères résident dans l'onglet Relations du panneau de droite (pas "Relationships"). Faites défiler jusqu'à "Add relationship" : choisissez la colonne FK sur la table courante, le type de relation (One-to-One / One-to-Many / Many-to-Many), la table référencée, la colonne référencée, et cliquez sur Add Relationship. Chaque relation possède des listes déroulantes On Delete et On Update (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION).

Auto-save : l'indicateur en bas à gauche bascule entre "Auto saving..." et "Saved". Il y a aussi un bouton Save dans la barre d'outils (activé uniquement quand il y a des modifications en attente) pour les sauvegardes explicites.

Autres boutons de la barre d'outils : Add Group (conteneurs visuels pour les tables liées, panneau de propriétés intitulé "Group"), Add Text (annotations libres, panneau de propriétés intitulé "Text Annotation"), contrôles de zoom, filtre par package, recherche de table.

Pièges : renommer une colonne produit un plan destructif (drop + add), modifiez le SQL dans l'onglet Generated SQL du déploiement pour utiliser RENAME COLUMN si vous voulez préserver les données. Ajouter une colonne NOT NULL à une table non vide sans valeur par défaut échoue, rendez-la nullable, remplissez, puis altérez. Le bouton Deploy de la barre d'outils vous amène à une page de configuration de déploiement, pas directement dans un déploiement.

Database Deployments

Liste chaque déploiement avec son statut (Draft / Executing / Succeeded / Failed) et en exécute de nouveaux. Route : /database-deployments/overview.

Boutons en haut à droite : Refresh (recharger la liste), New Deployment (créer un nouveau brouillon).

Depuis Schema Designer : le bouton vert Deploy avec une icône de fusée dans la barre d'outils du Schema Designer vous amène à /database-deployments/configure/new, une page de configuration de déploiement fraîche.

Page de configuration de déploiement : deux onglets.

  • Configuration - éditeurs de scripts pré/post-déploiement (PostgreSQL) avec un bouton Test Run chacun. Le pré-script s'exécute avant les modifications de schéma ; le post-script après. Utiles pour les remplissages, les reconstructions d'index ou le nettoyage de zone de transit. Plus un basculeur Force Deploy (Allow Data Loss) pour les opérations destructives.
  • Generated SQL - affiche la migration produite par la plateforme. Le SQL est éditable. Un bouton Regenerate (icône rafraîchir) relance le diff si vous avez fait des modifications de schéma depuis l'ouverture de cette page.

Boutons du haut : Back, Save (enregistre la configuration en Draft), Deploy (exécute le déploiement immédiatement). Deploy est ce sur quoi vous cliquez en dernier.

Pièges : les déploiements ne sont pas automatiquement enveloppés dans une transaction, un échec à mi-chemin laisse la base dans un état partiel. Pour les migrations risquées, enveloppez vos instructions dans BEGIN; ... COMMIT; dans le pré-script. Le basculeur Force Deploy contourne les vérifications de sécurité, utilisez-le délibérément.

Business Entities

Vues organisées construites sur une table maître plus des jointures depuis des tables liées. Les pages se lient aux Business Entities, jamais aux tables brutes. Voir Concepts -> Business Entities pour le pourquoi.

Workflow courant : cliquez sur Create. L'éditeur pleine page s'ouvre avec les onglets Visual, JSON, Events. Définissez Entity Name, Master Table (Autocomplete), et Label Column (la colonne dont la valeur représente un enregistrement dans les sélecteurs). Enregistrez, le BE auto-génère une liste de colonnes natives à partir de la table maître.

Ajout de jointures : faites défiler jusqu'à la carte Join Configurator et cliquez sur Add Join. Chaque jointure s'ouvre comme un Accordion avec : Join Type (INNER / LEFT / RIGHT), From Table, To Table (Autocomplete groupé par Database Tables / Third Party), From Column, To Column, listes déroulantes optionnelles Type Cast, et un sélecteur de colonnes pour les colonnes cibles à exposer.

Agrégations : sur une jointure, basculez Aggregate Mode. Pour chaque colonne choisie vous choisissez ensuite une Aggregate Function : COUNT / SUM / AVG / MIN / MAX / COUNT DISTINCT. "Nombre de contacts ouverts sur cette Company" est l'exemple canonique.

Run Preview : l'éditeur a un bouton Run Preview (avec une icône play) qui exécute la configuration de jointure complète et affiche de vraies lignes dans une grille de données. Si une colonne de jointure revient vide, la colonne n'a pas été cochée dans le sélecteur de colonnes de la jointure (ou la relation est mal configurée).

Plusieurs BE par table maître : rien ne vous empêche d'avoir customer (sales view) et customer (support view) sur la même table maître. Pages différentes, colonnes adaptées à des publics différents, même ligne sous-jacente.

Pièges : les colonnes jointes et agrégées sont en lecture seule. Pour éditer le libellé joint, naviguez vers l'enregistrement source. Les colonnes agrégées sont recalculées à chaque requête, acceptable pour des centaines de lignes, lent sur des millions.

Page Editor

Configure l'UI d'exécution en liant une Page à un Business Entity. La plateforme auto-génère des sections en fonction des colonnes du BE ; vous affinez la disposition à partir de là. Il n'y a pas de "sélecteur de template" séparé, chaque page commence à partir de la même baseline générée et vous la personnalisez.

Workflow courant : cliquez sur Create. Remplissez Page Name, Page Route (par exemple /companies), choisissez le Business Entity (Autocomplete). La page s'ouvre dans l'éditeur avec les onglets Visual, Overview, Create, Entities, Events, JSON. Affinez la disposition auto-générée, puis basculez le switch Published dans l'en-tête supérieur sur ON.

Types de widget de champ (le Select Type sur chaque champ) : Text, Textarea, Number, Date, Select, Checkbox, Email. Utilisez Select pour les champs de clé étrangère et réglez l'autocomplete Entity du champ sur le BE référencé pour que les utilisateurs choisissent par libellé.

Onglets dans le formulaire de détail : cliquez sur Add Tab dans l'onglet Visual. À l'intérieur d'un onglet vous pouvez ajouter une section RelatedGrid liée à un autre BE filtré par la PK de l'enregistrement courant. Exemple classique : une page Company avec les onglets "Contacts" et "Deals", chacun filtré par company_id = id de la company courante.

Boutons d'action dans l'inspecteur Header du formulaire de détail. Cliquez sur Add dans la section Actions. Chaque action a Label, Icon (Save / Delete / Add / Refresh / Download / Bolt / None), Variant (Contained / Outlined / Text), Color (Primary / Secondary / Error / Success / Warning / Info) et Steps (une liste ordonnée de : Save Record, Delete Record, Navigate, Business Events, Custom). Pour exécuter un Business Event depuis un bouton, réglez le type de step sur Business Events et choisissez l'event par nom.

Publication : le switch Published dans l'en-tête supérieur est le seul contrôle de publication. ON = la page apparaît sous Published Pages dans la barre latérale et les utilisateurs finaux qui naviguent vers sa route la voient. OFF = brouillon (seuls vous dans l'éditeur voyez vos modifications).

Pièges : les colonnes auto-générées sur une Page fraîche reflètent le BE, si le BE change (vous ajoutez une jointure), la Page ne récupérera pas automatiquement les nouvelles colonnes. Rechargez la page dans l'éditeur ou ajoutez la colonne manuellement à la section. Il n'y a actuellement pas de type de step d'action "Generate PDF", pour câbler un PDF à un bouton de page, exposez le PDF via un Script Module qui appelle l'endpoint REST et utilisez un step Custom (ou appelez l'endpoint directement depuis un Frontend Template).

Business Events

Règles qui se déclenchent sur les changements de données (ou selon un calendrier, ou manuellement) et exécutent des actions quand les conditions sont satisfaites. Le mécanisme "quand X arrive, fais Y" de la plateforme. Voir Concepts -> Business Events pour le modèle.

Workflow courant : cliquez sur Create. Définissez Rule Name, basculez Enabled. Choisissez Business Entity. Cochez un ou plusieurs Triggers : BeforeCreate / BeforeUpdate / AfterCreate / AfterUpdate / BeforeDelete / OnSchedule / Manual / InitialValue. Construisez l'arbre de conditions (groupes AND/OR). Ajoutez une ou plusieurs actions dans le FlowCanvas. Enregistrez.

Types d'action (RuleActionKind) :

  • Execute Script - exécute un script C#. L'action la plus flexible ; vous y avez recours dès que vous voulez définir un champ, faire un calcul, appeler une API, n'importe quoi de sur-mesure. Le script reçoit Entity (mutez-le sur les triggers Before* pour modifier l'écriture en cours), OldEntity, Log, Db, Modules.
  • Validate - un script qui retourne bool. true signifie que la validation échoue et la sauvegarde est bloquée avec le message d'erreur configuré. Met optionnellement en évidence certaines colonnes via errorColumns.
  • Block Operation - arrête net l'opération avec un message configuré. Pas de script.
  • Create Entity - insère un enregistrement dans un autre BE. Les valeurs de champ prennent en charge les expressions de template comme {{ Entity.column_name }} et {{ now() }}.
  • Update Entity - met à jour les enregistrements dans un autre BE correspondant à un filtre de condition. Mêmes expressions de template.
  • Delete Entity - supprime les enregistrements correspondant à une condition. Refuse de se déclencher sans condition (sécurité).
  • Send Email / Send Webhook / Publish Event - définis dans le schéma comme améliorations futures ; actuellement journalisés et ignorés.

Schéma "Set a field" : il n'y a pas d'action discrète Set Field. La façon de définir un champ est un Execute Script sur un trigger Before* qui mute Entity. Exemple : Entity.title = ((string)Entity.title)?.Trim();. La plateforme persiste l'Entity modifiée dans le cadre de l'écriture en cours, pas de requête supplémentaire, pas de risque de récursion.

Run Simulation : l'onglet Simulate de l'éditeur de trigger a un sélecteur d'enregistrement et un bouton Run Simulation. La plateforme exécute vos conditions et actions à l'encontre de l'enregistrement choisi sans persister les modifications : les écritures sont vérifiées et consignées, mais jamais exécutées, celles d'un script via Db et Files comprises, donc rien n'atteint la base de données ni les fichiers de l'espace de travail et aucun autre Business Event ne se déclenche. Les mails et les notifications ne sont pas envoyés. Tout ce qu'un script fait d'autre par lui-même est réel : les appels HTTP, System.IO, et tout ce que fait un paquet NuGet, comme une connexion à une autre base de données. Le panneau de sortie montre quelles conditions ont matché et ce que chaque action aurait fait. Utilisez ceci pour attraper les mauvaises configurations avant qu'elles ne frappent les données réelles.

Expressions de template dans la configuration d'action utilisent les accolades {{ ... }}. Les jetons réels : {{ Entity.column }}, {{ OldEntity.column }}, {{ now() }} / {{ getdate() }}, {{ today() }}, {{ guid() }} / {{ newid() }}, {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}, des agrégats comme {{ SUM(column) }} dans les contextes de jointure, et de l'arithmétique post-substitution comme {{ Entity.quantity * Entity.price }}. Il n'y a pas de jeton {{ user.email }}.

Opérateurs de condition : Equals, NotEquals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, Like (= ILIKE), Contains (= ILIKE %value%), IsNull, IsNotNull, In (= ANY(...)).

Pièges : un trigger qui mute le même enregistrement qu'il surveille se redéclenchera si vous utilisez Update Entity ; utilisez plutôt Before Update + Execute Script + Entity.field = .... Deux triggers sur le même event se déclenchent par ordre de priorité. Les events désactivés apparaissent toujours dans la liste, le basculeur Enabled est séparé de la configuration du trigger.

Script Modules

Scripts C# réutilisables compilés à l'exécution par Roslyn. Prennent des paramètres, interrogent la base de données via le helper Db, retournent une valeur. Appelables depuis les Business Events, les Scheduled Events et directement depuis le front-end.

Workflow courant : cliquez sur Create. L'éditeur s'ouvre avec les onglets Edit, Parameters, Test, JSON. Nommez le module (PascalCase, par exemple RecalcOpenRevenue). Dans l'onglet Parameters, cliquez sur Add par paramètre (Name, Type, Required, Default Value). Options de Type : string, int, decimal, double, bool, DateTime. Retour à l'onglet Edit, écrivez du C#. IntelliSense est activé.

Panneau Test : l'onglet Test affiche vos saisies de paramètres à gauche et un bouton Run (icône PlayArrow). Cliquez sur Run ; le côté droit affiche un indicateur de succès ou d'erreur, le log de sortie et la valeur de retour (formatée en JSON).

Le helper Db, voir la référence complète dans APIs appelables depuis un script plus bas.

Appel depuis un Business Event : ajoutez une action Execute Script dont le corps appelle le module :

// The module is a method; its name and parameters are checked when the script compiles
await Modules.RecalcCompanyOpenDealValue(company_id: (int)Entity.company_id);

// Calling it by name still works
await Modules.CallAsync("RecalcCompanyOpenDealValue", new Dictionary<string, object?> {
    ["company_id"] = Entity.company_id
});

Appel depuis un Scheduled Event : créez un event avec le trigger On Schedule, définissez le cron, ajoutez une action Execute Script avec le même corps Modules.CallAsync.

Journalisation : chaque invocation écrit une entrée dans Event Logs avec le temps d'exécution, les paramètres et le résultat (ou l'erreur).

Pièges : toujours await les appels Db, l'oublier compile mais retourne une Task. Les retours sont passés par objet (pas sérialisés JSON par le framework) ; pour la consommation côté front-end, retournez des objets anonymes avec des champs primitifs. L'acceptation sur caractère de validation est intentionnellement désactivée dans l'éditeur ; utilisez Tab pour accepter.

Shared Code

Du code dont plusieurs scripts ou pages ont besoin, rangé dans des fichiers à part. Un fichier est nommé par son chemin, comme Sales/OrderSummary.cs ou components/StatusChip.tsx, et son extension indique ce qu'il est : .cs est du C# que chaque script peut utiliser, tandis que .tsx, .ts, .jsx et .js sont des modules qu'importent les pages, les Frontend Templates et les blocs Code des layouts. Les fichiers partagés appartiennent à des packages comme n'importe quel autre artefact.

Workflow courant : dans la barre latérale, sous Logic, ouvrez Shared Code et cliquez sur New file. Tapez le chemin ; l'éditeur pré-remplit le fichier avec un exemple correspondant à son type. Écrivez le code et cliquez sur Save.

Les fichiers C# sont du C# ordinaire : espaces de noms, records, classes et helpers statiques. Chaque script (Script Modules, Business Events, Scheduled Events, data scripts PDF et tests) peut utiliser leurs types publics. Un type sans espace de noms est disponible par son nom ; un type placé dans un espace de noms demande un using dans le script, ou un global using dans n'importe quel fichier partagé, qui s'applique alors à tous les scripts. Un fichier partagé voit le modèle de données typé (Order, WorkspaceDb) mais pas les variables globales d'un script, donc un helper qui a besoin de Db ou de Log le reçoit en argument.

// Shared code: Sales/OrderSummary.cs, ordinary C# every script can use
namespace Sales;

/// <summary>One order as the invoices and the dashboard report it.</summary>
public record OrderSummary(int Id, string Number, decimal Total);

public static class Vat
{
    public static decimal Add(decimal amount, decimal rate = 0.21m) =>
        Math.Round(amount * (1 + rate), 2);
}
// A script module using it
using Sales;

var open = await Db.Order.Where(o => o.Status == "Open").ToListAsync();
return open.Select(o => new OrderSummary(o.Id, o.Number, Vat.Add(o.Total ?? 0))).ToList();

Une modification enregistrée atteint chaque script à sa prochaine exécution. L'éditeur vérifie un fichier partagé conjointement avec les autres fichiers partagés, et Go to Definition sur un type partagé dans un script ouvre le fichier qui le déclare. Quand le code partagé ne compile pas, les scripts qui l'utilisent échouent en nommant le fichier fautif ; les scripts qui ne l'utilisent pas continuent de s'exécuter. Une modification de table qui casserait un fichier partagé est signalée, avec les scripts qu'elle casse, avant que le déploiement ne s'exécute.

Les modules s'importent depuis @/ suivi de leur chemin sans l'extension : components/StatusChip.tsx s'importe avec import { StatusChip } from "@/components/StatusChip". Une page peut aussi avoir ses propres fichiers à côté de ses vues, ajoutés avec + au-dessus de son éditeur de code et importés sous la forme ./OrderRow. Un module voit les mêmes variables globales que le code des pages et exporte ce que les autres utilisent. Une vue qui importe peut se terminer par l'export par défaut de son composant au lieu d'un return. Le code sans import ni export s'exécute exactement comme il l'a toujours fait.

// Shared code: components/StatusChip.tsx, a module pages import
export function StatusChip({ status }: { status: string }) {
  const color = status === "Open" ? "warning" : status === "Paid" ? "success" : "default";
  return <Chip size="small" color={color} label={status} />;
}
// A page's overview, with a file of its own, OrderRow.tsx, beside it
import { StatusChip } from "@/components/StatusChip";
import { OrderRow } from "./OrderRow";

export default function Orders() {
  const [rows, setRows] = useState([]);
  useEffect(() => {
    backend.getBusinessEntityData("Order").then((r) => setRows(r.rows.items));
  }, []);
  return (
    <Stack spacing={1}>
      {rows.map((row) => (
        <OrderRow key={row.id} row={row}>
          <StatusChip status={row.orders_status} />
        </OrderRow>
      ))}
    </Stack>
  );
}

Les paquets que le code peut importer sont ceux que l'application embarque déjà, et un import est le même objet que celui que contiennent les variables globales de la page : react, @mui/material, @mui/x-date-pickers, dayjs, lucide-react, three, @react-three/fiber et @react-three/drei. Aucun autre paquet n'est disponible.

Publication : l'application publiée lit les modules partagés tels que la release courante les a figés, donc une modification atteint les utilisateurs finaux à la prochaine publication, comme pour une page. Un visiteur non connecté ne reçoit que les modules qu'importent les pages publiques et les layouts publics.

Pièges : le chemin est l'identité d'un fichier dans chaque espace de travail, et les imports le nomment, donc renommer un fichier casse ce qui l'importe jusqu'à ce que ces imports soient mis à jour. Les chemins utilisent des lettres, des chiffres, des points, des traits de soulignement et des traits d'union. L'espace de noms Archestack est réservé. Un alias dans un global using (global using Money = decimal;) n'est pas transmis aux scripts.

NuGet Packages

Des bibliothèques NuGet pour les scripts C# de l'espace de travail : un pilote pour une autre base de données, un lecteur CSV ou Excel, un client pour un service. Chaque script peut les utiliser (Script Modules, scripts des Business Events, Scheduled Events, data scripts PDF, scripts de test et vérifications de code), tout comme les fichiers .cs partagés. L'éditeur de script et VS Code complètent leurs types. Un espace de travail qui n'en ajoute aucune exécute ses scripts exactement comme avant.

Workflow courant : dans la barre latérale, sous Logic, ouvrez NuGet Packages et cliquez sur Add NuGet package. Recherchez le paquet, ou tapez son id exact quand la source ne permet pas la recherche, et choisissez une version exacte : la dernière version stable est présélectionnée, et Include prerelease versions liste les autres. Choisissez dans Belongs to à quoi il appartient : l'un des packages de l'espace de travail, ou Workspace only. Add le télécharge d'abord sur le serveur avec ses dépendances. Quand cela échoue, la boîte de dialogue en indique la raison et rien n'est enregistré ; quand cela fonctionne, elle affiche les espaces de noms qu'il apporte.

Dans un package : un paquet NuGet qui appartient à un package voyage avec lui, dans son export et dans ce qu'un pipeline déploie, et l'installation d'un package liste les paquets NuGet qu'elle ajoute ou dont elle relève la version avant que quoi que ce soit ne change. Dans un dépôt, ils se trouvent dans packages/<package>/nuget.json, écrit seulement quand le package en déclare, et VS Code vérifie le fichier au fil de la frappe. Un push qui modifie le fichier les télécharge d'abord, et il est refusé, sans rien changer, quand ils ne peuvent pas être restaurés.

{
  "packages": [
    { "id": "CsvHelper", "version": "33.0.1" },
    { "id": "Dapper", "version": "2.1.35" },
    { "id": "MySqlConnector", "version": "2.4.0" }
  ]
}

Utilisation : un paquet NuGet apporte des types, jamais une variable globale, et rien n'est importé pour vous. Écrivez un using pour son espace de noms, qui n'est souvent pas son id : la colonne Namespaces de la page affiche les espaces de noms de chaque paquet NuGet sous forme de lignes using à copier. Retournez depuis un script des valeurs simples, pas les objets propres à la bibliothèque.

// NuGet package: CsvHelper. Reads a price list an admin uploaded on the Files page.
using System.Globalization;
using System.IO;
using CsvHelper;

var text = await Files.ReadTextAsync("imports/prices.csv");
var csv = new CsvReader(new StringReader(text), CultureInfo.InvariantCulture);
var rows = new List<object>();
try
{
    csv.Read();
    csv.ReadHeader();
    while (csv.Read())
        rows.Add(new { Sku = csv.GetField("sku"), Price = csv.GetField<decimal>("price") });
}
finally
{
    csv.Dispose();
}

// Plain rows, not CsvHelper objects.
return rows;
// NuGet packages: MySqlConnector and Dapper. A script module that lists the open invoices in
// the old ERP's MySQL database. The connection string is a file an admin uploads on the Files
// page of each environment: a password written in a script travels to git, package exports
// and every environment.
using Dapper;
using MySqlConnector;

var connectionString = await Files.ReadTextAsync("config/erp-connection.txt");
var connection = new MySqlConnection(connectionString.Trim());
try
{
    await connection.OpenAsync();
    var invoices = await connection.QueryAsync(
        "SELECT number, customer_number, amount FROM invoices WHERE paid = 0 ORDER BY number");

    // Plain values, not driver objects.
    return invoices
        .Select(i => new { Number = (string)i.number, Customer = (string)i.customer_number, Amount = (decimal)i.amount })
        .ToList();
}
finally
{
    // A top-level using var does not parse in a script, so the connection is closed here.
    await connection.DisposeAsync();
}

Versions : une déclaration nomme une seule version exacte, comme 2.1.35, jamais une plage. Quand plusieurs packages demandent des versions différentes du même paquet NuGet, la plus élevée est utilisée par chaque script de l'espace de travail. La colonne In use indique de quelle version il s'agit et qui l'a demandée.

Quelle copie s'exécute : un espace de travail charge sa propre copie de chaque paquet NuGet, séparée de tous les autres espaces de travail du serveur, y compris quand le serveur embarque la même bibliothèque pour lui-même. .NET lui-même et les bibliothèques dont les scripts disposent déjà, comme System.Text.Json, la journalisation, EF Core et Npgsql, restent la copie du serveur. Un paquet NuGet qui demande une version plus récente de l'une d'elles reçoit un avertissement : il s'exécute sur la copie du serveur, et une méthode que seule la version plus récente possède échoue quand elle est appelée.

Restaurations : le premier script qui a besoin des paquets NuGet sur un serveur fait que ce serveur les télécharge. Le script attend jusqu'à cinq secondes ; quand le téléchargement prend plus de temps, il est invité à s'exécuter à nouveau un instant plus tard. Après une modification, l'espace de travail restaure le nouvel ensemble. Tant que cela ne fonctionne pas, les scripts gardent le dernier ensemble restauré, donc une restauration qui échoue ne retire jamais des paquets NuGet qui fonctionnaient. Le bandeau de la page affiche l'état et les erreurs d'une restauration échouée expliquées. Après un échec, le serveur réessaie la première fois qu'un script a besoin des paquets NuGet après l'heure qu'indique le bandeau, ou tout de suite avec Restore now. Resolved liste chaque paquet NuGet qu'utilisent les scripts, dépendances comprises.

Exécutions à blanc et tests : une exécution à blanc (l'onglet Test, Run Simulation) ne retient que ce qui passe par Archestack : les écritures via Db et Files sont simulées et non enregistrées, et les mails et les notifications ne sont pas envoyés. Une exécution de tests fait ses écritures via Db dans sa sandbox et retient le reste de la même façon. Tout ce que le script fait d'autre par lui-même est réel : les appels HTTP, System.IO, et tout ce que fait un paquet NuGet, comme une connexion à une autre base de données, si bien qu'une écriture via un pilote a lieu à chaque exécution. Faites pointer un tel script vers une base de données de test pendant que vous l'essayez.

Pièges :

  • Les paquets NuGet ont besoin de l'accès typé aux données, comme le C# partagé. Sur un serveur qui le désactive (Scripting:TypedData:Enabled), la page l'indique et ne permet pas d'en ajouter.
  • #r "nuget: Dapper, 2.1.35", la façon dont d'autres exécuteurs de scripts en chargent un, ne fonctionne pas ici. Ajoutez-le sur la page.
  • Une bibliothèque native qu'apporte un paquet NuGet est chargée une fois par processus serveur, partagée par les espaces de travail qui utilisent le même fichier, et reste chargée jusqu'au redémarrage du serveur.
  • Les paquets NuGet Workspace only ne sont pas exportés avec un package, ne sont pas dans le dépôt et ne sont pas déployés par les pipelines. Attribuez un paquet NuGet dont une application a besoin au package de cette application.
  • Un using var de premier niveau n'est pas valide dans un script, donc fermez vous-même une connexion, dans un finally, comme le fait l'exemple.
  • Une valeur lue depuis Entity est dynamic, et une méthode d'extension comme QueryAsync de Dapper ne peut pas en prendre une comme argument : le script ne compile pas (CS1973). Castez-la d'abord : (string)Entity.customers_number.
  • Les paquets NuGet qui sont des outils, des templates ou des SDK MSBuild sont refusés, de même qu'un paquet sans rien que les scripts puissent compiler sur .NET 8, ou un paquet qui étend le pilote PostgreSQL du serveur ou EF Core. Les ids nommés OmniCore ou Archestack, ou qui commencent par l'un d'eux suivi d'un point, appartiennent à la plateforme.

Pour les exploitants : les paquets NuGet sont activés par défaut et toujours désactivés en mode essai. Les paramètres se trouvent sous Scripting:NuGet, ou en variables d'environnement Scripting__NuGet__Enabled et ainsi de suite. Chaque serveur télécharge pour lui-même, donc chacun doit pouvoir joindre nuget.org ou les flux configurés ici, via le proxy de https_proxy, http_proxy et no_proxy quand il y en a un. Donnez à tous les serveurs d'une même installation les mêmes sources.

ParamètrePar défautEffet
EnabledtrueÀ false, rien n'est téléchargé et les scripts n'en chargent aucun. La page ne peut pas en ajouter ; les déclarations qu'apporte un import ou un push sont conservées, inutilisées.
Sourcesnuget.orgD'où viennent les paquets NuGet, séparés par ; : des flux désignés par leur index de service v3 (l'URL qui se termine par index.json), ou des dossiers absolus, d'où les paquets sont copiés et qui sont considérés comme fiables : les limites de téléchargement ne s'appliquent qu'aux flux. nuget.org n'est utilisé que quand ni ce paramètre ni Feeds ne nomme de source.
Feeds:<name>:Url, Username, PasswordAucunUn flux nommé, pour un flux qui demande des identifiants. L'URL ne contient jamais de nom d'utilisateur ni de mot de passe.
Feeds:<name>:PatternsAucunLes ids que ce flux sert, séparés par ; (Contoso.*). Dès qu'un flux a des motifs, un id vient du flux dont le motif lui correspond, et un flux sans motifs sert tout le reste.
Feeds:<name>:AllowInsecureConnectionsfalseLaisse passer un flux en http:// simple.
DirectoryUn dossier dans les données d'application localesOù ce serveur garde les paquets NuGet qu'il a téléchargés : un dossier par serveur sur son disque local, jamais un volume partagé entre serveurs. Dans un conteneur, donnez-lui un volume propre à ce serveur, sinon chaque nouveau conteneur les télécharge à nouveau.
MaxPackageBytes, MaxExtractedBytes100 Mo, 1 GoLe plus gros paquet NuGet qu'une restauration télécharge, et la taille maximale d'un paquet NuGet une fois décompressé.
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages500 Mo, 2 Go, 150Le maximum qu'une restauration télécharge, décompresse et compte en paquets NuGet, au total.
MaxCacheBytes10 GoAu-delà, le dossier n'accepte plus de nouvelles restaurations, et les paquets NuGet que ce serveur n'utilise pas sont supprimés, les moins récemment utilisés d'abord.
MinFreeDiskBytes1 GoEn dessous de cet espace disque libre, une restauration ne démarre pas, et un téléchargement qui en laisserait moins est arrêté.
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds300, 120, 5000Combien de temps une restauration peut durer, combien de temps l'ajout d'un paquet NuGet attend son téléchargement, et combien de temps un script attend le téléchargement de ce serveur avant d'être invité à s'exécuter à nouveau.
AllowNativeAssetstrueÀ false, un paquet NuGet qui contient des bibliothèques natives est refusé, et aucune n'est jamais chargée.
AuditRefuseLevelVidelow, moderate, high ou critical : un paquet NuGet présentant une vulnérabilité connue de ce niveau ou plus est refusé. Vide, aucun n'est refusé et les vulnérabilités s'affichent comme des avertissements ; toute autre valeur fait échouer chaque restauration qui ajoute un paquet NuGet.
CertificateRevocationModeonlineonline ou offline : comment la révocation des certificats de signature des paquets NuGet est vérifiée.

Frontend Templates

Fragments d'UI réutilisables écrits en TSX que le page editor peut déposer dans une page. Utile quand les templates configurés ne suffisent pas, graphiques sur-mesure, dispositions inhabituelles, intégrations de widgets tiers.

Workflow courant : cliquez sur Create, nommez le template, écrivez un composant TSX dans l'éditeur. Le composant reçoit des props : record (l'enregistrement courant lorsqu'embarqué dans un formulaire de détail), refresh (re-récupérer les données) et quelques helpers. Enregistrez. Référencez le template depuis un champ du Page Editor.

Packageable : les templates passent par le système de Packages comme tout autre objet de configuration, ils sont cascadés lorsqu'une page qui les utilise est ajoutée à un package.

Pièges : le TSX ne peut pas importer de paquets npm arbitraires : il importe du code partagé depuis @/ et les quelques paquets que l'application embarque.

PDF Templates

HTML + template Scriban + un petit script de données C# qui rend en PDF (Chromium headless). Chaque template est invocable par nom depuis un script via REST ou directement depuis un bouton sur une Page (avec un petit adaptateur, voir ci-dessous).

Workflow courant : cliquez sur + Create. L'éditeur s'ouvre avec des champs texte Name et Description en haut, cinq onglets sur le côté gauche (HTML Template, Data Script, Settings, Params, JSON) et un panneau PDF Preview toujours visible à droite. Les onglets Data Script et HTML Template ouvrent chacun un éditeur Monaco sur la moitié gauche ; l'aperçu se recompose à l'enregistrement (bouton vert Update en haut à droite).

Déclarez les paramètres dans l'onglet Params via le bouton + Add (Name, Type, Required, Default Value). Le libellé de l'onglet se met à jour en Params (n) avec le nombre. Format de page / orientation / marges / échelle / en-tête / pied de page résident dans l'onglet Settings.

Forme du data script (voir le tutoriel PDF Invoices pour un cas réel) :

var deal = await Db.GetAsync("deal", deal_id);
var lines = await Db.From("deal_line")
    .Where("deal_id", "=", deal_id).ToListAsync();

return new {
    InvoiceNumber = $"INV-{deal.id:D6}",
    Lines = lines.Select(l => new { l.description, l.quantity })
};

Forme du template HTML (Scriban, similaire à Liquid/Handlebars) :

<h1>{{ InvoiceNumber }}</h1>
<table>
  {{ for line in Lines }}
  <tr><td>{{ line.description }}</td><td>{{ line.quantity }}</td></tr>
  {{ end }}
</table>

Encodage HTML : chaque valeur {{ }} est encodée en HTML, donc une valeur d'enregistrement contenant <b> s'affiche comme le texte <b>. html.raw est la désactivation explicite, réservée au HTML de confiance : {{ notes | html.raw }}. Ne l'utilisez jamais sur des valeurs que les utilisateurs finaux peuvent saisir, car cela injecte leur balisage dans le document rendu. Seul le template peut désactiver l'encodage, pas les données. html.escape et html.newline_to_br fonctionnent toujours (newline_to_br échappe son texte mais conserve les balises <br />).

Panneau d'aperçu : toujours visible sur le côté droit de l'éditeur. Se recompose à l'enregistrement (bouton vert Update). Inclut une colonne de miniatures pour les PDF multi-pages et une barre d'outils de visualisation intégrée (zoom, rotation, download, print). Les boutons-icônes en haut à droite de l'éditeur basculent la visibilité du panneau d'aperçu et l'édition plein écran.

L'appeler depuis l'extérieur de l'éditeur :

  • POST /api/v1/pdf-templates/{id}/preview - rend par ID, retourne application/pdf. Utilisé par le panneau d'aperçu de l'éditeur.
  • POST /api/v1/pdf-templates/generate/{name} - rend par nom, retourne application/pdf.
  • POST /api/v1/pdf-templates/generate/{name}/base64 - identique mais retourne { "data": "<base64>" }.

Passez les valeurs de paramètre dans le corps JSON. L'auth est le Bearer token standard.

Limitation d'essai : à la fois l'endpoint generate et l'endpoint {id}/preview (utilisé par le panneau d'aperçu de l'éditeur) retournent 403 en mode essai, le rendu PDF est entièrement bloqué. Vous pouvez toujours créer le template (data script, HTML, paramètres, settings) et l'enregistrer ; vous ne verrez simplement pas de PDF rendu tant que vous n'êtes pas sur un environnement payant.

Note sur le rendu appelable depuis un script : le contexte de scripting C# expose bien une variable globale Pdf, donc un Script Module rend un template stocké directement avec await Pdf.GenerateAsync("invoice", ...) ou Pdf.GenerateBase64Async(...). Aucun aller-retour HttpClient vers l'endpoint generate n'est nécessaire. (L'autocomplete de l'éditeur de script annonce toujours Pdf.RenderAsync et un objet Templates ; aucun de ces deux noms n'existe à l'exécution.)

Pièges : Scriban échappe le HTML par défaut. Les sauts de page sont pilotés par CSS, page-break-before: always sur une section. Les polices disponibles sur le serveur de rendu se limitent aux polices système plus les familles Noto et Liberation, embarquez la vôtre via base64 @font-face si vous avez besoin d'une police de marque.

Images, polices et feuilles de style : le moteur de rendu ne charge les ressources que depuis des adresses http(s) publiques, plus les URL data: et blob:. Les adresses internes et de réseau privé (localhost, 10.x, 192.168.x, noms de services de conteneurs, etc.) sont bloquées, donc une image servie par un service interne n'apparaîtra pas dans le PDF. Utilisez une URL publique ou une URL data:.

Scheduled Events

Le même système d'Event Trigger que les Business Events, avec le trigger On Schedule coché. Utilisez-le pour les recalculs nocturnes, les digests hebdomadaires, les nettoyages périodiques, les rapports mensuels.

Workflow courant : créez un Business Event avec le trigger On Schedule (au lieu de, ou en plus des triggers de changement de données). Configurez l'expression cron. Ajoutez une action Execute Script dont le corps invoque un Script Module via Modules.CallAsync(...).

Expressions cron : Quartz à 6 ou 7 champs, voir Concepts -> Scheduled Events pour les motifs courants. Les heures sont l'heure serveur (UTC sur la stack de production).

Pièges : un job long qui déborde son intervalle cron ne se double pas, Quartz ne déclenchera pas une seconde instance du même job en simultané, le second tick est sauté. Les exécutions échouées ne réessaient pas automatiquement ; intégrez le réessai dans le script.

Packages

Bundles exportables d'objets de configuration (BE, pages, events, scripts, templates, schéma) avec dépendances en cascade. Ajouter une Page à un package tire automatiquement tout ce que cette page référence.

Workflow courant (export) : cliquez sur Create, nommez et versionnez le package. Ajoutez les objets de haut niveau qui vous intéressent, généralement une poignée de pages. Ouvrez le panneau des éléments liés pour voir la liste cascadée (ce que le package contiendra réellement). Décochez ce que vous voulez omettre. Décidez d'inclure ou non les données (basculeur à l'export). Cliquez sur Export, vous obtenez un ZIP.

Workflow courant (import) : sur l'environnement de destination, ouvrez Packages, cliquez sur Import, uploadez le ZIP. La plateforme affiche ce qui sera ajouté ou mis à jour. Les deltas de schéma ne sont pas appliqués automatiquement, si le package référence une colonne qui n'existe pas sur la destination, l'import échoue.

Pièges : les identifiants sont par nom, pas par ID numérique. Un BE appelé "customer" sur la source se lie à un BE appelé "customer" sur la destination, renommez d'un côté ou de l'autre et le lien se casse.

Dans git : l'extension VS Code récupère un package dans un dossier, à raison d'un fichier par artefact, et renvoie le dossier vers l'espace de travail, si bien qu'un package peut être relu et versionné comme n'importe quel autre code.

Third-Party Data Connections

Connexions à des bases de données externes (Postgres, SQL Server, MySQL, autres) qui importent les tables selon un calendrier. Les tables importées se comportent comme des tables natives, elles apparaissent dans Schema Designer et peuvent être référencées par des Business Entities, Pages et Scripts.

Workflow courant : créez une connexion (chaîne de connexion, test, enregistrer). Listez les tables distantes disponibles ; choisissez celles à importer. Pour chaque table importée, définissez le mode de sync (Full / Delta) et le déclencheur de sync (cron / manuel / intervalle fixe).

Miroir, pas fédéré : les données importées résident dans votre propre omnicore-db. Les lectures sont rapides (Postgres local). Compromis : fraîcheur entre les syncs.

Colonnes gérées par l'application : marquez certaines colonnes comme "gérées par Archestack", elles ne seront pas écrasées par les syncs futurs. Utile lorsque vous voulez annoter des données fournisseur avec vos propres drapeaux de statut.

Pièges : la première sync d'une grande table peut prendre un certain temps. Les syncs Delta nécessitent une colonne que le connecteur peut utiliser comme watermark, généralement un timestamp modified_at.

AI Assistant

Un assistant de chat intégré à l'application, capable à la fois de répondre à des questions sur vos données et de modifier votre environnement. Demandez "combien d'ordres de travail ouverts par site ce mois-ci ?" et il interroge puis répond. Demandez "ajoute une colonne statut aux rendez-vous de service et une page pour les lister" et, si vous êtes administrateur, il le construit.

Apportez votre propre modèle. Archestack ne livre aucun LLM et ne paie aucune inférence. Un administrateur ajoute un fournisseur sur l'écran AI Configuration : une base URL, un nom de modèle et une clé d'API. Tout ce qui parle l'API chat-completions d'OpenAI fonctionne (ChatGPT, DeepSeek, OpenRouter, Groq, Together, un Ollama ou vLLM auto-hébergé), et l'API native d'Anthropic est prise en charge directement. Vous pouvez en configurer plusieurs et en choisir un par conversation.

Tout ce qu'il fait passe par le MCP Server. L'assistant n'a pas de porte dérobée vers la plateforme, il appelle les mêmes outils que n'importe quel client MCP externe, en portant votre identité. Les limites que vous fixez là-bas s'appliquent donc aussi à lui, et chaque action atterrit dans le même journal d'audit. Voir MCP Server ci-dessous.

Workflow courant : ouvrez l'assistant depuis la barre latérale ou la bulle flottante. Demandez en une ou deux phrases. Il lit votre schéma actuel avant de proposer quoi que ce soit, annonce ce qu'il va changer, et attend votre confirmation. Chaque appel d'outil apparaît sous la réponse, vous voyez donc ce qu'il a réellement fait.

Fonctionne bien pour : les questions ponctuelles sur vos données, les fonctionnalités de forme CRUD, les automatismes simples, les extensions d'entités existantes.

Moins bien pour : les règles métier non évidentes et les workflows complexes en plusieurs étapes. Traitez ce qu'il construit comme la PR d'un développeur junior.

Pièges : il travaille sur des données de production et il n'y a pas d'annulation, prenez donc une sauvegarde avant toute opération risquée. Vos invites et ce que renvoient les outils sont envoyés au fournisseur que vous avez configuré, sous ses conditions ; pointez-le vers un modèle auto-hébergé si cela compte. Les longues conversations ne rejouent que leurs tours récents, une discussion laissée ouverte toute la journée n'en retient donc pas le début. Et notez les deux interrupteurs : avec le MCP Server arrêté, l'assistant peut encore parler, mais ne peut rien lire ni modifier.

MCP Server

Chaque environnement peut s'exposer via le Model Context Protocol, afin qu'un client IA extérieur à Archestack, Claude Desktop, Claude Code, ChatGPT deep research, un IDE, ou votre propre script, puisse travailler directement avec lui. C'est également ce sur quoi repose l'AI Assistant intégré.

Désactivé par défaut, et les outils qui modifient la construction de l'application restent désactivés même après le démarrage. C'est délibéré.

Trois groupes d'outils. Read couvre l'énumération et la description des entités, l'interrogation des lignes, la lecture du schéma, des pages, des events, des script modules, des packages, des translations et des event logs. Write correspond à la création, la mise à jour et la suppression d'enregistrements, et emprunte le même chemin que l'interface, vos Business Events se déclenchent donc exactement comme pour une personne qui clique sur Enregistrer. Admin est le groupe qui permet à un modèle de construire l'application elle-même : entités, schéma, pages, events, script modules, translations, packages et les paquets NuGet qu'utilisent les scripts.

Quatre verrous décident si un appel aboutit : le serveur tourne-t-il, le groupe est-il autorisé, cet outil précis est-il activé, et le jeton porte-t-il la portée. Un outil bloqué par l'un d'eux est invisible pour le client plutôt que refusé après coup.

Jetons. Émettez-en un par client sur l'écran MCP Server, avec les portées dont il a réellement besoin et une expiration facultative. Le secret n'est affiché qu'une fois, à la création, et seul un hachage est stocké : un jeton perdu se fait tourner, pas récupérer. Donnez à chaque intégration son propre jeton, cela garde le journal d'activité lisible et révoquer l'un ne perturbe pas les autres.

Journal d'activité. Chaque invocation est enregistrée, y compris celles qui ont été refusées et pourquoi. C'est là que vous vérifiez ce qu'une intégration a réellement fait.

Pièges : un jeton doté de la portée admin est traité comme un administrateur complet par tout le reste, distribuez-les avec parcimonie. "Allow token in URL" existe pour les clients incapables d'envoyer un en-tête Authorization, mais une URL se retrouve bien plus facilement dans les logs et les captures d'écran qu'un en-tête, laissez l'option désactivée sauf nécessité. Et sur un environnement de production avec de vraies données, laissez le groupe admin désactivé.

Object Browser

Vue brute de toute table native et importée, ajout, édition, suppression de lignes directement. Inclut un import CSV pour les chargements en masse.

Quand y recourir :

  • Chargement en masse de données de référence avant que les pages n'existent (pays, devises, enums de statut).
  • Débogage, examen visuel de ce qui se trouve réellement dans une table quand une page se comporte mal.
  • Édits admin rapides sur des données non encore exposées via une Page.
  • Import CSV : choisissez une table, uploadez un fichier, mappez les colonnes aux champs, aperçu, commit.

Pièges : Object Browser déclenche bien les Business Events (il passe par le même chemin d'écriture EntityService que le front-end). L'exception est le SQL direct via Database Deployments, qui contourne tout.

Dependency Graph

Une carte en lecture seule de la façon dont les objets de configuration de votre environnement se référencent les uns les autres. Huit types de ressources apparaissent sous forme de nœuds, colorés par type : Business Entities, Pages, Event Triggers, Script Modules, PDF Templates, Frontend Templates, Scheduled Events et Packages. Une arête entre deux nœuds signifie qu'une ressource dépend de l'autre. Recourez-y pour répondre aux questions "qu'est-ce qui casserait si je supprimais ceci ?" et "y a-t-il quelque chose d'inutilisé ici ?".

Ce que signifie une arête : une Page liée à une Business Entity, un Event Trigger enregistré sur une Business Entity, une étape d'action de Page qui déclenche un Event Trigger, un script qui appelle un autre Script Module via Modules.CallAsync("Name"), un script qui rend un PDF Template, un Scheduled Event qui déclenche un Event Trigger, ou un Package qui contient une ressource. C'est une vue de design-time, elle reflète ce que votre configuration câble ensemble, pas le trafic d'exécution en direct.

Deux types de problèmes sont mis en évidence visuellement :

  • Loose ends (orphelins) - des nœuds que rien ne référence et qui ne référencent rien. Ils sont rassemblés dans une bande "Loose ends" bordée de rouge au bas du canevas. Un Script Module que personne n'appelle ; une Page liée à rien.
  • Dangling references - dessinées comme des arêtes pointillées rouges. La référence existe dans la configuration mais sa cible est introuvable, par exemple une étape d'action de Page qui pointe encore vers un Event Trigger supprimé, ou un script qui appelle un Script Module par un nom qui n'existe plus.

Workflow courant : ouvrez Dependency Graph depuis la section Tools de la sidebar. Le panneau de gauche comporte une recherche par nom, un sélecteur Layout (Left to right, Top to bottom, By type, Force-directed), les bascules Group by package, Show loose ends et Show edges, et une liste à cocher par type. Cliquez sur un nœud pour entrer en mode focus : ce nœud et ses voisins directs restent vifs tandis que le reste du graphe s'estompe, ce qui vous permet de tracer exactement ce qu'une ressource touche. Cliquez sur le fond du canevas pour effacer le focus. Chaque nœud porte un petit bouton d'ouverture dans un nouvel onglet qui saute directement vers l'éditeur de cette ressource. La ligne de stats en haut indique le nombre de nœuds, d'arêtes, d'orphelins et de dangling references.

Group by package : enveloppe les membres de chaque package dans une boîte pointillée étiquetée, pour que vous puissiez voir un package comme une unité et repérer les ressources qu'il a tirées par cascade.

Pièges : le graphe est construit à la demande à partir de l'état courant, utilisez le bouton de rafraîchissement après avoir modifié des ressources ailleurs. Les arêtes dérivées des scripts sont trouvées en scannant les corps de script à la recherche de la forme d'appel littérale Modules.CallAsync("Name"), un module invoqué via un nom calculé n'affichera pas d'arête. Une dangling reference vaut toujours la peine d'être corrigée ; un orphelin souvent non, un Script Module flambant neuf compte comme orphelin jusqu'à ce que quelque chose l'appelle.

Event Logs

La piste d'audit pour tout ce que la plateforme a exécuté, Business Events, Script Modules, Scheduled Events, syncs tierces.

Workflow courant : ouvrez Event Logs, réglez Status sur Failed, cliquez dans une entrée. Le panneau de détail affiche le payload (l'état de l'enregistrement au moment, les paramètres passés), l'exception avec la stack trace et le timing.

Logs et Charts : la page comporte deux onglets qui partagent une même rangée de filtres. Logs est le tableau ci-dessus. Charts est un tableau de bord opérationnel sur le même filtre : le volume au fil du temps empilé par statut, une tendance du taux de réussite, une répartition par source, les events qui échouent le plus et les plus lents, la durée moyenne et maximale au fil du temps, une répartition par type de trigger, et une heatmap jour de la semaine par heure de la journée, avec les totaux principaux en haut. Les deux onglets partagent une plage de dates De / À (avec les préréglages Dernière 1h / 24h / 7j) ; l'onglet Charts choisit automatiquement un intervalle de temps adapté à la plage et peut se rafraîchir à intervalle régulier pour un suivi en direct.

Notifications en temps réel : l'icône cloche dans la barre supérieure de l'application admin fait remonter les échecs non lus via SignalR.

Pièges : le log peut grossir. Les anciennes entrées sont conservées indéfiniment par défaut ; si le disque est une préoccupation, mettez en place un Scheduled Event qui purge les entrées de plus de N jours.

User Management

Créer, inviter et gérer les utilisateurs ; assigner les rôles realm. Enveloppe l'API REST admin Keycloak.

Rôles :

  • admin - tout ; peut gérer toutes les Business Units, utilisateurs, rôles, paramètres système.
  • owner - gère ses propres Business Units, peut éditer les pages d'administration et d'exécution.
  • editor - peut voir et éditer les pages d'exécution, mais pas modifier la configuration de schéma/BE/event.
  • user - lecture seule sur les pages d'exécution.

Pièges : rétrograder un utilisateur d'admin en cours de session ne le déconnecte pas, le changement s'applique au prochain rafraîchissement de token (généralement en moins d'une minute).

Business Units

Groupes adossés à Keycloak qui filtrent la visibilité sur des ressources individuelles. Voir Concepts -> Business Units pour le modèle.

Workflow courant : créez une BU. Invitez des membres par email et choisissez le rôle. Assignez les ressources en allant sur chaque BE / Page / Trigger ou via le panneau des éléments liés de la BU.

Assignments en cascade : lorsque vous assignez une Page à une BU, l'assignment se propage au BE auquel la page se lie.

Pièges : la BU active d'un utilisateur est définie dans le sélecteur de BU de la barre supérieure et persiste dans localStorage. Les nouveaux utilisateurs ont par défaut leur première BU ; si une page de liste est suspicieusement vide, vérifiez quelle BU est active.

Database Backups

pg_dump nocturne des trois bases de données, plus un ZIP des fichiers de l'espace de travail. Déclenchement / upload / restore manuels depuis l'UI.

Workflow courant : Trigger now -> Download -> Upload -> Restore. Le calendrier est par défaut nocturne à 03:00 UTC.

Limitation d'essai : Les Backups sont désactivés dans l'environnement d'essai.

Branding

Modifier le nom d'affichage de l'application, le logo (clair + sombre), les couleurs, les polices et le reste du thème Material UI, appliqué à l'exécution, sans rebuild.

Workflow courant : définissez le nom de l'application, uploadez un logo clair et un logo sombre (PNG/JPG/SVG, ≤ 10 Mo), choisissez les couleurs de marque dans l'onglet Theme. Enregistrez : votre session l'affiche aussitôt, celles des autres au prochain chargement de page.

Thème : l'onglet Theme est un éditeur complet de thèmes Material UI avec aperçu en direct en mode clair et sombre : les couleurs de marque et chaque couleur de la palette par mode, les polices (n'importe quelle Google Font) et chaque style de texte, le rayon des coins, l'espacement, les ombres, la vitesse des animations, le style des boutons, cartes et champs, les valeurs par défaut des composants et leurs overrides en JSON. Il vérifie le contraste, part de modèles, importe un thème d'une autre application Material UI et exporte celui-ci en code. Show on the whole app essaie un brouillon sur tous les écrans avant d'enregistrer. Un workspace qui ne change jamais son thème garde exactement l'apparence intégrée. Dans un dépôt, le thème est la clé theme de workspace/branding.json.

Pièges : le logo est aussi l'icône de l'onglet du navigateur et l'icône qu'un téléphone affiche pour l'application sur son écran d'accueil. L'écran d'accueil demande un logo PNG, JPG ou WebP ; avec un SVG, il affiche l'icône d'Archestack.

Analytics

Google Analytics sur l'application publiée, configuré sur la page Analytics (Configuration). Le builder n'est jamais suivi.

Workflow courant : collez l'ID de mesure GA4 (G-XXXXXXXXXX, sous Administration, Flux de données dans Google Analytics), cochez Track visits to the published app et enregistrez. Ask visitors for consent first est activé par défaut : la balise de Google ne se charge qu'après qu'un visiteur a accepté la bannière, comme l'exigent les règles de l'UE sur les cookies. Le texte de la bannière, ses boutons et un lien vers votre politique de confidentialité peuvent être les vôtres.

Événements depuis le code de page : une fois la balise chargée, le code de page peut envoyer ses propres événements, comme window.gtag?.("event", "sign_up"). Rien n'est envoyé tant que le suivi est désactivé ou que le visiteur a refusé.

Translations

Éditeur clé/valeur pour les chaînes UI de l'application. Ajouter de nouvelles locales, surcharger les libellés par défaut par client.

Workflow courant : choisissez une locale, cherchez une clé, tapez une nouvelle valeur, enregistrez. Rafraîchissez l'application admin.

Pièges : les traductions sont chargées une fois par session ; les utilisateurs sur un onglet rance ne voient pas les changements tant qu'ils ne rafraîchissent pas. Certains libellés (en-têtes de colonnes des BE) viennent de la config du BE, pas des Translations.


APIs appelables depuis un script

La surface exacte que les scripts peuvent appeler. Vérifiée par rapport au code source, chaque méthode ci-dessous existe réellement.

Variables globales

Chaque script reçoit ces variables de premier niveau (aucun autre nom n'existe dans le scope du script) :

  • Entity - dynamic ExpandoObject. Enregistrement courant. Accédez aux champs avec Entity.column_name. Muter Entity dans un trigger Before* persiste les modifications dans le cadre de l'écriture en cours.
  • OldEntity - dynamic ExpandoObject. Enregistrement précédent (triggers update + delete). Lecture seule.
  • Log - ILogger. Log.LogInformation("..."), Log.LogWarning(...), Log.LogError(...). Écrit dans les logs serveur et l'entrée Event Log.
  • Db - DbHelper. Accès base de données : un ensemble typé par Business Entity, et les helpers non typés (voir ci-dessous).
  • Modules - appeler d'autres Script Modules. Chaque module est une méthode avec ses propres paramètres : await Modules.OrdersOf(customer: "Acme"), vérifiée à la compilation du script ; le résultat est dynamic. await Modules.CallAsync("OrdersOf", new Dictionary<string, object?> { ["customer"] = "Acme" }) appelle toujours un module par son nom et retourne Task<object?>.
  • Pdf - ScriptPdfHelper. Rendre un PDF Template stocké par nom : await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) retourne byte[] ; await Pdf.GenerateBase64Async(...) retourne le même payload encodé en base64. Les paramètres sont transmis au data script du template comme variables locales typées string.
  • Ocr - ScriptOcrHelper. Lire le texte imprimé d'une image, dans le processus, sans service externe ni clé d'API : await Ocr.ExtractAsync(byte[] image) retourne un OcrExtraction. await Ocr.ExtractBase64Async(string) prend la même image encodée en base64 (un préfixe data:image/png;base64, est toléré), await Ocr.ExtractTextAsync(byte[]) retourne uniquement le texte.
  • Doc - ScriptDocHelper. Trouver le document dans une photo et l'aplatir : await Doc.CropAsync(byte[] image) retourne un DocumentCrop. await Doc.CropBase64Async(string) prend une capture en base64.
  • Files - ScriptFilesHelper. Le dossier de fichiers propre au workspace, celui que gère la page Files : await Files.WriteAsync("invoices/1042.pdf", bytes), await Files.ReadAsync(path), ListAsync(folder), MoveAsync, DeleteAsync. Voir Files.

Il n'y a pas de User, Http, Email, Templates ni aucune autre variable globale.

Db - méthodes de ligne unique + écriture

Toutes async, toutes sur l'objet Db de premier niveau :

  • await Db.GetAsync(string table, object id) - récupérer une ligne par clé primaire. Retourne dynamic? (ou null si non trouvée).
  • await Db.CreateAsync(string table, object values) - insertion. values peut être un objet anonyme ou un Dictionary<string, object?>. Déclenche les triggers Before/After Create. Retourne int (le nouvel ID).
  • await Db.UpdateAsync(string table, object id, object values) - update par PK. Déclenche les triggers Before/After Update.
  • await Db.DeleteAsync(string table, object id) - delete par PK. Déclenche le trigger Before Delete.

Db - constructeur de requête fluide

Commencez par Db.From(table). Chaînez les filtres, puis un terminal :

  • .Where(string column, string op, object? value = null) - ajouter une clause WHERE. Opérateurs : =, != / <>, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL. Chaînez plusieurs appels .Where(...) pour AND.
  • .OrderBy(string column, bool desc = false) - définir ORDER BY. Un seul autorisé par requête.
  • .Limit(int limit) - lignes max. Par défaut 1000.

Terminaux (chacun exécute la requête) :

  • await ...ToListAsync() - retourne List<dynamic>.
  • await ...FirstAsync() - retourne dynamic? (première correspondance ou null). Définit temporairement la limit à 1.
  • await ...CountAsync() - retourne int.

Il n'y a pas de SumAsync, MaxAsync, FirstOrDefaultAsync ni SingleAsync sur Db.From. Pour les agrégations, récupérez avec ToListAsync() et réduisez en C#, ou utilisez les ensembles typés ci-dessous, qui proposent toutes ces méthodes.

Exemple concret

// Get a row by ID, query a list, calculate a sum, update.
var order = await Db.GetAsync("orders", 42);
if (order == null) return false;

var items = await Db.From("order_items")
    .Where("order_id", "=", order.id)
    .OrderBy("id")
    .ToListAsync();

decimal totalAmount = 0;
foreach (var item in items) {
    totalAmount += (decimal)(item.amount ?? 0);
}

await Db.UpdateAsync("orders", 42, new {
    total = totalAmount,
    updated_at = DateTime.UtcNow
});

return true;

Db - les Business Entities comme ensembles typés

Chaque Business Entity est aussi un ensemble typé sur Db, qui porte le nom de l'entité sans les espaces : une entité appelée Service Order devient Db.ServiceOrder. Sous le capot, c'est EF Core, donc tout LINQ fonctionne (Where, Include, SumAsync, GroupBy, projections), avec une propriété de navigation pour chaque clé étrangère et chaque jointure de Business Entity. Les colonnes sont des propriétés en PascalCase avec leurs vrais types : planned_date devient PlannedDate, un DateOnly.

// Every business entity is a typed set on Db, named after the entity.
var since = new DateOnly(2026, 1, 1);
var open = await Db.Order
    .Where(o => o.Status == "Open" && o.OrderDate >= since)
    .Include(o => o.OrderItems)
    .OrderBy(o => o.OrderDate)
    .ToListAsync();

foreach (var order in open)
    order.Total = order.OrderItems.Sum(i => i.Amount ?? 0);

var customer = await Db.Customer.SingleAsync(c => c.Email == "ada@example.com");
Db.Order.Add(new Order { Customer = customer, Status = "Open" });

// Writes every changed, added and removed row; business events fire as usual.
await Db.SaveChangesAsync();
return open.Count;
  • Noms. Une table sur laquelle aucun Business Entity n'est construit reçoit un ensemble nommé d'après la table (bank_account devient Db.BankAccount). Une colonne de clé étrangère sans son suffixe _id donne la navigation : customer_id devient Customer, et l'autre côté est au pluriel, Customer.Orders.
  • Enregistrement. await Db.SaveChangesAsync() écrit chaque ligne ajoutée, modifiée ou supprimée comme le font CreateAsync, UpdateAsync et DeleteAsync, donc les Business Events se déclenchent et les colonnes d'audit sont estampillées, et une ligne modifiée n'envoie que les colonnes qui ont changé. Ce n'est pas une seule transaction : les lignes enregistrées avant un échec restent enregistrées. ExecuteUpdate, ExecuteDelete et les écritures en SQL brut sont refusés.
  • Types. Un timestamp with time zone se lit comme un DateTime UTC, et une valeur comparée à une telle colonne doit elle aussi être en UTC : DateTime.UtcNow. Les colonnes d'un type que le modèle ne mappe pas (tableaux, enums, types géométriques) sont laissées de côté ; lisez-les avec Db.From.
  • Toujours à jour. Le modèle est reconstruit chaque fois qu'une table ou un Business Entity change, donc une colonne supprimée devient une erreur de compilation sur sa ligne plutôt qu'un échec à l'exécution. Avant que vous ne l'exécutiez, un déploiement en attente liste les scripts typés qui cesseraient de compiler.

Db.From et les méthodes ci-dessus continuent de fonctionner à côté des ensembles typés, et l'éditeur de script comme VS Code complètent automatiquement les ensembles, les colonnes et les navigations.

Mode simulation

Pendant la simulation d'un trigger (le bouton Run Simulation sur un Business Event) et dans l'onglet Test d'un Script Module, Db.SimulationMode vaut true. Les écritures (CreateAsync, UpdateAsync, DeleteAsync et SaveChangesAsync) vérifient la table, la ligne et les valeurs, consignent ce qu'elles auraient fait et n'écrivent rien, donc aucun Business Event ne se déclenche non plus. Les lectures voient les données réelles. Les écritures, déplacements et suppressions via Files sont simulés aussi, et les mails et les notifications sont consignés, pas envoyés. C'est ce qui rend Run Simulation sûr à utiliser contre des données réelles. Tout ce que le script fait d'autre par lui-même est réel : les appels HTTP, System.IO, et tout ce que fait un paquet NuGet, comme une connexion à une autre base de données.

Modules

  • await Modules.OrdersOf(customer: "Acme") - chaque Script Module est une méthode nommée d'après le module, sans les espaces ("Orders of" devient Modules.OrdersOf), qui prend les paramètres qu'il déclare avec leurs types, les obligatoires d'abord ; un paramètre optionnel omis reçoit sa valeur par défaut. Un nom mal orthographié ou une valeur du mauvais type est une erreur de compilation sur sa ligne. Retourne Task<dynamic?> : le résultat se lit sans cast, y compris un type anonyme. Les arguments lus depuis Entity sont dynamic : castez-les ((int)Entity.company_id) pour que l'appel soit vérifié à la compilation.
  • await Modules.CallAsync(string moduleName, Dictionary<string, object?>? parameters = null) - appeler un autre Script Module par son nom. Les paramètres deviennent des variables typées dans le script du module appelé. Retourne Task<object?> (ce que le module a retourné).

Files - le dossier du workspace

Chaque workspace a sur le serveur un dossier à lui, géré sur la page Files (Operations). Les fichiers envoyés y arrivent, ainsi que tout ce qu'un script conserve : une facture générée, un export, un modèle entraîné. Rien n'en est stocké dans une base de données, et aucun chemin ne mène au dossier d'un autre workspace. Les chemins sont relatifs et utilisent des barres obliques ; un chemin contenant .., un lecteur ou un lien est refusé.

// A generated invoice, kept in the workspace's own folder.
var pdf = await Pdf.GenerateAsync("Invoice", new Dictionary<string, string> { ["id"] = Entity.orders_id.ToString() });
var path = $"invoices/{DateTime.UtcNow:yyyy}/{Entity.orders_number}.pdf";
await Files.WriteAsync(path, pdf);

// Read it back, or anything an admin uploaded on the Files page.
var rates = await Files.ReadTextAsync("config/vat-rates.json");
foreach (var file in await Files.ListAsync("invoices", recursive: true))
    Log.LogInformation("{Path} is {Size} bytes", file.Path, file.Size);

return path;
  • ReadAsync(path) retourne les octets et lève une exception si le fichier n'existe pas ; ReadTextAsync lit du texte UTF-8 ; ReadDataUriAsync retourne data:image/png;base64,… pour une image dans un PDF Template ou un mail.
  • WriteAsync(path, bytes) et WriteTextAsync créent les dossiers et remplacent un fichier existant. Une écriture aboutit entièrement ou pas du tout.
  • ListAsync(folder, recursive) liste d'abord les dossiers ; chaque entrée a Path, Name, IsFolder, Size, ModifiedUtc et ContentType.
  • MoveAsync(from, to) n'écrase jamais ; DeleteAsync(path) supprime un fichier, ou un dossier avec tout son contenu.
  • PathOf(path) est le chemin absolu sur le disque, pour une bibliothèque qui en attend un.

Lors d'une exécution à blanc (le bouton Test) et d'une exécution de tests, les écritures, déplacements et suppressions n'atteignent jamais le dossier : l'exécution les garde pour elle, si bien que relire un fichier qu'elle vient d'écrire fonctionne, et les liste avec ce qu'elle a enregistré. Le dossier .archestack contient les fichiers propres de la plateforme (logos, icônes de paquets, pièces jointes des mails) : les scripts peuvent le lire, jamais l'écrire. Une sauvegarde inclut le dossier sous forme d'archive workspace-files à côté des dumps des bases de données.

Ocr et Doc - lire une photo

Les deux tournent dans le processus de l'API. Aucun service d'OCR à déployer, aucune clé à configurer, aucun coût par page.

OcrExtraction (ce que Ocr retourne) :

  • Text - chaque ligne reconnue jointe par des sauts de ligne, dans l'ordre de lecture.
  • Lines - par ligne Text, Confidence, et une bounding box : X, Y, Width, Height, plus Right et Bottom. En pixels de l'image source, origine en haut à gauche.
  • Confidence - moyenne des confidences par ligne, 0 à 1. ElapsedMs, IsEmpty.

Servez-vous des boîtes. Lire un document, c'est localiser une étiquette puis lire ce qui se trouve à côté, ce que le texte seul ne peut pas exprimer : trouvez la ligne contenant "Total", puis prenez la ligne dont la plage Y la chevauche et dont le X dépasse son Right.

DocumentCrop (ce que Doc retourne) :

  • Found - si un contour a réellement été détecté. Vérifiez-le en premier.
  • Image - octets PNG de la page aplatie, ou les octets d'origine inchangés quand Found est false. Une photo jamais recadrée se lit quand même ; une photo recadrée sur le mauvais quadrilatère non, donc le détecteur refuse plutôt que de deviner.
  • Corners - le contour dans la photo source, haut-gauche, haut-droite, bas-droite, bas-gauche. Width, Height, ElapsedMs.

Les deux se composent dans l'ordre évident, et comme un recadrage raté renvoie l'original, les enchaîner fait toujours quelque chose : var crop = await Doc.CropAsync(photo); var scan = await Ocr.ExtractAsync(crop.Image);

Trois limites à intégrer dès la conception. Le modèle embarqué est PP-OCRv5 latin, donc alphabets occidentaux et chiffres, pas le CJK. Ce n'est pas un lecteur de PDF, une page PDF doit d'abord être rasterisée en image. Et c'est de l'OCR, pas de la compréhension de document : transformer des lignes en fournisseur, numéro de facture et total est une logique que vous écrivez, et elle se trompera parfois, donc vérifiez line.Confidence et orientez les lectures faibles vers un humain plutôt que de les écrire directement dans une table.

Attention au timeout du script. Une page complète prend une à deux secondes et le premier appel dans le processus ajoute environ une seconde de chargement des modèles. Le timeout de dix secondes n'interrompt pas un await, donc un script qui lit plusieurs pages ne retourne rien du tout au lieu d'échouer tôt. Traitez le multi-pages dans un Scheduled Event, une page par exécution.

Expressions de template (actions de Business Event)

La configuration d'action utilise les accolades {{ ... }}. La liste complète des jetons valides :

  • {{ Entity.column_name }} / {{ Entity.id }}
  • {{ OldEntity.column_name }}
  • {{ now() }} / {{ getdate() }} - date-heure UTC ISO 8601
  • {{ today() }} - chaîne date
  • {{ guid() }} / {{ newid() }} - GUID frais
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }} - secondes Unix
  • {{ empty() }}, {{ null() }}
  • Agrégats dans un contexte de jointure : {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Arithmétique : {{ Entity.quantity * Entity.price }} - évaluée après substitution via DataTable.Compute()

Il n'y a pas de {{ user.email }} ni d'autre jeton à portée utilisateur.