Reference

Ogni strumento, in un solo posto

Questa pagina è il reference pratico per ogni strumento nella sidebar del tuo ambiente Archestack. Ogni sezione copre cosa fa lo strumento, quando lo useresti, i passi per i workflow più comuni e le trappole in cui inciampano i nuovi utenti. La sezione finale (API chiamabili dagli script) documenta l'esatta superficie che gli script possono chiamare.

Schema Designer

Un editor visuale per lo schema del database, tabelle, colonne, tipi, relazioni, indici, che si salva automaticamente su un documento JSON. Lo schema è la fonte di verità per la tua applicazione; nessun altro strumento vede una colonna finché non la aggiungi qui e ne fai il deploy.

Workflow comune: nella toolbar, fai clic su Add Table (pulsante icona). Si apre un dialog intitolato "Create New Table"; digita un nome (il testo di aiuto ti ricorda "Lowercase with underscores recommended"), fai clic su Create. La tabella compare sul canvas con una primary key SERIAL id auto-generata. Seleziona la tabella, il pannello destro si apre sulla tab Columns. Aggiungi colonne dal box "Add column" con bordo tratteggiato in basso (campo nome, dropdown Type, fai clic su Add Column). Fai clic su ogni colonna per espanderla e regolare i toggle (PK, NULL, UQ), Length e Default value.

Tipi di colonna disponibili nel dropdown 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, più i tipi geometrici e array. La Length è rilevante solo per VARCHAR e CHAR.

Le foreign key stanno nella tab Relations del pannello destro (non "Relationships"). Scorri fino a "Add relationship": scegli la colonna FK sulla tabella corrente, il tipo di relazione (One-to-One / One-to-Many / Many-to-Many), la tabella di riferimento, la colonna di riferimento, poi fai clic su Add Relationship. Ogni relazione ha i dropdown On Delete e On Update (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION).

Auto-save: l'indicatore in basso a sinistra oscilla tra "Auto saving..." e "Saved". C'è anche un pulsante Save nella toolbar (abilitato solo quando ci sono modifiche in attesa) per i salvataggi espliciti.

Altri pulsanti della toolbar: Add Group (container visuali per tabelle correlate, pannello proprietà intitolato "Group"), Add Text (annotazioni libere, pannello proprietà intitolato "Text Annotation"), controlli di zoom, filtro package, ricerca tabelle.

Trappole: rinominare una colonna produce un piano distruttivo (drop + add): modifica l'SQL nella tab Generated SQL del deployment per usare RENAME COLUMN se vuoi preservare i dati. Aggiungere una colonna NOT NULL a una tabella non vuota senza un default fallisce, rendila nullable, fai il backfill, poi altera. Il pulsante toolbar Deploy ti porta su una pagina di configurazione del deployment, non direttamente nel deploy.

Database Deployments

Elenca ogni deployment con il suo stato (Draft / Executing / Succeeded / Failed) e ne lancia di nuovi. Route: /database-deployments/overview.

Pulsanti in alto a destra: Refresh (ricarica la lista), New Deployment (crea una nuova bozza).

Da Schema Designer: il pulsante verde Deploy con icona razzo nella toolbar di Schema Designer ti porta a /database-deployments/configure/new, una pagina di configurazione deployment nuova.

Pagina di configurazione del deployment: due tab.

  • Configuration - editor per gli script pre/post deployment (PostgreSQL) ciascuno con un pulsante Test Run. Il pre-script gira prima dei cambi di schema; il post-script dopo. Utili per backfill, ricostruzione di indici o pulizia di staging area. Più un toggle Force Deploy (Allow Data Loss) per operazioni distruttive.
  • Generated SQL - mostra la migration prodotta dalla piattaforma. L'SQL è modificabile. Un pulsante Regenerate (icona refresh) riesegue il diff se hai fatto cambi di schema da quando hai aperto questa pagina.

Pulsanti in alto: Back, Save (salva la configurazione come Draft), Deploy (esegue il deployment immediatamente). Deploy è quello che clicchi alla fine.

Trappole: i deployment non vengono automaticamente avvolti in una transazione, un fallimento a metà strada lascia il database in uno stato parziale. Per le migration rischiose, avvolgi le tue istruzioni in BEGIN; ... COMMIT; nel pre-script. Il toggle Force Deploy salta i controlli di sicurezza; usalo in modo deliberato.

Business Entities

Viste curate costruite su una master table più join da tabelle correlate. Le pagine si collegano alle Business Entities, mai a tabelle raw. Vedi Concetti -> Business Entities per il perché.

Workflow comune: fai clic su Create. Si apre l'editor a tutta pagina con le tab Visual, JSON, Events. Imposta Entity Name, Master Table (Autocomplete), e Label Column (la colonna il cui valore rappresenta un record nei picker). Salva, la BE auto-genera una lista di colonne native dalla master table.

Aggiungere join: scorri fino alla card del Join Configurator e fai clic su Add Join. Ogni join si apre come un Accordion con: Join Type (INNER / LEFT / RIGHT), From Table, To Table (Autocomplete raggruppato per Database Tables / Third Party), From Column, To Column, dropdown opzionali Type Cast, e un picker di colonne per quali colonne target esporre.

Aggregazioni: su un join, sposta Aggregate Mode. Per ogni colonna scelta poi scegli una Aggregate Function: COUNT / SUM / AVG / MIN / MAX / COUNT DISTINCT. "Numero di contatti aperti su questa Company" è l'esempio canonico.

Run Preview: l'editor ha un pulsante Run Preview (con un'icona play) che esegue la configurazione completa dei join e mostra righe reali in una griglia dati. Se una colonna joined torna vuota, la colonna non era spuntata nel picker delle colonne del join (o la relazione è mal configurata).

Più BE per master table: niente ti impedisce di avere customer (sales view) e customer (support view) sulla stessa master. Pagine diverse, colonne appropriate per pubblici diversi, stessa riga sottostante.

Trappole: le colonne joined e aggregate sono read-only. Per modificare l'etichetta joined, vai al record sorgente. Le colonne aggregate vengono ricalcolate a ogni query, va bene per centinaia di righe, lente su milioni.

Page Editor

Configura la UI runtime collegando una Page a una Business Entity. La piattaforma auto-genera le sezioni in base alle colonne della BE; tu raffini il layout da lì. Non c'è un "template picker" separato, ogni pagina parte dalla stessa baseline generata e tu la personalizzi.

Workflow comune: fai clic su Create. Compila Page Name, Page Route (es. /companies), scegli la Business Entity (Autocomplete). La pagina si apre nell'editor con le tab Visual, Overview, Create, Entities, Events, JSON. Aggiusta il layout auto-generato, poi sposta lo Switch Published nell'header in alto su ON.

Tipi di widget di campo (il Select Type su ogni campo): Text, Textarea, Number, Date, Select, Checkbox, Email. Usa Select per i campi foreign key e imposta l'autocomplete Entity del campo sulla BE referenziata così gli utenti scelgono per label.

Tab nel form di dettaglio: fai clic su Add Tab nella tab Visual. Dentro una tab puoi aggiungere una sezione RelatedGrid collegata a un'altra BE filtrata per PK del record corrente. Esempio classico: una pagina Company con tab "Contacts" e "Deals", ciascuna filtrata per company_id = current company's id.

Pulsanti azione nell'inspector Header del form di dettaglio. Fai clic su Add nella sezione Actions. Ogni azione ha Label, Icon (Save / Delete / Add / Refresh / Download / Bolt / None), Variant (Contained / Outlined / Text), Color (Primary / Secondary / Error / Success / Warning / Info), e Steps (una lista ordinata di: Save Record, Delete Record, Navigate, Business Events, Custom). Per eseguire un Business Event da un pulsante, imposta il tipo di step su Business Events e scegli l'event per nome.

Pubblicazione: lo Switch Published nell'header in alto è l'unico controllo di pubblicazione. ON = la pagina appare sotto Published Pages nella sidebar e gli utenti finali che navigano alla sua route la vedono. OFF = bozza (solo tu nell'editor vedi le tue modifiche).

Trappole: le colonne auto-generate su una Page fresca rispecchiano la BE, se la BE cambia (aggiungi un join), la Page non prenderà automaticamente le nuove colonne. Ricarica la pagina nell'editor o aggiungi la colonna manualmente alla sezione. Attualmente non c'è un tipo di step azione "Generate PDF", per collegare un PDF a un pulsante di pagina, esponi il PDF tramite uno Script Module che chiama l'endpoint REST e usa uno step Custom (oppure chiama l'endpoint direttamente da un Frontend Template).

Business Events

Regole che scattano sui cambi di dati (o su pianificazione, o manualmente) ed eseguono action quando le condizioni sono soddisfatte. Il meccanismo "quando succede X, fai Y" della piattaforma. Vedi Concetti -> Business Events per il modello.

Workflow comune: fai clic su Create. Imposta Rule Name, sposta Enabled. Scegli Business Entity. Spunta uno o più Triggers: BeforeCreate / BeforeUpdate / AfterCreate / AfterUpdate / BeforeDelete / OnSchedule / Manual / InitialValue. Costruisci l'albero di condizioni (gruppi AND/OR). Aggiungi una o più action nel FlowCanvas. Salva.

Tipi di action (RuleActionKind):

  • Execute Script - esegue uno script C#. L'action più flessibile; usala ogni volta che vuoi impostare un campo, fare un calcolo, chiamare un'API, qualsiasi cosa custom. Lo script riceve Entity (mutalo su trigger Before* per cambiare la scrittura in corso), OldEntity, Log, Db, Modules.
  • Validate - uno script che ritorna bool. true significa che la validazione fallisce e il salvataggio è bloccato con il messaggio di errore configurato. Opzionalmente evidenzia colonne specifiche tramite errorColumns.
  • Block Operation - ferma duramente l'operazione con un messaggio configurato. Niente script.
  • Create Entity - inserisce un record in un'altra BE. I valori dei campi supportano template expression come {{ Entity.column_name }} e {{ now() }}.
  • Update Entity - aggiorna record in un'altra BE che matchano un filtro di condizione. Stesse template expression.
  • Delete Entity - elimina record che matchano una condizione. Rifiuta di scattare senza una condizione (sicurezza).
  • Send Email / Send Webhook / Publish Event - definite nello schema come miglioramenti futuri; attualmente loggate e saltate.

Pattern "imposta un campo": non c'è un'action discreta Set Field. Il modo per impostare un campo è un Execute Script su un trigger Before* che muta Entity. Esempio: Entity.title = ((string)Entity.title)?.Trim();. La piattaforma persiste l'Entity modificato come parte della scrittura in corso, nessuna query extra, nessun rischio di ricorsione.

Run Simulation: la tab Simulate dell'editor di trigger ha un picker di record e un pulsante Run Simulation. La piattaforma esegue le tue condizioni e action contro il record scelto senza persistere modifiche: le scritture vengono controllate e registrate, ma mai eseguite, comprese quelle di uno script tramite Db e Files, quindi niente arriva al database o ai file del workspace e non scatta nessun altro Business Event. Mail e notifiche non vengono inviate. Tutto il resto che uno script fa da sé, invece, è reale: le chiamate HTTP, System.IO e tutto ciò che fa un pacchetto NuGet, come una connessione a un altro database. Il pannello di output mostra quali condizioni hanno matchato e cosa avrebbe fatto ogni action. Usalo per beccare configurazioni sbagliate prima che i dati live le incontrino.

Template expression nella configurazione delle action usano parentesi {{ ... }}. I token effettivi: {{ Entity.column }}, {{ OldEntity.column }}, {{ now() }} / {{ getdate() }}, {{ today() }}, {{ guid() }} / {{ newid() }}, {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }}, aggregati come {{ SUM(column) }} dentro contesti di join, e aritmetica post-sostituzione come {{ Entity.quantity * Entity.price }}. Non c'è un token {{ user.email }}.

Operatori di condizione: Equals, NotEquals, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, Like (= ILIKE), Contains (= ILIKE %value%), IsNull, IsNotNull, In (= ANY(...)).

Trappole: un trigger che muta lo stesso record che osserva si rilancerà da solo se usi Update Entity; usa invece Before Update + Execute Script + Entity.field = .... Due trigger sullo stesso event scattano in ordine di priorità. Gli event disabilitati appaiono comunque nella lista, il toggle Enabled è separato dalla config del trigger.

Script Modules

Script C# riutilizzabili compilati a runtime da Roslyn. Prendono parametri, interrogano il database tramite l'helper Db, ritornano un valore. Chiamabili da Business Events, Scheduled Events, e direttamente dal front end.

Workflow comune: fai clic su Create. L'editor si apre con le tab Edit, Parameters, Test, JSON. Nomina il modulo (PascalCase, es. RecalcOpenRevenue). Nella tab Parameters, fai clic su Add per parametro (Name, Type, Required, Default Value). Opzioni di Type: string, int, decimal, double, bool, DateTime. Torna alla tab Edit, scrivi C#. IntelliSense è attivo.

Pannello Test: la tab Test mostra gli input dei parametri a sinistra e un pulsante Run (icona PlayArrow). Fai clic su Run; il lato destro mostra un indicatore di successo o errore, log di output, e il valore di ritorno (formattato JSON).

L'helper Db, vedi il reference completo in API chiamabili dagli script sotto.

Chiamare da un Business Event: aggiungi un'action Execute Script il cui corpo chiama il modulo:

// 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
});

Chiamare da uno Scheduled Event: crea un event con il trigger On Schedule, imposta il cron, aggiungi un'action Execute Script con lo stesso corpo Modules.CallAsync.

Logging: ogni invocazione scrive una voce in Event Logs con tempo di esecuzione, parametri, e risultato (o errore).

Trappole: sempre await le chiamate Db, dimenticarlo compila ma ritorna un Task. I ritorni sono passati per oggetto (non serializzati JSON dal framework); per il consumo front-end, ritorna oggetti anonimi con campi primitivi. La suggestion-on-commit-character è disabilitata di proposito nell'editor; usa Tab per accettare.

Shared Code

Codice che serve a più di uno script o di una pagina, tenuto in file a sé. Il nome di un file è il suo percorso, come Sales/OrderSummary.cs o components/StatusChip.tsx, e la sua estensione dice cos'è: .cs è C# che ogni script può usare, mentre .tsx, .ts, .jsx e .js sono moduli importati dalle pagine, dai Frontend Templates e dai blocchi Code dei layout. I file condivisi appartengono ai package come qualsiasi altro artefatto.

Workflow comune: nella sidebar, sotto Logic, apri Shared Code e fai clic su New file. Digita il percorso; l'editor precompila il file con un esempio del suo tipo. Scrivi il codice e fai clic su Save.

I file C# sono C# ordinario: namespace, record, classi e helper statici. Ogni script (Script Modules, Business Events, Scheduled Events, data script dei PDF e test) può usarne i tipi pubblici. Un tipo senza namespace è disponibile con il suo nome; uno in un namespace richiede un using nello script, oppure un global using in un qualsiasi file condiviso, che vale poi per ogni script. Un file condiviso vede il modello dati tipizzato (Order, WorkspaceDb) ma non i globali di uno script, quindi un helper che ha bisogno di Db o di Log lo riceve come argomento.

// 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();

Una modifica salvata raggiunge ogni script alla sua prossima esecuzione. L'editor controlla un file condiviso insieme agli altri file condivisi, e Go to Definition su un tipo condiviso in uno script apre il file che lo dichiara. Quando il codice condiviso non compila, gli script che lo usano falliscono e indicano il file che contiene l'errore; quelli che non lo usano continuano a girare. Una modifica a una tabella che romperebbe un file condiviso viene segnalata, insieme agli script che rompe, prima che il deployment venga eseguito.

I moduli si importano da @/ seguito dal loro percorso senza l'estensione: components/StatusChip.tsx diventa import { StatusChip } from "@/components/StatusChip". Una pagina può anche avere file propri accanto alle sue viste, aggiunti con + sopra il suo editor di codice e importati come ./OrderRow. Un modulo vede gli stessi globali del codice delle pagine ed esporta ciò che gli altri file usano. Una vista che importa può terminare esportando il suo componente come default, invece che con return. Il codice senza import né export gira esattamente come ha sempre fatto.

// 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>
  );
}

I pacchetti che il codice può importare sono quelli che l'app include già, e un import è lo stesso oggetto che contengono i globali della pagina: react, @mui/material, @mui/x-date-pickers, dayjs, lucide-react, three, @react-three/fiber e @react-three/drei. Qualsiasi altro pacchetto non c'è.

Pubblicazione: l'app pubblicata legge i moduli condivisi come li ha fissati la release corrente, quindi una modifica raggiunge gli utenti finali con la prossima pubblicazione, come succede per una pagina. Un visitatore che non ha effettuato l'accesso riceve solo i moduli importati dalle pagine pubbliche e dai layout pubblici.

Trappole: il percorso è l'identità di un file in ogni workspace, e gli import vi fanno riferimento, quindi rinominare un file rompe ciò che lo importa finché gli import non vengono aggiornati. I percorsi usano lettere, cifre, punti, underscore e trattini. Il namespace Archestack è riservato. Un alias in un global using (global using Money = decimal;) non viene passato agli script.

NuGet Packages

Librerie da NuGet per gli script C# del workspace: un driver per un altro database, un lettore di CSV o di Excel, un client per un servizio. Ogni script può usarle (Script Modules, script dei Business Events, Scheduled Events, data script dei PDF, script dei test e controlli in codice), e anche i file .cs condivisi. L'editor di script e VS Code ne completano i tipi. Un workspace che non ne aggiunge nessuno esegue i suoi script esattamente come prima.

Workflow comune: nella sidebar, sotto Logic, apri NuGet Packages e fai clic su Add NuGet package. Cercalo, oppure digita il suo id esatto quando la sorgente non supporta la ricerca, e scegli una versione esatta: l'ultima stabile è preselezionata, e Include prerelease versions elenca le altre. In Belongs to scegli a cosa appartiene: uno dei package del workspace, oppure Workspace only. Add lo scarica prima sul server, insieme alle sue dipendenze. Quando non ci riesce, il dialog dice perché e non viene salvato niente; quando funziona, mostra i namespace che porta.

In un package: un pacchetto NuGet che appartiene a un package viaggia con esso, nella sua esportazione e in ciò di cui una pipeline fa il deploy, e l'installazione di un package elenca, prima che cambi qualcosa, i pacchetti NuGet che aggiunge o di cui alza la versione. In un repository stanno in packages/<package>/nuget.json, scritto solo quando il package ne dichiara qualcuno, e VS Code controlla il file mentre scrivi. Un push che modifica il file li scarica prima, e viene rifiutato, senza cambiare niente, quando non si possono ripristinare.

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

Usarne uno: un pacchetto NuGet porta tipi, mai un globale, e niente viene importato al posto tuo. Scrivi un using per il suo namespace, che spesso non è l'id: la colonna Namespaces della pagina mostra i namespace di ogni pacchetto NuGet come righe using da copiare. Da uno script ritorna valori semplici, non gli oggetti propri della libreria.

// 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();
}

Versioni: una dichiarazione indica una sola versione esatta, come 2.1.35, mai un intervallo. Quando più package chiedono versioni diverse dello stesso pacchetto NuGet, ogni script del workspace usa la più alta. La colonna In use mostra qual è e chi l'ha chiesta.

Quale copia gira: un workspace carica una copia propria di ogni pacchetto NuGet, tenuta separata da ogni altro workspace sul server, anche quando il server contiene la stessa libreria per sé. Resta invece la copia del server quella di .NET stesso e delle librerie che gli script hanno già, come System.Text.Json, il logging, EF Core e Npgsql. Un pacchetto NuGet che chiede una versione più recente di una di queste riceve un avviso: gira sulla copia del server, e un metodo che esiste solo nella versione più recente fallisce quando viene chiamato.

Ripristini: il primo script che ha bisogno dei pacchetti NuGet su un server li fa scaricare a quel server. Aspetta fino a cinque secondi e, quando il download dura di più, gli viene chiesto di girare di nuovo tra un momento. Dopo una modifica, il workspace ripristina il nuovo insieme. Finché non ci riesce, gli script tengono l'ultimo insieme ripristinato, così un ripristino fallito non toglie mai pacchetti NuGet che funzionavano. Il banner della pagina mostra lo stato e gli errori di un ripristino fallito con la loro spiegazione. Dopo un fallimento il server riprova la prima volta che uno script ha bisogno dei pacchetti NuGet dopo l'ora indicata dal banner, oppure subito con Restore now. Resolved elenca ogni pacchetto NuGet usato dagli script, dipendenze comprese.

Simulazioni e test: una simulazione (la tab Test, Run Simulation) blocca solo ciò che passa da Archestack: le scritture tramite Db e Files vengono simulate e non salvate, e mail e notifiche non vengono inviate. Un'esecuzione di test fa le sue scritture tramite Db nella sua sandbox e blocca il resto allo stesso modo. Tutto il resto che lo script fa da sé, invece, è reale: le chiamate HTTP, System.IO e tutto ciò che fa un pacchetto NuGet, come una connessione a un altro database, quindi una scrittura tramite un driver avviene a ogni esecuzione. Mentre provi uno script del genere, puntalo a un database di test.

Trappole:

  • I pacchetti NuGet richiedono l'accesso ai dati tipizzato, come il C# condiviso. Su un server che lo disattiva (Scripting:TypedData:Enabled), la pagina lo dice e non permette di aggiungerne.
  • #r "nuget: Dapper, 2.1.35", il modo in cui altri esecutori di script ne caricano uno, qui non funziona. Aggiungilo dalla pagina.
  • Una libreria nativa portata da un pacchetto NuGet viene caricata una volta per processo del server, è condivisa dai workspace che usano lo stesso file e resta caricata finché il server non si riavvia.
  • I pacchetti NuGet Workspace only non vengono esportati con un package, non stanno nel repository e le pipeline non ne fanno il deploy. Assegna un pacchetto NuGet di cui un'app ha bisogno al package di quell'app.
  • In uno script un using var di primo livello non viene accettato dal parser, quindi chiudi tu la connessione, in un finally, come fa l'esempio.
  • Un valore letto da Entity è dynamic, e un metodo di estensione come QueryAsync di Dapper non può riceverlo come argomento: lo script non compila (CS1973). Fanne prima il cast: (string)Entity.customers_number.
  • I pacchetti NuGet che sono strumenti, template o SDK MSBuild vengono rifiutati, e così anche uno che su .NET 8 non offre niente su cui gli script possano compilare, o uno che estende il driver PostgreSQL proprio del server o EF Core. Gli id che si chiamano OmniCore o Archestack, o che iniziano con uno dei due seguito da un punto, sono riservati alla piattaforma.

Per chi gestisce i server: i pacchetti NuGet sono attivi per impostazione predefinita e sempre disattivati in un trial. Le impostazioni stanno sotto Scripting:NuGet, oppure nelle variabili d'ambiente Scripting__NuGet__Enabled e così via. Ogni server scarica per conto proprio, quindi ognuno deve raggiungere nuget.org o i feed configurati qui, tramite il proxy in https_proxy, http_proxy e no_proxy quando ce n'è uno. Dai a tutti i server di un'installazione le stesse sorgenti.

ImpostazionePredefinitoCosa fa
EnabledtrueSe disattivato, non si scarica nulla e gli script non ne caricano nessuno. La pagina non può aggiungerne; le dichiarazioni portate da un'importazione o da un push restano, inutilizzate.
Sourcesnuget.orgDa dove arrivano i pacchetti NuGet, separati da ;: feed indicati dal loro service index v3 (l'URL che finisce con index.json), oppure cartelle assolute, da cui i pacchetti vengono copiati e che sono considerate attendibili: i limiti di download valgono solo per i feed. nuget.org viene usato solo quando né questa impostazione né Feeds indicano una sorgente.
Feeds:<name>:Url, Username, PasswordNessunoUn feed con un nome, per uno che richiede credenziali. L'URL non contiene mai un nome utente o una password.
Feeds:<name>:PatternsNessunoGli id che questo feed serve, separati da ; (Contoso.*). Quando un feed qualsiasi ha dei pattern, un id arriva dal feed il cui pattern gli corrisponde, e un feed senza pattern serve tutto il resto.
Feeds:<name>:AllowInsecureConnectionsfalseLascia passare un feed in semplice http://.
DirectoryUna cartella nei dati applicativi localiDove questo server tiene i pacchetti NuGet che ha scaricato: una cartella per server sul suo disco locale, mai un volume condiviso tra server. In un container, dagli un volume proprio di quel server, altrimenti ogni nuovo container li scarica di nuovo.
MaxPackageBytes, MaxExtractedBytes100 MB, 1 GBIl pacchetto NuGet più grande che un ripristino scarica, e lo spazio massimo che un pacchetto NuGet può occupare una volta estratto.
MaxRestoreBytes, MaxRestoreExtractedBytes, MaxPackages500 MB, 2 GB, 150Il massimo che un ripristino scarica, estrae e conta in pacchetti NuGet, tutti insieme.
MaxCacheBytes10 GBOltre questa soglia la cartella non accetta nuovi ripristini, e i pacchetti NuGet che questo server non sta usando vengono rimossi, a partire da quelli usati meno di recente.
MinFreeDiskBytes1 GBCon meno spazio libero su disco di così un ripristino non parte, e un download che ne lascerebbe meno viene interrotto.
RestoreTimeoutSeconds, AddWaitSeconds, MaterializeWaitMilliseconds300, 120, 5000Quanto può durare un ripristino, quanto l'aggiunta di un pacchetto NuGet aspetta il suo download, e quanto uno script aspetta il download di questo server prima che gli venga chiesto di girare di nuovo.
AllowNativeAssetstrueSe disattivato, un pacchetto NuGet che contiene librerie native viene rifiutato, e nessuna viene mai caricata.
AuditRefuseLevelVuotolow, moderate, high o critical: un pacchetto NuGet con una vulnerabilità nota di quel livello o superiore viene rifiutato. Vuoto non ne rifiuta nessuno, e le vulnerabilità compaiono come avvisi; qualsiasi altro valore fa fallire ogni ripristino che aggiunge un pacchetto NuGet.
CertificateRevocationModeonlineonline o offline: come viene controllata la revoca dei certificati di firma dei pacchetti NuGet.

Frontend Templates

Frammenti UI riutilizzabili scritti in TSX che il page editor può inserire in una pagina. Utili quando i template configurati non bastano, grafici su misura, layout insoliti, integrazioni con widget di terze parti.

Workflow comune: fai clic su Create, nomina il template, scrivi un componente TSX nell'editor. Il componente riceve props: record (la riga corrente quando incorporato in un form di dettaglio), refresh (ri-recupera i dati), e qualche helper. Salva. Referenzia il template da un campo del Page Editor.

Pacchettizzabile: i template fluiscono attraverso il sistema Package come qualsiasi altro oggetto config, vengono inclusi in cascading quando una pagina che li usa viene aggiunta a un package.

Trappole: il TSX non può importare pacchetti npm arbitrari: importa il codice condiviso da @/ e i pochi pacchetti che l'app include.

PDF Templates

HTML + template Scriban + un piccolo data script C# che renderizza in un PDF (Chromium headless). Ogni template è invocabile per nome da uno script via REST o direttamente da un pulsante su una Page (con un piccolo adattatore, vedi sotto).

Workflow comune: fai clic su + Create. L'editor si apre con i campi testo Name e Description in alto, cinque tab sul lato sinistro (HTML Template, Data Script, Settings, Params, JSON), e un pannello PDF Preview sempre attivo sulla destra. Le tab Data Script e HTML Template aprono ciascuna un editor Monaco sulla metà sinistra; l'anteprima si rigenera quando salvi (pulsante verde Update in alto a destra).

Dichiara i parametri nella tab Params tramite il pulsante + Add (Name, Type, Required, Default Value). L'etichetta della tab si aggiorna a Params (n) con il conteggio. Page format / orientation / margins / scale / header / footer vivono nella tab Settings.

Forma del data script (vedi tutorial PDF Invoices per uno reale):

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 })
};

Forma del template HTML (Scriban, simile a Liquid/Handlebars):

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

Encoding HTML: ogni valore {{ }} viene codificato in HTML, quindi un valore di un record che contiene <b> viene stampato come il testo <b>. html.raw è l'opt-out esplicito, solo per HTML fidato: {{ notes | html.raw }}. Non usarlo mai su valori che gli utenti finali possono digitare, perché così il loro markup finisce nel documento renderizzato. Solo il template può disattivare l'encoding, i dati no. html.escape e html.newline_to_br continuano a funzionare (newline_to_br fa l'escape del testo ma mantiene i tag <br />).

Pannello di anteprima: sempre visibile sul lato destro dell'editor. Si rigenera quando salvi (pulsante verde Update). Include una colonna di thumbnail per i PDF multi-pagina e una toolbar di viewer integrata (zoom, ruota, download, stampa). I pulsanti icona in alto a destra dell'editor alternano la visibilità del pannello di anteprima e l'editing a tutto schermo.

Chiamarlo da fuori dall'editor:

  • POST /api/v1/pdf-templates/{id}/preview - renderizza per ID, ritorna application/pdf. Usato dal pannello di anteprima dell'editor.
  • POST /api/v1/pdf-templates/generate/{name} - renderizza per nome, ritorna application/pdf.
  • POST /api/v1/pdf-templates/generate/{name}/base64 - stesso ma ritorna { "data": "<base64>" }.

Passa i valori dei parametri nel body JSON. L'auth è il Bearer token standard.

Limitazione del trial: sia l'endpoint generate sia l'endpoint {id}/preview (usato dal pannello di anteprima dell'editor) ritornano 403 nella modalità trial, il rendering PDF è interamente disabilitato. Puoi comunque creare il template (data script, HTML, settings, parametri) e salvarlo; semplicemente non vedrai un PDF renderizzato finché non sei su un ambiente a pagamento.

Nota sul rendering chiamabile da script: il contesto di scripting C# espone eccome un globale Pdf, quindi uno Script Module renderizza un template salvato direttamente con await Pdf.GenerateAsync("invoice", ...) o Pdf.GenerateBase64Async(...). Non serve alcun giro con HttpClient verso l'endpoint generate. (L'autocomplete dell'editor di script reclamizza ancora Pdf.RenderAsync e un oggetto Templates; nessuno dei due nomi esiste a runtime.)

Trappole: Scriban fa l'escape dell'HTML di default. I page break sono guidati dal CSS, page-break-before: always su una sezione. I font disponibili sul server di rendering sono limitati ai font di sistema più le famiglie Noto e Liberation, incorpora i tuoi via @font-face base64 se ti serve un font di brand.

Immagini, font e fogli di stile: il renderer carica risorse solo da indirizzi http(s) pubblici, più gli URL data: e blob:. Gli indirizzi interni e di rete privata (localhost, 10.x, 192.168.x, nomi dei servizi dei container e così via) sono bloccati, quindi un'immagine servita da un servizio interno non comparirà nel PDF. Usa un URL pubblico o un URL data:.

Scheduled Events

Lo stesso sistema di Event Trigger dei Business Events, con il trigger On Schedule spuntato. Usalo per ricalcoli notturni, digest settimanali, pulizie periodiche, report mensili.

Workflow comune: crea un Business Event con il trigger On Schedule (invece di, o in aggiunta ai, trigger di cambio dati). Configura l'espressione cron. Aggiungi un'action Execute Script il cui corpo invoca uno Script Module via Modules.CallAsync(...).

Espressioni cron: in stile Quartz a 6 o 7 campi, vedi Concetti -> Scheduled Events per i pattern comuni. Gli orari sono il server time (UTC sullo stack di produzione).

Trappole: un job lungo che eccede il suo intervallo cron non si raddoppia, Quartz non fa scattare una seconda istanza dello stesso job in concorrenza, il secondo tick viene saltato. Le esecuzioni fallite non riprovano automaticamente; costruisci il retry nello script.

Packages

Bundle esportabili di oggetti di configurazione (BE, pagine, event, script, template, schema) con dipendenze in cascading. Aggiungere una Page a un package porta automaticamente tutto ciò che quella pagina referenzia.

Workflow comune (export): fai clic su Create, nomina e versiona il package. Aggiungi gli oggetti di livello superiore che ti interessano, di solito una manciata di pagine. Apri il pannello degli item collegati per vedere la lista in cascading (cosa conterrà effettivamente il package). Despunta qualsiasi cosa tu voglia omettere. Decidi se includere i dati (toggle all'export). Fai clic su Export, ricevi uno ZIP.

Workflow comune (import): sull'ambiente di destinazione, apri Packages, fai clic su Import, carica lo ZIP. La piattaforma mostra cosa sarà aggiunto o aggiornato. I delta di schema non sono auto-applicati, se il package referenzia una colonna che non esiste sulla destinazione, l'import fallisce.

Trappole: gli identificatori sono per nome, non per ID numerico. Una BE chiamata "customer" sulla sorgente si collega a una BE chiamata "customer" sulla destinazione, rinomina da una delle due parti e il link si rompe.

In git: l'estensione VS Code scarica un package in una cartella, un file per artefatto, e rimanda indietro la cartella, così un package si può rivedere e versionare come qualsiasi altro codice.

Third-Party Data Connections

Connessioni a database esterni (Postgres, SQL Server, MySQL, altri) che importano tabelle su pianificazione. Le tabelle importate si comportano come tabelle native, appaiono in Schema Designer e possono essere referenziate da Business Entities, Pages, e Scripts.

Workflow comune: crea una connessione (stringa di connessione, test, salva). Elenca le tabelle remote disponibili; scegli quali importare. Per ogni tabella importata, imposta la modalità di sync (Full / Delta) e il trigger di sync (cron / manuale / intervallo fisso).

In mirror, non federata: i dati importati vivono nel tuo omnicore-db. Le letture sono veloci (Postgres locale). Compromesso: obsolescenza tra i sync.

Colonne gestite dall'app: marca alcune colonne come "managed by Archestack", non saranno sovrascritte dai sync futuri. Utile quando vuoi annotare dati del vendor con i tuoi flag di stato.

Trappole: il primo sync di una tabella grande può richiedere un po' di tempo. I sync delta richiedono una colonna che il connettore può usare come watermark, di solito un timestamp modified_at.

AI Assistant

Un assistente chat integrato nell'app che può sia rispondere a domande sui tuoi dati sia modificare il tuo ambiente. Chiedi "quanti ordini di lavoro aperti per sede questo mese?" e interroga e risponde. Chiedi "aggiungi una colonna stato agli appuntamenti di servizio e una pagina per elencarli" e, se sei amministratore, lo costruisce.

Porta il tuo modello. Archestack non include alcun LLM e non paga alcuna inferenza. Un amministratore aggiunge un provider nella schermata AI Configuration: una base URL, un nome di modello e una chiave API. Funziona tutto ciò che parla l'API chat-completions di OpenAI (ChatGPT, DeepSeek, OpenRouter, Groq, Together, un Ollama o vLLM self-hosted), e l'API nativa di Anthropic è supportata direttamente. Puoi configurarne diversi e sceglierne uno per conversazione.

Tutto ciò che fa passa dall'MCP Server. L'assistente non ha una porta di servizio privata verso la piattaforma, chiama gli stessi tool di qualsiasi client MCP esterno, con la tua identità. I limiti che imposti lì valgono quindi anche per lui, e ogni azione finisce nello stesso audit log. Vedi MCP Server qui sotto.

Workflow comune: apri l'assistente dalla sidebar o dalla bolla fluttuante. Chiedi in una o due frasi. Legge il tuo schema attuale prima di proporre qualcosa, dice cosa sta per cambiare e aspetta la tua conferma. Ogni chiamata a un tool appare sotto la risposta, così vedi cosa ha fatto davvero.

Funziona bene per: domande estemporanee sui tuoi dati, funzionalità in forma CRUD, automazioni semplici, estensioni di entity esistenti.

Meno bene per: regole di business non ovvie e workflow multi-step complessi. Tratta ciò che costruisce come la PR di uno junior dev.

Trappole: lavora su dati di produzione e non c'è undo, quindi fai un backup prima di qualcosa di rischioso. I tuoi prompt e ciò che i tool restituiscono vengono inviati al provider che hai configurato, secondo le sue condizioni; puntalo su un modello self-hosted se la cosa conta. Le conversazioni lunghe ripropongono solo i turni più recenti, quindi una chat lasciata aperta tutto il giorno non ne ricorda l'inizio. E nota i due interruttori: con l'MCP Server fermo l'assistente può ancora parlare, ma non può leggere né cambiare nulla.

MCP Server

Ogni ambiente può esporsi tramite il Model Context Protocol, così che un client AI esterno ad Archestack, Claude Desktop, Claude Code, ChatGPT deep research, un IDE o un tuo script, possa lavorarci direttamente. È anche ciò su cui gira l'AI Assistant integrato.

Spento di default, e i tool che cambiano come è costruita l'applicazione restano spenti anche dopo averlo avviato. È deliberato.

Tre gruppi di tool. Read copre l'elenco e la descrizione delle entity, l'interrogazione delle righe e la lettura di schema, pagine, event, script modules, packages, translations ed event logs. Write è creare, aggiornare ed eliminare record, e passa dallo stesso percorso della UI, quindi i tuoi Business Events scattano esattamente come per una persona che clicca Salva. Admin è il gruppo che permette a un modello di costruire l'applicazione stessa: entity, schema, pagine, event, script modules, translations, packages e i pacchetti NuGet usati dagli script.

Quattro cancelli decidono se una chiamata va a segno: il server è avviato, il gruppo è consentito, quello specifico tool è abilitato, e il token porta lo scope. Un tool bloccato da uno qualsiasi di questi è invisibile al client, invece di essere rifiutato dopo.

Token. Emettine uno per client nella schermata MCP Server, con gli scope che gli servono davvero e una scadenza facoltativa. Il segreto viene mostrato una sola volta, alla creazione, e viene memorizzato solo un hash: un token perso si ruota, non si recupera. Dai a ogni integrazione il proprio token: mantiene leggibile l'activity log, e revocarne uno non disturba gli altri.

Activity log. Ogni invocazione viene registrata, comprese quelle rifiutate e il motivo. È qui che controlli cosa ha fatto davvero un'integrazione.

Trappole: un token con lo scope admin viene trattato come amministratore a tutti gli effetti da tutto il resto, quindi distribuiscili con parsimonia. "Allow token in URL" esiste per i client che non possono inviare un header Authorization, ma un URL finisce nei log e negli screenshot molto più facilmente di un header: lascialo spento se non ti serve. E su un ambiente di produzione con dati reali, lascia spento il gruppo admin.

Object Browser

Vista raw di ogni tabella nativa e importata, aggiungi, modifica, elimina righe direttamente. Include un import CSV per caricamenti bulk.

Quando usarlo:

  • Caricamento bulk di dati di riferimento prima che le pagine esistano (paesi, valute, enum di stato).
  • Debugging, vedere a occhio cosa c'è effettivamente in una tabella quando una pagina si comporta male.
  • Edit admin veloci a dati non ancora esposti tramite una Page.
  • Import CSV: scegli una tabella, carica un file, mappa le colonne ai campi, anteprima, commit.

Trappole: Object Browser fa scattare i Business Events (passa attraverso lo stesso write path di EntityService del front end). L'eccezione è l'SQL diretto via Database Deployments, quello bypassa tutto.

Dependency Graph

Una mappa in sola lettura di come gli oggetti di configurazione nel tuo ambiente si referenziano a vicenda. Otto tipi di risorsa appaiono come nodi, colorati per tipo: Business Entities, Pages, Event Triggers, Script Modules, PDF Templates, Frontend Templates, Scheduled Events e Packages. Un arco tra due nodi significa che una risorsa dipende dall'altra. Usalo per rispondere alle domande "cosa si romperebbe se elimino questo?" e "c'è qualcosa di inutilizzato qui?".

Cosa significa un arco: una Page collegata a una Business Entity, un Event Trigger registrato su una Business Entity, uno step di azione di Page che fa scattare un Event Trigger, uno script che chiama un altro Script Module tramite Modules.CallAsync("Name"), uno script che renderizza un PDF Template, uno Scheduled Event che fa scattare un Event Trigger, o un Package che contiene una risorsa. È una vista di design-time, riflette ciò che la tua configurazione collega insieme, non il traffico runtime dal vivo.

Due tipi di problema vengono fatti risaltare visivamente:

  • Loose ends (orfani) - nodi che niente referenzia e che non referenziano niente. Vengono raccolti in una banda "Loose ends" bordata di rosso in fondo al canvas. Uno Script Module che nessuno chiama; una Page collegata a niente.
  • Dangling references - disegnate come archi tratteggiati rossi. Il riferimento esiste nella configurazione ma il suo target non può essere trovato, per esempio uno step di azione di Page che punta ancora a un Event Trigger eliminato, o uno script che chiama uno Script Module con un nome che non esiste più.

Workflow comune: apri Dependency Graph dalla sezione Tools della sidebar. Il pannello di sinistra ha una ricerca per nome, un selettore Layout (Left to right, Top to bottom, By type, Force-directed), gli interruttori Group by package, Show loose ends e Show edges, e una checklist per tipo. Clicca su un nodo per entrare in modalità focus: quel nodo e i suoi vicini diretti restano luminosi mentre il resto del grafo si attenua, così puoi tracciare esattamente cosa tocca una risorsa. Clicca sullo sfondo del canvas per cancellare il focus. Ogni nodo porta un piccolo pulsante di apertura in una nuova scheda che salta direttamente all'editor di quella risorsa. La riga di statistiche in alto riporta il numero di nodi, archi, orfani e dangling references.

Group by package: avvolge i membri di ogni package in un riquadro tratteggiato etichettato, così puoi vedere un package come un'unità e individuare le risorse che ha tirato dentro per cascata.

Trappole: il grafo viene costruito su richiesta a partire dallo stato corrente, usa il pulsante di refresh dopo aver modificato risorse altrove. Gli archi derivati dagli script vengono trovati scansionando i corpi degli script per la forma di chiamata letterale Modules.CallAsync("Name"), un module invocato tramite un nome calcolato non mostrerà un arco. Una dangling reference vale sempre la pena di essere corretta; un orfano spesso no, uno Script Module nuovo di zecca conta come orfano finché qualcosa non lo chiama.

Event Logs

L'audit trail per tutto ciò che la piattaforma ha eseguito, Business Events, Script Modules, Scheduled Events, sync di terze parti.

Workflow comune: apri Event Logs, imposta Status su Failed, fai clic su una voce. Il pannello di dettaglio mostra il payload (stato del record al momento, parametri passati), l'eccezione con stack trace, e i tempi.

Logs e Charts: la pagina ha due schede che condividono un'unica riga di filtri. Logs è la tabella qui sopra. Charts è una dashboard operativa sullo stesso filtro: il volume nel tempo impilato per stato, un andamento del tasso di successo, una ripartizione per source, gli events che falliscono di più e i più lenti, la durata media e massima nel tempo, una ripartizione per tipo di trigger, e una heatmap giorno della settimana per ora del giorno, con i totali principali in alto. Entrambe le schede condividono un intervallo di date Da / A (con i preset Ultime 1h / 24h / 7d); la scheda Charts sceglie automaticamente un intervallo di tempo adatto al periodo e può aggiornarsi a cadenza regolare per il monitoraggio in tempo reale.

Notifiche in tempo reale: l'icona campanella nella barra in alto dell'app admin emerge i failure non letti via SignalR.

Trappole: il log può crescere molto. Le voci vecchie sono tenute per sempre di default; se il disco è un problema, configura uno Scheduled Event che pota le voci più vecchie di N giorni.

User Management

Crea, invita e gestisce utenti; assegna ruoli realm. Fa da wrapper all'API REST admin di Keycloak.

Ruoli:

  • admin - tutto; può gestire tutte le Business Units, utenti, ruoli, impostazioni di sistema.
  • owner - gestisce le proprie Business Units, può modificare le pagine di amministrazione e di runtime.
  • editor - può vedere e modificare le pagine di runtime, ma non cambiare la configurazione di schema/BE/event.
  • user - read-only sulle pagine di runtime.

Trappole: declassare un utente da admin a metà sessione non lo butta fuori, il cambiamento si applica al prossimo refresh del token (tipicamente entro un minuto).

Business Units

Gruppi basati su Keycloak che controllano la visibilità sulle singole risorse. Vedi Concetti -> Business Units per il modello.

Workflow comune: crea una BU. Invita i membri per email e scegli il ruolo. Assegna le risorse andando su ogni BE / Page / Trigger oppure tramite il pannello degli item collegati della BU.

Assegnazioni in cascading: quando assegni una Page a una BU, l'assegnazione si propaga alla BE a cui la pagina si collega.

Trappole: la BU attiva di un utente è impostata nello switcher BU della barra in alto e persiste in localStorage. I nuovi utenti partono dalla loro prima BU; se una pagina lista è sospettosamente vuota, controlla quale BU è attiva.

Database Backups

pg_dump notturno di tutti e tre i database, più uno ZIP dei file del workspace. Trigger manuale / upload / restore dalla UI.

Workflow comune: Trigger now -> Download -> Upload -> Restore. Lo scheduling di default è notturno alle 03:00 UTC.

Limitazione del trial: i Backup sono disabilitati nell'ambiente trial.

Branding

Cambia il display name dell'app, il logo (chiaro + scuro), i colori, i font e il resto del tema Material UI, applicato a runtime, niente rebuild.

Workflow comune: imposta il nome dell'app, carica un logo chiaro e uno scuro (PNG/JPG/SVG, ≤ 10 MB), scegli i colori del brand nella scheda Theme. Salva: la tua sessione lo mostra subito, quelle degli altri al prossimo caricamento della pagina.

Tema: la scheda Theme è un editor completo di temi Material UI con anteprima dal vivo in modalità chiara e scura: i colori del brand e ogni colore della palette per modalità, i font (qualsiasi Google Font) e ogni stile di testo, il raggio degli angoli, la spaziatura, le ombre, la velocità delle animazioni, lo stile di pulsanti, card e campi, i valori predefiniti dei componenti e i loro override in JSON. Controlla il contrasto, parte da preset, importa un tema da un'altra app Material UI ed esporta questo come codice. Show on the whole app prova una bozza su tutte le schermate prima di salvare. Un workspace che non cambia mai il suo tema mantiene esattamente l'aspetto integrato. In un repository il tema è la chiave theme di workspace/branding.json.

Trappole: il logo è anche l'icona della scheda del browser e l'icona che un telefono mostra per l'app nella schermata home. La schermata home vuole un logo PNG, JPG o WebP; con un SVG mostra l'icona di Archestack.

Analytics

Google Analytics sull'app pubblicata, configurato nella pagina Analytics (Configuration). Il builder non viene mai tracciato.

Flusso tipico: incolla l'ID di misurazione GA4 (G-XXXXXXXXXX, in Amministrazione, Stream di dati di Google Analytics), spunta Track visits to the published app e salva. Ask visitors for consent first è attivo per impostazione predefinita: il tag di Google si carica solo dopo che un visitatore ha accettato il banner, come chiedono le regole UE sui cookie. Il testo del banner, i suoi pulsanti e un link alla tua informativa sulla privacy possono essere i tuoi.

Eventi dal codice di pagina: una volta caricato il tag, il codice di pagina può inviare eventi propri, come window.gtag?.("event", "sign_up"). Non viene inviato nulla finché il tracciamento è spento o il visitatore ha rifiutato.

Translations

Editor key/value per le stringhe UI dell'app. Aggiungi nuovi locale, sovrascrivi le label di default per cliente.

Workflow comune: scegli un locale, cerca una chiave, digita un nuovo valore, salva. Aggiorna l'app admin.

Trappole: le traduzioni sono caricate una volta per sessione; gli utenti su una tab vecchia non vedono i cambiamenti finché non aggiornano. Alcune label (intestazioni di colonna dalle BE) vengono dalla config della BE, non dalle traduzioni.


API chiamabili dagli script

L'esatta superficie che gli script possono chiamare. Verificata contro il sorgente, ogni metodo qui sotto esiste davvero.

Globali

Ogni script riceve queste variabili di livello superiore (nessun altro nome esiste nello scope dello script):

  • Entity - dynamic ExpandoObject. Record corrente. Accedi ai campi con Entity.column_name. Mutare Entity in un trigger Before* persiste le modifiche come parte della scrittura in corso.
  • OldEntity - dynamic ExpandoObject. Record precedente (trigger di update + delete). Read-only.
  • Log - ILogger. Log.LogInformation("..."), Log.LogWarning(...), Log.LogError(...). Scrive sui log server e sulla voce dell'Event Log.
  • Db - DbHelper. Accesso al database: un set tipizzato per ogni Business Entity, e gli helper non tipizzati (vedi sotto).
  • Modules - chiama altri Script Modules. Ogni modulo è un metodo con i propri parametri: await Modules.OrdersOf(customer: "Acme"), controllato quando lo script viene compilato; il risultato è dynamic. await Modules.CallAsync("OrdersOf", new Dictionary<string, object?> { ["customer"] = "Acme" }) chiama ancora un modulo per nome e ritorna Task<object?>.
  • Pdf - ScriptPdfHelper. Renderizza un PDF Template salvato per nome: await Pdf.GenerateAsync("invoice", new Dictionary<string, string> { ["id"] = Entity.id.ToString() }) ritorna byte[]; await Pdf.GenerateBase64Async(...) ritorna lo stesso payload codificato in base64. I parametri sono inoltrati al data script del template come variabili locali tipo stringa.
  • Ocr - ScriptOcrHelper. Legge il testo stampato da un'immagine, in-process, senza servizio esterno e senza API key: await Ocr.ExtractAsync(byte[] image) ritorna un OcrExtraction. await Ocr.ExtractBase64Async(string) prende la stessa immagine codificata in base64 (un prefisso data:image/png;base64, è tollerato), await Ocr.ExtractTextAsync(byte[]) ritorna solo il testo.
  • Doc - ScriptDocHelper. Trova il documento in una foto e lo appiattisce: await Doc.CropAsync(byte[] image) ritorna un DocumentCrop. await Doc.CropBase64Async(string) prende una cattura in base64.
  • Files - ScriptFilesHelper. La cartella di file propria del workspace, quella gestita dalla pagina Files: await Files.WriteAsync("invoices/1042.pdf", bytes), await Files.ReadAsync(path), ListAsync(folder), MoveAsync, DeleteAsync. Vedi Files.

Non c'è User, Http, Email, Templates, o nessun altro globale.

Db - metodi single-row + di scrittura

Tutti async, tutti sull'oggetto di livello superiore Db:

  • await Db.GetAsync(string table, object id) - prende una riga per primary key. Ritorna dynamic? (o null se non trovata).
  • await Db.CreateAsync(string table, object values) - insert. values può essere un oggetto anonimo o un Dictionary<string, object?>. Fa scattare i trigger Before/After Create. Ritorna int (il nuovo ID).
  • await Db.UpdateAsync(string table, object id, object values) - update per PK. Fa scattare i trigger Before/After Update.
  • await Db.DeleteAsync(string table, object id) - delete per PK. Fa scattare il trigger Before Delete.

Db - query builder fluente

Parti con Db.From(table). Concatena filtri, poi un terminale:

  • .Where(string column, string op, object? value = null) - aggiunge una clausola WHERE. Operatori: =, != / <>, >, >=, <, <=, LIKE, ILIKE, IN, IS NULL, IS NOT NULL. Concatena più chiamate .Where(...) per AND.
  • .OrderBy(string column, bool desc = false) - imposta ORDER BY. Solo uno consentito per query.
  • .Limit(int limit) - massimo di righe. Default è 1000.

Terminali (ciascuno esegue la query):

  • await ...ToListAsync() - ritorna List<dynamic>.
  • await ...FirstAsync() - ritorna dynamic? (primo match o null). Imposta temporaneamente il limit a 1.
  • await ...CountAsync() - ritorna int.

Non c'è SumAsync, MaxAsync, FirstOrDefaultAsync, o SingleAsync su Db.From. Per le aggregazioni, recupera con ToListAsync() e riduci in C#, oppure usa i set tipizzati qui sotto, che li hanno tutti.

Esempio pratico

// 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 - Business Entities come set tipizzati

Ogni Business Entity è anche un set tipizzato su Db, con il nome dell'entity senza gli spazi: un'entity chiamata Service Order è Db.ServiceOrder. Sotto c'è EF Core, quindi funziona tutto LINQ (Where, Include, SumAsync, GroupBy, proiezioni), con una proprietà di navigazione per ogni foreign key e per ogni join di una Business Entity. Le colonne sono proprietà PascalCase con i loro tipi reali: planned_date è 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;
  • Nomi. Una tabella su cui non è costruita nessuna Business Entity riceve un set con il nome della tabella (bank_account è Db.BankAccount). Una colonna foreign key senza il suffisso _id è la proprietà di navigazione: customer_id è Customer, e l'altro lato è il plurale, Customer.Orders.
  • Salvataggio. await Db.SaveChangesAsync() scrive ogni riga aggiunta, modificata o rimossa come fanno CreateAsync, UpdateAsync e DeleteAsync, quindi i Business Events scattano e le colonne di audit vengono marcate, e una riga modificata invia solo le colonne cambiate. Non è un'unica transazione: le righe salvate prima di un errore restano salvate. Le scritture con ExecuteUpdate, ExecuteDelete o SQL raw vengono rifiutate.
  • Tipi. Un timestamp with time zone si legge come un DateTime UTC, e anche un valore confrontato con esso deve essere UTC: DateTime.UtcNow. Le colonne di un tipo che il modello non mappa (array, enum, tipi geometrici) vengono omesse; leggile con Db.From.
  • Sempre aggiornato. Il modello viene ricostruito ogni volta che cambia una tabella o una Business Entity, quindi una colonna rimossa è un errore di compilazione sulla sua riga invece di un errore a runtime. Un deployment in sospeso elenca, prima che tu lo esegua, gli script tipizzati che smetterebbero di compilare.

Db.From e i metodi qui sopra continuano a funzionare accanto ai set tipizzati, e l'editor di script e VS Code completano i set, le colonne e le proprietà di navigazione.

Modalità simulazione

Durante la simulazione del trigger (il pulsante Run Simulation su un Business Event) e nella tab Test di uno Script Module, Db.SimulationMode è true. Le scritture (CreateAsync, UpdateAsync, DeleteAsync e SaveChangesAsync) controllano la tabella, la riga e i valori, registrano quello che avrebbero fatto e non scrivono niente, quindi non scatta nemmeno alcun Business Event. Le letture vedono i dati reali. Anche scritture, spostamenti ed eliminazioni tramite Files vengono simulati, e mail e notifiche vengono registrate, non inviate. È questo che rende Run Simulation sicuro da usare contro dati reali. Tutto il resto che lo script fa da sé, invece, è reale: le chiamate HTTP, System.IO e tutto ciò che fa un pacchetto NuGet, come una connessione a un altro database.

Modules

  • await Modules.OrdersOf(customer: "Acme") - ogni Script Module è un metodo chiamato come il modulo senza spazi ("Orders of" diventa Modules.OrdersOf), che prende i parametri che dichiara con i loro tipi, prima quelli obbligatori; un parametro opzionale omesso riceve il suo valore predefinito. Un nome scritto male o un valore del tipo sbagliato è un errore di compilazione sulla sua riga. Ritorna Task<dynamic?>, quindi leggi il risultato senza cast, anche un tipo anonimo. Gli argomenti letti da Entity sono dynamic: fanne il cast ((int)Entity.company_id) perché la chiamata venga controllata in compilazione.
  • await Modules.CallAsync(string moduleName, Dictionary<string, object?>? parameters = null) - chiama un altro Script Module per nome. I parametri diventano variabili tipizzate nello script del modulo chiamato. Ritorna Task<object?> (qualunque cosa il modulo abbia ritornato).

Files - la cartella del workspace

Ogni workspace ha sul server una cartella propria, gestita dalla pagina Files (Operations). Lì arrivano i caricamenti, e anche tutto ciò che uno script conserva: una fattura generata, un'esportazione, un modello addestrato. Niente di tutto questo è salvato in un database, e nessun percorso raggiunge la cartella di un altro workspace. I percorsi sono relativi e usano la barra normale; un percorso con .., un'unità o un collegamento viene rifiutato.

// 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) ritorna i byte e lancia un'eccezione se il file non esiste; ReadTextAsync legge testo UTF-8; ReadDataUriAsync ritorna data:image/png;base64,… per un'immagine in un PDF Template o in una mail.
  • WriteAsync(path, bytes) e WriteTextAsync creano le cartelle e sostituiscono un file esistente. Una scrittura riesce per intero o per niente.
  • ListAsync(folder, recursive) elenca prima le cartelle; ogni voce ha Path, Name, IsFolder, Size, ModifiedUtc e ContentType.
  • MoveAsync(from, to) non sovrascrive mai; DeleteAsync(path) elimina un file, o una cartella con tutto il suo contenuto.
  • PathOf(path) è il percorso assoluto su disco, per una libreria che ne vuole uno.

In una prova a vuoto (il pulsante Test) e in un'esecuzione dei test, scritture, spostamenti ed eliminazioni non raggiungono mai la cartella: l'esecuzione li tiene per sé, quindi rileggere un file appena scritto funziona, e li elenca insieme a ciò che ha registrato. La cartella .archestack contiene i file propri della piattaforma (loghi, icone dei pacchetti, allegati delle mail): gli script possono leggerla, mai scriverla. Un backup include la cartella come archivio workspace-files accanto ai dump dei database.

Ocr e Doc - leggere una foto

Entrambi girano dentro il processo dell'API. Nessun servizio OCR da deployare, nessuna chiave da configurare, nessun costo per pagina.

OcrExtraction (quello che ritorna Ocr):

  • Text - ogni riga riconosciuta unita da newline, in ordine di lettura.
  • Lines - per riga Text, Confidence e un bounding box: X, Y, Width, Height, più Right e Bottom. Pixel dell'immagine sorgente, origine in alto a sinistra.
  • Confidence - media delle confidenze per riga, da 0 a 1. ElapsedMs, IsEmpty.

Usa i box. Leggere un documento significa localizzare un'etichetta e poi leggere ciò che le sta accanto, cosa che il solo testo non può esprimere: trova la riga che contiene "Totale", poi prendi la riga il cui intervallo Y la sovrappone e la cui X supera il suo Right.

DocumentCrop (quello che ritorna Doc):

  • Found - se un contorno è stato davvero rilevato. Controllalo per primo.
  • Image - byte PNG della pagina appiattita, o i byte originali intatti quando Found è false. Una foto mai ritagliata si legge comunque; una ritagliata sul quadrilatero sbagliato no, quindi il rilevatore preferisce rinunciare piuttosto che tirare a indovinare.
  • Corners - il contorno nella foto sorgente, alto-sinistra, alto-destra, basso-destra, basso-sinistra. Width, Height, ElapsedMs.

I due si compongono nell'ordine ovvio, e poiché un ritaglio fallito restituisce l'originale, concatenarli fa sempre qualcosa: var crop = await Doc.CropAsync(photo); var scan = await Ocr.ExtractAsync(crop.Image);

Tre limiti da considerare in fase di progetto. Il modello incluso è PP-OCRv5 latin, quindi alfabeti occidentali e cifre, non CJK. Non è un lettore di PDF, una pagina PDF va prima rasterizzata in immagine. E questo è OCR, non comprensione del documento: trasformare le righe in fornitore, numero fattura e totale è logica che scrivi tu, e a volte sbaglierà, quindi controlla line.Confidence e manda le letture basse a una persona invece di scriverle dritte in tabella.

Attenzione al timeout dello script. Una pagina intera richiede un secondo o due e la prima chiamata nel processo aggiunge circa un secondo per caricare i modelli. Il timeout di dieci secondi non interrompe un await, quindi uno script che legge più pagine non ritorna niente invece di fallire presto. Fai il lavoro multipagina in uno Scheduled Event, una pagina per esecuzione.

Template expression (action dei Business Event)

La configurazione delle action usa parentesi {{ ... }}. La lista completa dei token validi:

  • {{ Entity.column_name }} / {{ Entity.id }}
  • {{ OldEntity.column_name }}
  • {{ now() }} / {{ getdate() }} - datetime UTC ISO 8601
  • {{ today() }} - stringa di data
  • {{ guid() }} / {{ newid() }} - GUID fresco
  • {{ year() }}, {{ month() }}, {{ day() }}, {{ timestamp() }} - secondi Unix
  • {{ empty() }}, {{ null() }}
  • Aggregati dentro un contesto di join: {{ SUM(column) }}, {{ AVG() }}, {{ COUNT() }}, {{ MIN() }}, {{ MAX() }}
  • Aritmetica: {{ Entity.quantity * Entity.price }} - valutata post-sostituzione via DataTable.Compute()

Non c'è un {{ user.email }} o altro token di scope utente.