wuic-framework-lib 1.7.9 → 1.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/fesm2022/{wuic-framework-lib-chart-list.component-Bz93lddX.mjs → wuic-framework-lib-chart-list.component-BM4iMVEF.mjs} +2 -2
  2. package/fesm2022/{wuic-framework-lib-code-editor.component-CerH_Ptt.mjs → wuic-framework-lib-code-editor.component-BhLnfWq3.mjs} +2 -2
  3. package/fesm2022/{wuic-framework-lib-designer.component-D-TDt9Yp.mjs → wuic-framework-lib-designer.component-yPNbRYOE.mjs} +2 -2
  4. package/fesm2022/{wuic-framework-lib-first-run-wizard.component-DTd6aeZQ.mjs → wuic-framework-lib-first-run-wizard.component-Cf1BHCvJ.mjs} +2 -2
  5. package/fesm2022/{wuic-framework-lib-framework-docs.component-B2AdE4N0.mjs → wuic-framework-lib-framework-docs.component-DQEh15Vz.mjs} +68 -38
  6. package/fesm2022/{wuic-framework-lib-pivot-builder.component-B0Y_UHcw.mjs → wuic-framework-lib-pivot-builder.component-Df00Yaxo.mjs} +2 -2
  7. package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-RLZTS8VK.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-DExcTKa6.mjs} +2 -2
  8. package/fesm2022/{wuic-framework-lib-report-designer.component-DMHg11S1.mjs → wuic-framework-lib-report-designer.component-DTFAvjaD.mjs} +2 -2
  9. package/fesm2022/{wuic-framework-lib-report-scaffold-dialog.component-Dz-0CKFf.mjs → wuic-framework-lib-report-scaffold-dialog.component-CigGqFfV.mjs} +2 -2
  10. package/fesm2022/{wuic-framework-lib-report-viewer.component-DcjeKFl3.mjs → wuic-framework-lib-report-viewer.component-DKgW13uF.mjs} +2 -2
  11. package/fesm2022/{wuic-framework-lib-scene3d-designer.component-DkFSmqjd.mjs → wuic-framework-lib-scene3d-designer.component-Bcih9wqu.mjs} +3 -3
  12. package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-CDOGvkje.mjs → wuic-framework-lib-scene3d-light-baker-gpu-VH-R0E-j.mjs} +2 -2
  13. package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-CXzEo7SW.mjs → wuic-framework-lib-scene3d-viewer.component-CqitWdf-.mjs} +2 -2
  14. package/fesm2022/{wuic-framework-lib-scheduler-list.component-BWwp_Y2n.mjs → wuic-framework-lib-scheduler-list.component-Bhyln-cQ.mjs} +2 -2
  15. package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-C7XVqrk-.mjs → wuic-framework-lib-spreadsheet-list-sf.component-BirgUqOO.mjs} +2 -2
  16. package/fesm2022/{wuic-framework-lib-timeline-list.component-BPCARDTv.mjs → wuic-framework-lib-timeline-list.component-x3HAhB4G.mjs} +4 -4
  17. package/fesm2022/{wuic-framework-lib-workflow-designer.component-BOTf2H42.mjs → wuic-framework-lib-workflow-designer.component-DLBbgX0s.mjs} +2 -2
  18. package/fesm2022/{wuic-framework-lib-wuic-framework-lib-CNl2-wAT.mjs → wuic-framework-lib-wuic-framework-lib-HGQNVwv6.mjs} +22 -22
  19. package/fesm2022/wuic-framework-lib.mjs +1 -1
  20. package/package.json +1 -1
  21. package/types/wuic-framework-lib.d.ts +1 -1
@@ -16,12 +16,12 @@ import * as i1$3 from 'primeng/image';
16
16
  import { ImageModule } from 'primeng/image';
17
17
  import * as i8$1 from '@ngx-translate/core';
18
18
  import { TranslateService, TranslateModule } from '@ngx-translate/core';
19
- import { v as getThemeOptions, T as TranslationManagerService, U as UserInfoService } from './wuic-framework-lib-wuic-framework-lib-CNl2-wAT.mjs';
19
+ import { v as getThemeOptions, T as TranslationManagerService, U as UserInfoService } from './wuic-framework-lib-wuic-framework-lib-HGQNVwv6.mjs';
20
20
  import * as i1 from '@angular/router';
21
21
 
22
22
  const frameworkDocsContent = {
23
23
  "version": "1.0.0",
24
- "generatedAt": "2026-09-17T18:27:06.604Z",
24
+ "generatedAt": "2026-09-19T16:40:07.118Z",
25
25
  "groups": [
26
26
  {
27
27
  "id": "primi-passi",
@@ -1157,7 +1157,7 @@ const frameworkDocsContent = {
1157
1157
  {
1158
1158
  "id": "overview",
1159
1159
  "title": "Overview",
1160
- "html": "<h1>Panoramica Framework</h1><p>WUIC Framework e&#39; una piattaforma metadata-driven per creare interfacce business in modo rapido e coerente.</p><h2>Cosa include</h2><ul><li>DataSource e DataRepeater per orchestrare dati e rendering.</li><li>Archetypes visuali (list, map, scheduler, chart, carousel).</li><li>Designer runtime per dashboard e template dinamici.</li><li>Workflow designer/runner per processi operativi.</li><li>RAG Chatbot per interrogare la codebase in linguaggio naturale.</li><li>Multi-DBMS: SQL Server, MySQL, PostgreSQL e Oracle tramite provider drop-in (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Avvio ambiente dev</h2><p>Lo stack WUIC e&#39; composto da piu&#39; servizi. Avviali nell&#39;ordine indicato.</p><h3>Prerequisiti</h3><table><thead><tr><th>Cosa</th><th>Verifica</th><th>Note</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle per IIS in produzione</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>La 22 e&#39; la versione con cui il pacchetto e&#39; testato; con npm 10.9.x installa solo grazie al <code>package-lock.json</code> incluso</td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>La PowerShell preinstallata su Windows basta: <code>rename-project.ps1</code> e gli script <code>llm-workspace/</code> girano anche su 5.1. PowerShell 7 e&#39; consigliata, non richiesta</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>Se manca, <strong>il one-liner lo installa da se&#39;</strong> (SQL Server Express, o il motore scelto con <code>-Dbms</code>); su Windows Server no, vedi il riquadro sotto. 2019 basta per i pacchetti senza <code>.bak</code>; il tutorial <code>.bak</code> e&#39; un backup di SQL Server 2022 e richiede <strong>2022 o superiore</strong></td></tr><tr><td>Linux (installer <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 o 24.04 LTS</strong> (24.04 raccomandata): sono le uniche Ubuntu su cui Microsoft supporta SQL Server, la <strong>26.04 non e&#39; supportata</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Stessa raccomandazione con MySQL, PostgreSQL e Oracle, perche&#39; l&#39;installer e&#39; provato su quelle</td></tr><tr><td>GPU NVIDIA (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Opzionale: accelera il RAG Chatbot (senza GPU gira su CPU)</td></tr></tbody></table><p>&gt; <strong>Su Windows Server il database va installato prima.</strong> Il one-liner si procura i componenti</p><p>&gt; mancanti tramite <code>winget</code>, che su Windows Server non e&#39; presente: senza un&#39;istanza gia&#39;</p><p>&gt; raggiungibile l&#39;installazione si ferma con `No SQL Server instance reachable and winget is</p><p>&gt; missing`. Installa quindi il motore prima di lanciare la riga - per SQL Server serve la</p><p>&gt; <strong>2019 o superiore</strong> (la Express basta; <strong>2022 o superiore</strong> per il tutorial <code>.bak</code>), e una</p><p>&gt; 2017 non viene accettata. Se l&#39;istanza non e&#39; quella predefinita, passala con</p><p>&gt; <code>-SqlServer &#39;localhost\\NOMEISTANZA&#39;</code>. Vale per tutti i motori, non solo SQL Server.</p><h3>1. Scarica il template di progetto</h3><p>Su Windows fai tutto con una riga di PowerShell: scarica il pacchetto, verifica .NET SDK 10 e Node.js 22</p><p>(installando cio&#39; che manca), esegue <code>dotnet restore</code> e <code>npm install</code>, e installa</p><p>l&#39;estensione WUIC Assistant in VS Code.</p><p>Snippet 1:</p><p>Opzioni utili sulla stessa riga: <code>-Dbms mysql</code> (oppure <code>postgres</code>, <code>oracle</code>) per un motore</p><p>diverso da SQL Server — che l&#39;installer installa e configura da se&#39; — e <code>-WithTutorial</code> per</p><p>scaricare anche il database di esempio.</p><p>Quando ha finito, l&#39;ultima schermata e&#39; gia&#39; l&#39;elenco delle cose da fare:</p><p>Snippet 2:</p><p>La stringa di connessione e&#39; gia&#39; pronta: al primo avvio il wizard non chiede altro. Con</p><p><code>-Dbms mysql</code> o <code>-Dbms postgres</code> la riga cambia di conseguenza e porta dentro di se&#39; utente e</p><p>password, perche&#39; la password del superutente <strong>la genera l&#39;installer</strong>; la stessa password</p><p>viene salvata in <code>wuic-secrets.json</code>, accanto all&#39;installazione, ed e&#39; l&#39;unico posto in cui</p><p>resta scritta.</p><p>Il percorso manuale resta valido e funziona su qualunque sistema:</p><p>Parti dal pacchetto <code>WuicTest-src-*.zip</code> (link nella sezione <strong>Download</strong> del sito).</p><p>Estrailo in una cartella di lavoro, es. <code>C:\\dev\\WuicTest</code> (i file vanno direttamente nella root,</p><p>NON dentro una sottocartella <code>src/</code>). Il pacchetto contiene:</p><ul><li><code>WuicTest.csproj</code> → progetto host con <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → app Angular con <code>package.json</code> che referenzia <code>wuic-framework-lib</code> da npm registry</li><li><code>appsettings.json</code> preconfigurato in modalita&#39; <code>firstRun = &quot;true&quot;</code> con placeholder <code>__SET_*__</code></li><li><code>appsettings-samples/</code> → 6 template pronti (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>Non serve clonare il repository del framework</strong>: <code>WuicCore</code> arriva come pacchetto NuGet da</p><p>&gt; <code>nuget.org</code> e <code>wuic-framework-lib</code> come pacchetto npm da npm registry. Il sorgente del framework</p><p>&gt; serve solo se vuoi modificarlo — tipicamente non e&#39; il tuo caso.</p><p>&gt; <strong>npm 10.9.x e i pacchetti precedenti alla 1.7.1</strong>: quei pacchetti non contengono</p><p>&gt; <code>package-lock.json</code>, e senza lock npm 10.9.x (quello che installa Node 22 LTS) si ferma con</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> mentre risolve i peer. Scarica il lock</p><p>&gt; pubblicato accanto alla release — <code>https://wuic-framework.com/downloads/locks/&lt;nome-zip&gt;.package-lock.json</code></p><p>&gt; — mettilo in <code>wwwroot/</code> e rilancia <code>npm install</code>. Il one-liner <code>-Src</code> lo fa da solo.</p><h3>2. Database</h3><p>Serve un&#39;istanza del motore raggiungibile. <strong>Con il one-liner non devi fare niente</strong>: la</p><p>installa e la configura lui, e la stringa di connessione da incollare nel wizard e&#39; l&#39;ultima</p><p>cosa che stampa. Nel percorso manuale l&#39;istanza deve essere gia&#39; presente.</p><p>Connessioni in <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — DB metadata (menu, tabelle, colonne, board, utenti)</li><li><code>DataSQLConnection</code> — DB dati applicativi</li></ul><p>Se e&#39; la prima installazione (<code>AppSettings.firstRun = &quot;true&quot;</code> nel template),</p><p>il wizard di scaffolding al primo avvio crea lo schema metadata e popola il DB dati.</p><p>Vedi la pagina <em>Scaffolding iniziale</em> per il flusso guidato.</p><p>#### Cosa chiede il wizard di primo avvio</p><p>Al primo avvio l&#39;applicazione non mostra il login ma la pagina **Configurazione iniziale</p><p>progetto**. Si compila una volta sola: al termine <code>firstRun</code> passa a <code>false</code> e la pagina non</p><p>ricompare piu&#39;.</p><ul><li><strong>Modalita&#39; setup</strong> — due voci: <strong>DB esistente</strong>, che registra un database gia&#39; tuo, e</li></ul><p> <strong>Tutorial WideWorldImporters</strong>, che installa il database di esempio. La seconda compare</p><p> solo se il pacchetto porta con se&#39; il tutorial (<code>tutorialAvailable</code>): nei pacchetti senza</p><p> tutorial la tendina non c&#39;e&#39; affatto e la modalita&#39; e&#39; &quot;DB esistente&quot;.</p><ul><li><strong>DBMS</strong> — il motore. Cambiandolo, la stringa di connessione sotto viene riscritta nella</li></ul><p> forma del provider scelto.</p><ul><li><strong>DataSQLConnection</strong> — la stringa verso il DB dati, ed e&#39; il campo da cui dipende tutto il</li></ul><p> resto: finche&#39; non premi <strong>Test connessione e carica DB</strong> l&#39;elenco dei database resta</p><p> disabilitato. Il test verifica le credenziali, elenca i database del server e, su SQL</p><p> Server, corregge da se&#39; i parametri dei certificati</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) dicendotelo. <code>Integrated Security</code> vale solo</p><p> su SQL Server: sugli altri motori servono utente e password.</p><ul><li><strong>Database dati</strong> e <strong>Nome DB metadati</strong> — il primo si sceglie dall&#39;elenco caricato dal</li></ul><p> test (in modalita&#39; tutorial e&#39; un campo libero, default <code>WideWorldImporters</code>); il secondo e&#39;</p><p> il nome del database metadati da creare (default <code>metadataDB</code>, <code>MetadataCRM</code> in modalita&#39;</p><p> tutorial). Se quel database esiste gia&#39;, il wizard chiede conferma prima di ricrearlo.</p><ul><li><strong>Esegui scaffold automatico delle tabelle</strong> (solo &quot;DB esistente&quot;) — popola</li></ul><p> <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> da tutte le tabelle del database scelto. E&#39; la</p><p> voce che allunga di piu&#39; il provisioning su database con molte tabelle.</p><ul><li><strong>Utente admin iniziale</strong> — username (default <code>admin</code>), password (minimo 4 caratteri) e</li></ul><p> lingua. Non esiste una password predefinita: quella scritta qui e&#39; l&#39;unica con cui entrerai</p><p> al primo login. La lingua scelta viene applicata all&#39;utente admin e a questa pagina stessa.</p><ul><li><strong>RAG Chatbot</strong> — solo la chiave Anthropic, opzionale. Niente da installare: vedi il punto 5.</li></ul><p>Premuto <strong>Conferma e genera metadataDB</strong> il provisioning va avanti da solo, con una barra e il</p><p>numero di batch elaborati. Quanto dura, misurato sulle installazioni di prova: <strong>30 s - 2 min</strong></p><p>in modalita&#39; tutorial, <strong>1-4 min</strong> su un database esistente con lo scaffold automatico attivo.</p><p>Finito, la pagina lascia il posto al form di login.</p><h3>3. Backend (.NET)</h3><p>Dalla root della cartella estratta (es. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>Il backend espone le API su <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>In una seconda shell, dalla sottocartella <code>wwwroot/</code>:</p><p>Snippet 4:</p><p>Il frontend e&#39; disponibile su <code>http://localhost:4200</code>.</p><p>Le credenziali sono quelle dell&#39;utente admin scelto nel wizard di primo avvio: lo username e&#39;</p><p><code>admin</code> se non l&#39;hai cambiato, la password e&#39; quella che hai scritto tu (non esiste una</p><p>password predefinita).</p><h3>5. RAG Chatbot (opzionale)</h3><p>Il RAG Chatbot (<code>Amministrazione &gt; RAG Chat</code>) gira <strong>dentro il backend .NET</strong>:</p><p>il motore di retrieval (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime</p><p>in-process) viene caricato dal backend quando <code>AppSettings.rag-use-dotnet-engine</code></p><p>e&#39; <code>&quot;true&quot;</code> (default nei pacchetti di release). **Niente Python, niente venv,</p><p>nessun server separato da avviare.**</p><p>Alla <strong>prima</strong> apertura del chatbot (o a fine first-run) il backend scarica da</p><p>solo modelli ONNX (<code>bge-m3</code> + reranker), tokenizer e indice (~4,5 GB) da</p><p><code>rag-engine-models-url</code> (default <code>https://wuic-framework.com/rag-models</code>) in</p><p><code>rag-engine/artifacts/</code>. Serve una connessione internet una sola volta; il</p><p>download gira in background (1-5 min) e a inizio/fine download arriva una</p><p>notifica in-app. Nel frattempo il componente mostra lo stato <strong>RAG offline</strong> e</p><p>il resto dell&#39;applicazione funziona normalmente.</p><p>Chiavi <code>AppSettings</code> rilevanti (tutte con default gia&#39; pronti):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — attiva l&#39;engine .NET (<code>false</code> = fallback Python, vedi sotto)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: con GPU NVIDIA (CUDA 12.x + cuDNN 9) ~1 s/query, su CPU ~15-25 s/query</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = sorgenti framework solo come firma; <code>internal</code> = indice integrale</li><li><code>rag-engine-models-url</code> — sorgente del download al primo avvio</li><li><code>rag-engine-cuda-path</code> = (vuoto) — cartella delle DLL CUDA/cuDNN se non installate a livello di sistema</li></ul><p>Per la chat con LLM valorizza <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> (Claude</p><p>via Anthropic, oppure un modello locale via Ollama con <code>rag-llm-base-url</code>).</p><p>Senza provider funziona solo la modalita&#39; retrieval (ricerca snippet).</p><p>Dettaglio delle chiavi nella pagina <em>AppSettings</em>.</p><p>&gt; Lo stack storico Python (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> su</p><p>&gt; <code>127.0.0.1:8765</code>) resta utilizzabile solo come fallback con</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> e non e&#39; incluso nei pacchetti di release.</p><h2>Riepilogo servizi</h2><table><thead><tr><th>Servizio</th><th>Porta</th><th>Obbligatorio</th><th>Comando rapido</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>si</td><td>(servizio Windows)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>si</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>si</td><td><code>npm run serve:dev</code></td></tr><tr><td>Engine RAG .NET (ONNX)</td><td>in-process</td><td>no</td><td>caricato dal backend alla prima chat</td></tr></tbody></table><h2>Quick Start — primo componente</h2><p>1. Configura endpoint AsmxProxy.</p><p>2. Definisci route metadata e colonne.</p><p>3. Monta <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>Le prime liste mostrano 20 record: e&#39; la modalita&#39; Trial</h2><p>Un&#39;installazione appena fatta gira <strong>senza licenza</strong>, cioe&#39; in modalita&#39; Trial, e in quella</p><p>modalita&#39; <strong>ogni query e&#39; limitata a 20 record</strong>. Non e&#39; un errore ne&#39; un limite del database:</p><p>una lista che dichiara &quot;20 di 20&quot; su una tabella con migliaia di righe sta semplicemente</p><p>girando senza licenza.</p><p>Come confermarlo in un secondo:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> conferma la Trial, e <code>licenseReason</code> dice perche&#39; (<code>invalid_signature</code>,</p><p><code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>Come uscirne: si incolla la licenza da <strong>Amministrazione &gt; Editor AppSettings</strong>, scheda Licensing,</p><p>ed e&#39; valida subito — non serve riavviare il backend. I dettagli, compresi i limiti che la</p><p>Trial impone oltre al numero di record, stanno nella pagina <a href=\"licensing.md\">Licensing</a>.</p>",
1160
+ "html": "<h1>Panoramica Framework</h1><p>WUIC Framework e&#39; una piattaforma metadata-driven per creare interfacce business in modo rapido e coerente.</p><h2>Cosa include</h2><ul><li>DataSource e DataRepeater per orchestrare dati e rendering.</li><li>Archetypes visuali (list, map, scheduler, chart, carousel).</li><li>Designer runtime per dashboard e template dinamici.</li><li>Workflow designer/runner per processi operativi.</li><li>RAG Chatbot per interrogare la codebase in linguaggio naturale.</li><li>Multi-DBMS: SQL Server, MySQL, PostgreSQL e Oracle tramite provider drop-in (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Avvio ambiente dev</h2><p>Lo stack WUIC e&#39; composto da piu&#39; servizi. Avviali nell&#39;ordine indicato.</p><h3>Prerequisiti</h3><table><thead><tr><th>Cosa</th><th>Verifica</th><th>Note</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle per IIS in produzione</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>La 22 e&#39; la versione con cui il pacchetto e&#39; testato; con npm 10.9.x installa solo grazie al <code>package-lock.json</code> incluso</td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>La PowerShell preinstallata su Windows basta: <code>rename-project.ps1</code> e gli script <code>llm-workspace/</code> girano anche su 5.1. PowerShell 7 e&#39; consigliata, non richiesta</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>Se manca, <strong>il one-liner lo installa da se&#39;</strong> (SQL Server Express, o il motore scelto con <code>-Dbms</code>); su Windows Server no, vedi il riquadro sotto. 2019 basta per i pacchetti senza <code>.bak</code>; il tutorial <code>.bak</code> e&#39; un backup di SQL Server 2022 e richiede <strong>2022 o superiore</strong></td></tr><tr><td>Linux (installer <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 o 24.04 LTS</strong> (24.04 raccomandata): sono le uniche Ubuntu su cui Microsoft supporta SQL Server, la <strong>26.04 non e&#39; supportata</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Stessa raccomandazione con MySQL, PostgreSQL e Oracle, perche&#39; l&#39;installer e&#39; provato su quelle</td></tr><tr><td>GPU NVIDIA (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Opzionale: accelera il RAG Chatbot (senza GPU gira su CPU)</td></tr></tbody></table><p>&gt; <strong>Su Windows Server il database va installato prima.</strong> Il one-liner si procura i componenti</p><p>&gt; mancanti tramite <code>winget</code>, che su Windows Server non e&#39; presente: senza un&#39;istanza gia&#39;</p><p>&gt; raggiungibile l&#39;installazione si ferma con `No SQL Server instance reachable and winget is</p><p>&gt; missing`. Installa quindi il motore prima di lanciare la riga - per SQL Server serve la</p><p>&gt; <strong>2019 o superiore</strong> (la Express basta; <strong>2022 o superiore</strong> per il tutorial <code>.bak</code>), e una</p><p>&gt; 2017 non viene accettata. Se l&#39;istanza non e&#39; quella predefinita, passala con</p><p>&gt; <code>-SqlServer &#39;localhost\\NOMEISTANZA&#39;</code>. Vale per tutti i motori, non solo SQL Server.</p><h3>1. Scarica il template di progetto</h3><p>Su Windows fai tutto con una riga di PowerShell: scarica il pacchetto, verifica .NET SDK 10 e Node.js 22</p><p>(installando cio&#39; che manca), esegue <code>dotnet restore</code> e <code>npm install</code>, e installa</p><p>l&#39;estensione WUIC Assistant in VS Code.</p><p>Snippet 1:</p><p>Opzioni utili sulla stessa riga: <code>-Dbms mysql</code> (oppure <code>postgres</code>, <code>oracle</code>) per un motore</p><p>diverso da SQL Server — che l&#39;installer installa e configura da se&#39; — e <code>-WithTutorial</code> per</p><p>scaricare anche il database di esempio.</p><p>Quando ha finito, l&#39;ultima schermata e&#39; gia&#39; l&#39;elenco delle cose da fare: si apre il file di</p><p>workspace <code>WuicTest.code-workspace</code> in VS Code e si preme <strong>F5</strong>, che avvia backend e frontend</p><p>insieme con il launcher <code>Fullstack: WuicTest + Chrome</code> (i launcher stanno in</p><p><code>.vscode/launch.json</code>, insieme a quello per il solo backend). Chi preferisce il terminale trova</p><p>i comandi equivalenti piu&#39; sotto.</p><p>Snippet 2:</p><p>La stringa di connessione e&#39; gia&#39; pronta: al primo avvio il wizard non chiede altro. Con</p><p><code>-Dbms mysql</code> o <code>-Dbms postgres</code> la riga cambia di conseguenza e porta dentro di se&#39; utente e</p><p>password, perche&#39; la password del superutente <strong>la genera l&#39;installer</strong>; la stessa password</p><p>viene salvata in <code>wuic-secrets.json</code>, accanto all&#39;installazione, ed e&#39; l&#39;unico posto in cui</p><p>resta scritta.</p><p>Il percorso manuale resta valido e funziona su qualunque sistema:</p><p>Parti dal pacchetto <code>WuicTest-src-*.zip</code> (link nella sezione <strong>Download</strong> del sito).</p><p>Estrailo in una cartella di lavoro, es. <code>C:\\dev\\WuicTest</code> (i file vanno direttamente nella root,</p><p>NON dentro una sottocartella <code>src/</code>). Il pacchetto contiene:</p><ul><li><code>WuicTest.csproj</code> → progetto host con <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → app Angular con <code>package.json</code> che referenzia <code>wuic-framework-lib</code> da npm registry</li><li><code>appsettings.json</code> preconfigurato in modalita&#39; <code>firstRun = &quot;true&quot;</code> con placeholder <code>__SET_*__</code></li><li><code>appsettings-samples/</code> → 6 template pronti (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>Non serve clonare il repository del framework</strong>: <code>WuicCore</code> arriva come pacchetto NuGet da</p><p>&gt; <code>nuget.org</code> e <code>wuic-framework-lib</code> come pacchetto npm da npm registry. Il sorgente del framework</p><p>&gt; serve solo se vuoi modificarlo — tipicamente non e&#39; il tuo caso.</p><p>&gt; <strong>npm 10.9.x e i pacchetti precedenti alla 1.7.1</strong>: quei pacchetti non contengono</p><p>&gt; <code>package-lock.json</code>, e senza lock npm 10.9.x (quello che installa Node 22 LTS) si ferma con</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> mentre risolve i peer. Scarica il lock</p><p>&gt; pubblicato accanto alla release — <code>https://wuic-framework.com/downloads/locks/&lt;nome-zip&gt;.package-lock.json</code></p><p>&gt; — mettilo in <code>wwwroot/</code> e rilancia <code>npm install</code>. Il one-liner <code>-Src</code> lo fa da solo.</p><h3>2. Database</h3><p>Serve un&#39;istanza del motore raggiungibile. <strong>Con il one-liner non devi fare niente</strong>: la</p><p>installa e la configura lui, e la stringa di connessione da incollare nel wizard e&#39; l&#39;ultima</p><p>cosa che stampa. Nel percorso manuale l&#39;istanza deve essere gia&#39; presente.</p><p>Connessioni in <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — DB metadata (menu, tabelle, colonne, board, utenti)</li><li><code>DataSQLConnection</code> — DB dati applicativi</li></ul><p>Se e&#39; la prima installazione (<code>AppSettings.firstRun = &quot;true&quot;</code> nel template),</p><p>il wizard di scaffolding al primo avvio crea lo schema metadata e popola il DB dati.</p><p>Vedi la pagina <em>Scaffolding iniziale</em> per il flusso guidato.</p><p>#### Cosa chiede il wizard di primo avvio</p><p>Al primo avvio l&#39;applicazione non mostra il login ma la pagina **Configurazione iniziale</p><p>progetto**. Si compila una volta sola: al termine <code>firstRun</code> passa a <code>false</code> e la pagina non</p><p>ricompare piu&#39;.</p><ul><li><strong>Modalita&#39; setup</strong> — due voci: <strong>DB esistente</strong>, che registra un database gia&#39; tuo, e</li></ul><p> <strong>Tutorial WideWorldImporters</strong>, che installa il database di esempio. La seconda compare</p><p> solo se il pacchetto porta con se&#39; il tutorial (<code>tutorialAvailable</code>): nei pacchetti senza</p><p> tutorial la tendina non c&#39;e&#39; affatto e la modalita&#39; e&#39; &quot;DB esistente&quot;.</p><ul><li><strong>DBMS</strong> — il motore. Cambiandolo, la stringa di connessione sotto viene riscritta nella</li></ul><p> forma del provider scelto.</p><ul><li><strong>DataSQLConnection</strong> — la stringa verso il DB dati, ed e&#39; il campo da cui dipende tutto il</li></ul><p> resto: finche&#39; non premi <strong>Test connessione e carica DB</strong> l&#39;elenco dei database resta</p><p> disabilitato. Il test verifica le credenziali, elenca i database del server e, su SQL</p><p> Server, corregge da se&#39; i parametri dei certificati</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) dicendotelo. <code>Integrated Security</code> vale solo</p><p> su SQL Server: sugli altri motori servono utente e password.</p><ul><li><strong>Database dati</strong> e <strong>Nome DB metadati</strong> — il primo si sceglie dall&#39;elenco caricato dal</li></ul><p> test (in modalita&#39; tutorial e&#39; un campo libero, default <code>WideWorldImporters</code>); il secondo e&#39;</p><p> il nome del database metadati da creare (default <code>metadataDB</code>, <code>MetadataCRM</code> in modalita&#39;</p><p> tutorial). Se quel database esiste gia&#39;, il wizard chiede conferma prima di ricrearlo.</p><ul><li><strong>Esegui scaffold automatico delle tabelle</strong> (solo &quot;DB esistente&quot;) — popola</li></ul><p> <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> da tutte le tabelle del database scelto. E&#39; la</p><p> voce che allunga di piu&#39; il provisioning su database con molte tabelle.</p><ul><li><strong>Utente admin iniziale</strong> — username (default <code>admin</code>), password (minimo 4 caratteri) e</li></ul><p> lingua. Non esiste una password predefinita: quella scritta qui e&#39; l&#39;unica con cui entrerai</p><p> al primo login. La lingua scelta viene applicata all&#39;utente admin e a questa pagina stessa.</p><ul><li><strong>RAG Chatbot</strong> — solo la chiave Anthropic, opzionale. Niente da installare: vedi il punto 5.</li></ul><p>Premuto <strong>Conferma e genera metadataDB</strong> il provisioning va avanti da solo, con una barra e il</p><p>numero di batch elaborati. Quanto dura, misurato sulle installazioni di prova: <strong>30 s - 2 min</strong></p><p>in modalita&#39; tutorial, <strong>1-4 min</strong> su un database esistente con lo scaffold automatico attivo.</p><p>Finito, la pagina lascia il posto al form di login.</p><h3>3. Backend (.NET)</h3><p>Dalla root della cartella estratta (es. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>Il backend espone le API su <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>In una seconda shell, dalla sottocartella <code>wwwroot/</code>:</p><p>Snippet 4:</p><p>Il frontend e&#39; disponibile su <code>http://localhost:4200</code>.</p><p>Le credenziali sono quelle dell&#39;utente admin scelto nel wizard di primo avvio: lo username e&#39;</p><p><code>admin</code> se non l&#39;hai cambiato, la password e&#39; quella che hai scritto tu (non esiste una</p><p>password predefinita).</p><h3>5. RAG Chatbot (opzionale)</h3><p>Il RAG Chatbot (<code>Amministrazione &gt; RAG Chat</code>) gira <strong>dentro il backend .NET</strong>:</p><p>il motore di retrieval (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime</p><p>in-process) viene caricato dal backend quando <code>AppSettings.rag-use-dotnet-engine</code></p><p>e&#39; <code>&quot;true&quot;</code> (default nei pacchetti di release). **Niente Python, niente venv,</p><p>nessun server separato da avviare.**</p><p>Alla <strong>prima</strong> apertura del chatbot (o a fine first-run) il backend scarica da</p><p>solo modelli ONNX (<code>bge-m3</code> + reranker), tokenizer e indice (~4,5 GB) da</p><p><code>rag-engine-models-url</code> (default <code>https://wuic-framework.com/rag-models</code>) in</p><p><code>rag-engine/artifacts/</code>. Serve una connessione internet una sola volta; il</p><p>download gira in background (1-5 min) e a inizio/fine download arriva una</p><p>notifica in-app. Nel frattempo il componente mostra lo stato <strong>RAG offline</strong> e</p><p>il resto dell&#39;applicazione funziona normalmente.</p><p>Chiavi <code>AppSettings</code> rilevanti (tutte con default gia&#39; pronti):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — attiva l&#39;engine .NET (<code>false</code> = fallback Python, vedi sotto)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: con GPU NVIDIA (CUDA 12.x + cuDNN 9) ~1 s/query, su CPU ~15-25 s/query</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = sorgenti framework solo come firma; <code>internal</code> = indice integrale</li><li><code>rag-engine-models-url</code> — sorgente del download al primo avvio</li><li><code>rag-engine-cuda-path</code> = (vuoto) — cartella delle DLL CUDA/cuDNN se non installate a livello di sistema</li></ul><p>Per la chat con LLM valorizza <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> (Claude</p><p>via Anthropic, oppure un modello locale via Ollama con <code>rag-llm-base-url</code>).</p><p>Senza provider funziona solo la modalita&#39; retrieval (ricerca snippet).</p><p>Dettaglio delle chiavi nella pagina <em>AppSettings</em>.</p><p>&gt; Lo stack storico Python (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> su</p><p>&gt; <code>127.0.0.1:8765</code>) resta utilizzabile solo come fallback con</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> e non e&#39; incluso nei pacchetti di release.</p><h2>Riepilogo servizi</h2><table><thead><tr><th>Servizio</th><th>Porta</th><th>Obbligatorio</th><th>Comando rapido</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>si</td><td>(servizio Windows)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>si</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>si</td><td><code>npm run serve:dev</code></td></tr><tr><td>Engine RAG .NET (ONNX)</td><td>in-process</td><td>no</td><td>caricato dal backend alla prima chat</td></tr></tbody></table><h2>Quick Start — primo componente</h2><p>1. Configura endpoint AsmxProxy.</p><p>2. Definisci route metadata e colonne.</p><p>3. Monta <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>Le prime liste mostrano 20 record: e&#39; la modalita&#39; Trial</h2><p>Un&#39;installazione appena fatta gira <strong>senza licenza</strong>, cioe&#39; in modalita&#39; Trial, e in quella</p><p>modalita&#39; <strong>ogni query e&#39; limitata a 20 record</strong>. Non e&#39; un errore ne&#39; un limite del database:</p><p>una lista che dichiara &quot;20 di 20&quot; su una tabella con migliaia di righe sta semplicemente</p><p>girando senza licenza.</p><p>Come confermarlo in un secondo:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> conferma la Trial, e <code>licenseReason</code> dice perche&#39; (<code>invalid_signature</code>,</p><p><code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>Come uscirne: si incolla la licenza da <strong>Amministrazione &gt; Editor AppSettings</strong>, scheda Licensing,</p><p>ed e&#39; valida subito — non serve riavviare il backend. I dettagli, compresi i limiti che la</p><p>Trial impone oltre al numero di record, stanno nella pagina <a href=\"licensing.md\">Licensing</a>.</p>",
1161
1161
  "codeSamples": [
1162
1162
  {
1163
1163
  "id": "code_1",
@@ -1520,7 +1520,7 @@ const frameworkDocsContent = {
1520
1520
  {
1521
1521
  "id": "overview",
1522
1522
  "title": "Overview",
1523
- "html": "<h1>Pattern: Framework component + Custom data</h1><p>I componenti UI ad alto livello del framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) accettano un <strong>datasource costruito a mano</strong> (<code>hardcodedDatasource</code>) che non passa dal data layer del framework. I dati arrivano dal tuo backend custom (Controller .NET tuo, REST esterna, file statici, websocket, ...).</p><h2>Quando usarlo</h2><ul><li>Vuoi l&#39;UX completa di una list-grid/chart-list (filtri, sort, paging client, export, edit dialog) ma i dati sono prodotti da:</li></ul><p> - Un endpoint REST esterno (3rd-party API, microservizio).</p><p> - Un Controller .NET tuo non integrato col data layer del framework.</p><p> - File statici, calcoli aggregati, dati live (websocket, polling).</p><ul><li>Hai un dominio legacy che non vuoi modellare nel framework.</li><li>Stai prototipando senza ancora aver definito la struttura dati.</li></ul><h2>Architettura</h2><ul><li><strong>Sviluppatore</strong>: scrive un piccolo componente Angular che recupera i dati dal proprio backend e li impacchetta in un datasource locale.</li><li><strong>Framework</strong>: la list-grid si comporta esattamente come se i dati venissero dal data layer standard (filtri, sort, paging, export funzionano).</li><li><strong>Backend</strong>: liberta&#39; totale. Endpoint REST classici, niente convenzioni di metadata.</li></ul><h2>Cosa fai tu (frontend)</h2><p>Crei un componente Angular standalone che:</p><p>1. Chiama il tuo endpoint custom con <code>HttpClient</code>.</p><p>2. Definisce le colonne (nome, label, tipo) per il datasource locale.</p><p>3. Pubblica righe + colonne sul datasource e lo passa alla list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Cosa fai tu (backend, opzionale)</h2><p>Se i dati vengono dal tuo backend interno, basta un Controller REST classico. Niente convenzioni del framework, niente metadata da scrivere.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contro</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> con filtri/sort/paging/export gratis (client-side)</td><td>Mantieni tu la coerenza tra le righe e la definizione delle colonne</td></tr><tr><td>Backend completamente libero</td><td>CRUD non funziona out-of-the-box: edit/insert vanno wirati a mano sul tuo backend</td></tr><tr><td>Buono per integrazioni 3rd-party</td><td>Paging/sort/filter server-side richiede wiring custom (vedi sotto)</td></tr><tr><td>Nessun lavoro di scaffolding</td><td>Type safety solo via cast</td></tr></tbody></table><h2>Filtri / sort / paging: client-side vs server-side</h2><p>La frase &quot;filtri/sort/paging/export gratis&quot; della tabella Trade-off vale <strong>solo in modalita&#39; client-side</strong>, ed e&#39; subordinata a un flag metadata che va impostato esplicitamente nel datasource hardcoded.</p><h3>Il flag chiave: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (proprieta&#39; di <code>MetadatiTabella</code>) controlla <strong>dove</strong> vengono eseguite paging/sort/filter:</p><table><thead><tr><th>Valore</th><th>Significato</th><th>Quando usarlo</th></tr></thead><tbody><tr><td><code>true</code> (default)</td><td>La list-grid invia gli eventi paging/sort/filter al backend tramite l&#39;endpoint CRUD standard del framework. Il backend ritorna solo la pagina richiesta e applica sort/filter SQL-side.</td><td>Pattern 1 e 2 (con route metadata reale e backend WUIC dietro).</td></tr><tr><td><code>false</code></td><td>La list-grid esegue paging/sort/filter in-memory sull&#39;array gia&#39; caricato. Niente roundtrip server.</td><td><strong>Sempre nei datasource hardcoded del Pattern 3</strong> (e ogni volta che pubblichi tutte le righe in un colpo solo via <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Trappola tipica del Pattern 3</strong>: se dimentichi di forzare <code>md_server_side_operations: false</code>, la list-grid mostra le 50/100 righe ricevute, ma <strong>cliccare la pagina 2, ordinare una colonna o digitare nel filtro non fa niente</strong> — la grid invia l&#39;evento al &quot;backend del framework&quot; che non esiste, e l&#39;UX appare bloccata pur senza errori in console.</p><p>&gt; <strong>Nota framework</strong>: <code>DataSourceComponent.fetchData()</code> rileva automaticamente il caso &quot;hardcoded datasource&quot; (nessun <code>[hardcodedRoute]</code> impostato) e <strong>salta la chiamata al backend</strong> ad ogni cambio paging/sort/filter, ripubblicando il payload gia&#39; presente in memoria su <code>fetchInfo$</code>. Significa che, una volta popolato <code>fetchInfo$.next(...)</code> la prima volta nel tuo <code>ngAfterViewInit</code>, paging/sort/filter funzionano client-side senza nessun roundtrip server, anche se il backend WUIC non ha registrato la route. Vedi <code>data-source.component.ts</code> (short-circuit dentro <code>fetchData()</code>).</p><h3>Modalita&#39; client-side (default consigliato per Pattern 3)</h3><p>Snippet 4:</p><ul><li>Carichi <strong>tutte</strong> le righe con un singolo <code>fetchInfo$.next</code>.</li><li>La list-grid applica filtri/sort/paging/export sull&#39;array gia&#39; presente.</li><li>Zero codice aggiuntivo.</li><li>Indicato per dataset piccoli/medi (ordine di qualche migliaio di righe).</li></ul><h3>Modalita&#39; server-side (wiring manuale)</h3><p>Per dataset grandi (decine/centinaia di migliaia di righe) <strong>non vuoi</strong> caricare tutto in memoria. Lasci <code>md_server_side_operations: true</code> (default), ti sottoscrivi agli <strong>@Output di `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) e re-chiami il tuo endpoint REST a ogni cambio di stato. La list-grid aggiorna <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>prima</strong> di emettere l&#39;evento, quindi nel handler basta leggere lo stato corrente.</p><p>Snippet 5:</p><p>Endpoint server complementare (esempio C#, vedi <code>SamplesController.GetInventory</code>):</p><p>Snippet 6:</p><p><strong>Punti chiave:</strong></p><ul><li>Gli <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> di <code>&lt;wuic-list-grid&gt;</code> espongono gli eventi UI <strong>dopo</strong> che il list-grid handler ha gia&#39; aggiornato lo stato del datasource. Niente da reimplementare: leggi <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>Il backend deve ritornare <code>{ rows, total }</code> dove <code>total</code> e&#39; il count <strong>POST-filter / PRE-page</strong>. Senza questo il pager UI non sa quante pagine esistono e non funziona correttamente.</li><li>L&#39;operatore di filtro arriva nel campo <code>operatore</code> della filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, ecc., vedi tabella matchMode in <a href=\"./list-grid.md\">List Grid</a>). Mappalo coerentemente lato server.</li></ul><h2>Variante: consumare l&#39;endpoint OData del framework</h2><p>Se l&#39;entita&#39; che vuoi visualizzare e&#39; gia&#39; esposta dal framework come entity set OData (<code>/odata/&lt;EntitySet&gt;</code>), non serve scrivere NESSUN controller: basta tradurre lo stato UI della list-grid in query string OData v4 standard.</p><p>&gt; <strong>Alternativa 100% framework-driven (Pattern 1 con backend OData)</strong>: se accetti di registrare una route metadata standard per l&#39;entita&#39;, puoi configurare <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> e il datasource fa <strong>tutto da solo</strong> (filter/sort/paging/export) tramite il provider OData interno. Niente codice Angular custom. Vedi <a href=\"./odata.md\">OData</a> per il setup completo. Pattern 3 (questa pagina) si applica invece quando vuoi controllo esplicito lato frontend o non hai metadata registrati per l&#39;entita&#39;.</p><p>Il framework espone <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> che fa tutto il mapping operator WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) con quoting automatico per string/numeric, supporto nested filter groups (AND/OR ricorsivi) e isnull/isnotnull. Il return e&#39; una URL relativa tipo <code>/odata/Cities?$filter=&lt;espressione encoded&gt;</code>. Ti basta prefissare la base URL e aggiungere <code>$top / $skip / $orderby</code>.</p><p>Snippet 7:</p><p>&gt; <strong>Nota su total/count</strong>: se l&#39;endpoint OData che usi e&#39; configurato per ritornare il wrapper OData standard <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (via <code>$count=true</code>), puoi leggere il total direttamente dalla response senza la seconda query. L&#39;endpoint del framework WUIC attualmente ritorna il plain array e richiede la query parallela.</p><h2>Esempi vivi nel WuicTest</h2><p>I tre esempi coprono le <strong>tre strategie principali</strong> del Pattern 3:</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; carica TUTTI i 50 post da un endpoint esterno (<code>jsonplaceholder.typicode.com/posts</code>) in un colpo solo, paging/sort/filter applicati in-memory dalla list-grid (<code>md_server_side_operations: false</code>). Cartella sorgente: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Apri demo</a>.</li><li><strong>Custom .NET grid (server-side, REST custom)</strong> &rarr; chiama il Controller <code>SamplesController.GetInventory</code> con offset/limit/sort/filter come query params ad hoc, ricarica solo la pagina corrente ad ogni cambio (<code>md_server_side_operations: true</code> + wiring esplicito su <code>(onPaging)/(onSorting)/(onFiltering)</code>). Cartella sorgente: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Apri demo</a>.</li><li><strong>OData Cities grid (server-side, standard OData v4)</strong> &rarr; consuma l&#39;endpoint OData generico del framework (<code>GET /odata/Cities</code>) con query string standard <code>$top / $skip / $filter / $orderby</code>, nessun controller custom da scrivere. Traduce gli eventi UI della list-grid in query OData (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, ecc.). Cartella sorgente: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Apri demo</a>.</li></ul><h3>Quando scegliere quale variante</h3><table><thead><tr><th>Esempio</th><th>Strategia</th><th>Backend</th><th>Quando usarla</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Endpoint REST classico che ritorna un array</td><td>Dataset piccolo-medio (&lt; qualche migliaio di righe), semplicita&#39; massima, API 3rd-party senza controllo server-side</td></tr><tr><td><strong>3b</strong></td><td>Server-side REST custom</td><td>Controller REST tuo con query params di paging/sort/filter</td><td>Dataset grande, vuoi controllo totale sulla query; l&#39;end-dev ha gia&#39; un endpoint esistente con offset/limit/ecc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Endpoint OData del framework (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Dataset grande esposto automaticamente dal framework come OData set; zero codice backend; sintassi standard compatibile con altri client</td></tr></tbody></table><h2>Vedi anche</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full autogeneration</a>: se l&#39;UX standard basta e i dati esistono nel modello scaffoldato.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework data + Custom component</a>: inverso (UI custom, dati framework).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full custom</a>: se non ti serve nemmeno la list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework component + Framework data (manual mount)</a>: variante &quot;framework&quot; di questo pattern: stessa compozione manuale dei widget, ma data layer metadata-driven invece che backend custom.</li></ul>",
1523
+ "html": "<h1>Pattern: Framework component + Custom data</h1><p>I componenti UI ad alto livello del framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) accettano un <strong>datasource costruito a mano</strong> (<code>hardcodedDatasource</code>) che non passa dal data layer del framework. I dati arrivano dal tuo backend custom (Controller .NET tuo, REST esterna, file statici, websocket, ...).</p><h2>Quando usarlo</h2><ul><li>Vuoi l&#39;UX completa di una list-grid/chart-list (filtri, sort, paging client, export, edit dialog) ma i dati sono prodotti da:</li></ul><p> - Un endpoint REST esterno (3rd-party API, microservizio).</p><p> - Un Controller .NET tuo non integrato col data layer del framework.</p><p> - File statici, calcoli aggregati, dati live (websocket, polling).</p><ul><li>Hai un dominio legacy che non vuoi modellare nel framework.</li><li>Stai prototipando senza ancora aver definito la struttura dati.</li></ul><h2>Architettura</h2><ul><li><strong>Sviluppatore</strong>: scrive un piccolo componente Angular che recupera i dati dal proprio backend e li impacchetta in un datasource locale.</li><li><strong>Framework</strong>: la list-grid si comporta esattamente come se i dati venissero dal data layer standard (filtri, sort, paging, export funzionano).</li><li><strong>Backend</strong>: liberta&#39; totale. Endpoint REST classici, niente convenzioni di metadata.</li></ul><h2>Cosa fai tu (frontend)</h2><p>Crei un componente Angular standalone che:</p><p>1. Chiama il tuo endpoint custom con <code>HttpClient</code>.</p><p>2. Definisce le colonne (nome, label, tipo) per il datasource locale.</p><p>3. Pubblica righe + colonne sul datasource e lo passa alla list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Cosa fai tu (backend, opzionale)</h2><p>Se i dati vengono dal tuo backend interno, basta un Controller REST classico. Niente convenzioni del framework, niente metadata da scrivere.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h3>Se apri tu la connessione: su Oracle porta la sessione sullo schema</h3><p>Quando il controller apre una connessione propria con la connection string <code>DataSQLConnection</code></p><p>(ADO.NET, Dapper, EF Core con <code>UseOracle(...)</code>), su <strong>Oracle</strong> c&#39;e&#39; un passaggio in piu&#39; che gli</p><p>altri motori non hanno: nella stringa non c&#39;e&#39; nessun <code>Database</code> / <code>Initial Catalog</code>, perche&#39; su</p><p>Oracle lo schema non fa parte della connessione. <code>DataSQLConnection</code> autentica soltanto; allo</p><p>schema dei dati il framework porta la sessione da solo, eseguendo</p><p><code>ALTER SESSION SET CURRENT_SCHEMA = &lt;schema&gt;</code> all&#39;apertura.</p><p>Una connessione aperta a mano quel passaggio non ce l&#39;ha: resta sullo schema dell&#39;utente con cui</p><p>si e&#39; autenticata, e la prima query sulle tue tabelle risponde `ORA-00942: table or view does not</p><p>exist` anche se la tabella esiste e i permessi ci sono. Due modi per farlo bene:</p><p>Snippet 4:</p><p>Sugli altri tre motori non serve: il database sta nella connection string e la sessione ci nasce</p><p>dentro.</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contro</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> con filtri/sort/paging/export gratis (client-side)</td><td>Mantieni tu la coerenza tra le righe e la definizione delle colonne</td></tr><tr><td>Backend completamente libero</td><td>CRUD non funziona out-of-the-box: edit/insert vanno wirati a mano sul tuo backend</td></tr><tr><td>Buono per integrazioni 3rd-party</td><td>Paging/sort/filter server-side richiede wiring custom (vedi sotto)</td></tr><tr><td>Nessun lavoro di scaffolding</td><td>Type safety solo via cast</td></tr></tbody></table><h2>Filtri / sort / paging: client-side vs server-side</h2><p>La frase &quot;filtri/sort/paging/export gratis&quot; della tabella Trade-off vale <strong>solo in modalita&#39; client-side</strong>, ed e&#39; subordinata a un flag metadata che va impostato esplicitamente nel datasource hardcoded.</p><h3>Il flag chiave: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (proprieta&#39; di <code>MetadatiTabella</code>) controlla <strong>dove</strong> vengono eseguite paging/sort/filter:</p><table><thead><tr><th>Valore</th><th>Significato</th><th>Quando usarlo</th></tr></thead><tbody><tr><td><code>true</code> (default)</td><td>La list-grid invia gli eventi paging/sort/filter al backend tramite l&#39;endpoint CRUD standard del framework. Il backend ritorna solo la pagina richiesta e applica sort/filter SQL-side.</td><td>Pattern 1 e 2 (con route metadata reale e backend WUIC dietro).</td></tr><tr><td><code>false</code></td><td>La list-grid esegue paging/sort/filter in-memory sull&#39;array gia&#39; caricato. Niente roundtrip server.</td><td><strong>Sempre nei datasource hardcoded del Pattern 3</strong> (e ogni volta che pubblichi tutte le righe in un colpo solo via <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Trappola tipica del Pattern 3</strong>: se dimentichi di forzare <code>md_server_side_operations: false</code>, la list-grid mostra le 50/100 righe ricevute, ma <strong>cliccare la pagina 2, ordinare una colonna o digitare nel filtro non fa niente</strong> — la grid invia l&#39;evento al &quot;backend del framework&quot; che non esiste, e l&#39;UX appare bloccata pur senza errori in console.</p><p>&gt; <strong>Nota framework</strong>: <code>DataSourceComponent.fetchData()</code> rileva automaticamente il caso &quot;hardcoded datasource&quot; (nessun <code>[hardcodedRoute]</code> impostato) e <strong>salta la chiamata al backend</strong> ad ogni cambio paging/sort/filter, ripubblicando il payload gia&#39; presente in memoria su <code>fetchInfo$</code>. Significa che, una volta popolato <code>fetchInfo$.next(...)</code> la prima volta nel tuo <code>ngAfterViewInit</code>, paging/sort/filter funzionano client-side senza nessun roundtrip server, anche se il backend WUIC non ha registrato la route. Vedi <code>data-source.component.ts</code> (short-circuit dentro <code>fetchData()</code>).</p><h3>Modalita&#39; client-side (default consigliato per Pattern 3)</h3><p>Snippet 5:</p><ul><li>Carichi <strong>tutte</strong> le righe con un singolo <code>fetchInfo$.next</code>.</li><li>La list-grid applica filtri/sort/paging/export sull&#39;array gia&#39; presente.</li><li>Zero codice aggiuntivo.</li><li>Indicato per dataset piccoli/medi (ordine di qualche migliaio di righe).</li></ul><h3>Modalita&#39; server-side (wiring manuale)</h3><p>Per dataset grandi (decine/centinaia di migliaia di righe) <strong>non vuoi</strong> caricare tutto in memoria. Lasci <code>md_server_side_operations: true</code> (default), ti sottoscrivi agli <strong>@Output di `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) e re-chiami il tuo endpoint REST a ogni cambio di stato. La list-grid aggiorna <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>prima</strong> di emettere l&#39;evento, quindi nel handler basta leggere lo stato corrente.</p><p>Snippet 6:</p><p>Endpoint server complementare (esempio C#, vedi <code>SamplesController.GetInventory</code>):</p><p>Snippet 7:</p><p><strong>Punti chiave:</strong></p><ul><li>Gli <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> di <code>&lt;wuic-list-grid&gt;</code> espongono gli eventi UI <strong>dopo</strong> che il list-grid handler ha gia&#39; aggiornato lo stato del datasource. Niente da reimplementare: leggi <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>Il backend deve ritornare <code>{ rows, total }</code> dove <code>total</code> e&#39; il count <strong>POST-filter / PRE-page</strong>. Senza questo il pager UI non sa quante pagine esistono e non funziona correttamente.</li><li>L&#39;operatore di filtro arriva nel campo <code>operatore</code> della filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, ecc., vedi tabella matchMode in <a href=\"./list-grid.md\">List Grid</a>). Mappalo coerentemente lato server.</li></ul><h2>Variante: consumare l&#39;endpoint OData del framework</h2><p>Se l&#39;entita&#39; che vuoi visualizzare e&#39; gia&#39; esposta dal framework come entity set OData (<code>/odata/&lt;EntitySet&gt;</code>), non serve scrivere NESSUN controller: basta tradurre lo stato UI della list-grid in query string OData v4 standard.</p><p>&gt; <strong>Alternativa 100% framework-driven (Pattern 1 con backend OData)</strong>: se accetti di registrare una route metadata standard per l&#39;entita&#39;, puoi configurare <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> e il datasource fa <strong>tutto da solo</strong> (filter/sort/paging/export) tramite il provider OData interno. Niente codice Angular custom. Vedi <a href=\"./odata.md\">OData</a> per il setup completo. Pattern 3 (questa pagina) si applica invece quando vuoi controllo esplicito lato frontend o non hai metadata registrati per l&#39;entita&#39;.</p><p>Il framework espone <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> che fa tutto il mapping operator WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) con quoting automatico per string/numeric, supporto nested filter groups (AND/OR ricorsivi) e isnull/isnotnull. Il return e&#39; una URL relativa tipo <code>/odata/Cities?$filter=&lt;espressione encoded&gt;</code>. Ti basta prefissare la base URL e aggiungere <code>$top / $skip / $orderby</code>.</p><p>Snippet 8:</p><p>&gt; <strong>Nota su total/count</strong>: se l&#39;endpoint OData che usi e&#39; configurato per ritornare il wrapper OData standard <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (via <code>$count=true</code>), puoi leggere il total direttamente dalla response senza la seconda query. L&#39;endpoint del framework WUIC attualmente ritorna il plain array e richiede la query parallela.</p><h2>Esempi vivi nel WuicTest</h2><p>I tre esempi coprono le <strong>tre strategie principali</strong> del Pattern 3:</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; carica TUTTI i 50 post da un endpoint esterno (<code>jsonplaceholder.typicode.com/posts</code>) in un colpo solo, paging/sort/filter applicati in-memory dalla list-grid (<code>md_server_side_operations: false</code>). Cartella sorgente: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Apri demo</a>.</li><li><strong>Custom .NET grid (server-side, REST custom)</strong> &rarr; chiama il Controller <code>SamplesController.GetInventory</code> con offset/limit/sort/filter come query params ad hoc, ricarica solo la pagina corrente ad ogni cambio (<code>md_server_side_operations: true</code> + wiring esplicito su <code>(onPaging)/(onSorting)/(onFiltering)</code>). Cartella sorgente: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Apri demo</a>.</li><li><strong>OData Cities grid (server-side, standard OData v4)</strong> &rarr; consuma l&#39;endpoint OData generico del framework (<code>GET /odata/Cities</code>) con query string standard <code>$top / $skip / $filter / $orderby</code>, nessun controller custom da scrivere. Traduce gli eventi UI della list-grid in query OData (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, ecc.). Cartella sorgente: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Apri demo</a>.</li></ul><h3>Quando scegliere quale variante</h3><table><thead><tr><th>Esempio</th><th>Strategia</th><th>Backend</th><th>Quando usarla</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Endpoint REST classico che ritorna un array</td><td>Dataset piccolo-medio (&lt; qualche migliaio di righe), semplicita&#39; massima, API 3rd-party senza controllo server-side</td></tr><tr><td><strong>3b</strong></td><td>Server-side REST custom</td><td>Controller REST tuo con query params di paging/sort/filter</td><td>Dataset grande, vuoi controllo totale sulla query; l&#39;end-dev ha gia&#39; un endpoint esistente con offset/limit/ecc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Endpoint OData del framework (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Dataset grande esposto automaticamente dal framework come OData set; zero codice backend; sintassi standard compatibile con altri client</td></tr></tbody></table><h2>Vedi anche</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full autogeneration</a>: se l&#39;UX standard basta e i dati esistono nel modello scaffoldato.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework data + Custom component</a>: inverso (UI custom, dati framework).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full custom</a>: se non ti serve nemmeno la list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework component + Framework data (manual mount)</a>: variante &quot;framework&quot; di questo pattern: stessa compozione manuale dei widget, ma data layer metadata-driven invece che backend custom.</li></ul>",
1524
1524
  "codeSamples": [
1525
1525
  {
1526
1526
  "id": "code_1",
@@ -1543,24 +1543,30 @@ const frameworkDocsContent = {
1543
1543
  {
1544
1544
  "id": "code_4",
1545
1545
  "title": "Snippet 4",
1546
- "language": "ts",
1547
- "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() per ereditare i default (md_sortable, ecc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- chiave: forza in-memory\r\ntableMeta.md_pageable = true; // abilita paginazione UI\r\ntableMeta.md_pagesize = 10; // righe per pagina\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
1546
+ "language": "csharp",
1547
+ "code": "await using var cn = new OracleConnection(cs);\r\nawait cn.OpenAsync();\r\n\r\n// 1) porta la sessione sullo schema dei dati (equivalente a cio' che fa il framework)\r\nawait using (var set = new OracleCommand(\"ALTER SESSION SET CURRENT_SCHEMA = MIO_SCHEMA\", cn))\r\n await set.ExecuteNonQueryAsync();\r\n\r\n// 2) oppure qualifica le tabelle: SELECT ... FROM MIO_SCHEMA.PRODUCT"
1548
1548
  },
1549
1549
  {
1550
1550
  "id": "code_5",
1551
1551
  "title": "Snippet 5",
1552
1552
  "language": "ts",
1553
- "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Usa l'URL assoluta del backend o `WtoolboxService.appSettings.api_url`\r\n // se il tuo dev server frontend non e' co-locato col backend (caso tipico\r\n // Angular `:4200` + ASP.NET `:5000` senza proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations resta `true` (default) -> p-table in [lazy]=\"true\",\r\n // niente sort/filter/paging interno: ricarichiamo la pagina giusta da server.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
1553
+ "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() per ereditare i default (md_sortable, ecc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- chiave: forza in-memory\r\ntableMeta.md_pageable = true; // abilita paginazione UI\r\ntableMeta.md_pagesize = 10; // righe per pagina\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
1554
1554
  },
1555
1555
  {
1556
1556
  "id": "code_6",
1557
1557
  "title": "Snippet 6",
1558
- "language": "csharp",
1559
- "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter prima del count: total deve essere POST-filter / PRE-page,\r\n // altrimenti il pager UI mostra pagine vuote.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
1558
+ "language": "ts",
1559
+ "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Usa l'URL assoluta del backend o `WtoolboxService.appSettings.api_url`\r\n // se il tuo dev server frontend non e' co-locato col backend (caso tipico\r\n // Angular `:4200` + ASP.NET `:5000` senza proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations resta `true` (default) -> p-table in [lazy]=\"true\",\r\n // niente sort/filter/paging interno: ricarichiamo la pagina giusta da server.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
1560
1560
  },
1561
1561
  {
1562
1562
  "id": "code_7",
1563
1563
  "title": "Snippet 7",
1564
+ "language": "csharp",
1565
+ "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter prima del count: total deve essere POST-filter / PRE-page,\r\n // altrimenti il pager UI mostra pagine vuote.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
1566
+ },
1567
+ {
1568
+ "id": "code_8",
1569
+ "title": "Snippet 8",
1564
1570
  "language": "ts",
1565
1571
  "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient } from '@angular/common/http';\r\nimport { BehaviorSubject, forkJoin } from 'rxjs';\r\nimport {\r\n DataProviderOdataService,\r\n DataSourceComponent, ListGridComponent,\r\n FilterInfo, MetaInfo, MetadatiColonna, MetadatiTabella,\r\n WtoolboxService\r\n} from 'wuic-framework-lib';\r\n\r\n@Component({ /* ... */ })\r\nexport class ODataCitiesGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private odataSrv = inject(DataProviderOdataService);\r\n // ... meta, fd init (come 3b) ...\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = (this.ds?.filterInfo || new FilterInfo('AND', [])) as FilterInfo;\r\n const base = String(WtoolboxService.appSettings.file_path || '').replace(/\\/$/, '');\r\n\r\n // 1) URL con $filter gia' tradotto dal framework:\r\n // es. '/odata/Cities?$filter=contains(cityName%2C%20%27Mil%27)'\r\n const baseRelUrl = this.odataSrv.filterInfoToOdata(filter, 'Cities');\r\n const sep = baseRelUrl.includes('?') ? '&' : '?';\r\n\r\n // 2) Aggiungi $top/$skip/$orderby come extra query params\r\n const pageExtras = [ `$top=${pageSize}`, `$skip=${(page - 1) * pageSize}` ];\r\n if (sort?.field) {\r\n pageExtras.push(`$orderby=${encodeURIComponent(`${sort.field} ${sort.dir || 'asc'}`)}`);\r\n }\r\n const pageUrl = `${base}${baseRelUrl}${sep}${pageExtras.join('&')}`;\r\n\r\n // 3) Count query: stesso filter, $select=cityID per payload minimo\r\n const countUrl = `${base}${baseRelUrl}${sep}$select=cityID`;\r\n\r\n forkJoin({\r\n page: this.http.get<any[]>(pageUrl),\r\n all: this.http.get<any[]>(countUrl)\r\n }).subscribe(({ page, all }) => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: page, totalRowCount: all.length, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n}"
1566
1572
  }
@@ -7883,7 +7889,7 @@ const frameworkDocsContent = {
7883
7889
  {
7884
7890
  "id": "overview",
7885
7891
  "title": "Overview",
7886
- "html": "<h1>Framework Overview</h1><p>WUIC Framework is a metadata-driven platform for building business interfaces quickly and consistently.</p><h2>What It Includes</h2><ul><li>DataSource and DataRepeater for orchestrating data and rendering.</li><li>Visual archetypes (list, map, scheduler, chart, carousel).</li><li>Runtime designer for dashboards and dynamic templates.</li><li>Workflow designer/runner for operational processes.</li><li>RAG Chatbot for querying the codebase in natural language.</li><li>Multi-DBMS: SQL Server, MySQL, PostgreSQL and Oracle via drop-in providers (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Starting the Dev Environment</h2><p>The WUIC stack is composed of several services. Start them in the order below.</p><h3>Prerequisites</h3><table><thead><tr><th>What</th><th>Check</th><th>Notes</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle for IIS in production</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>22 is the version the package is tested with; on npm 10.9.x it installs thanks to the bundled <code>package-lock.json</code></td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>The Windows PowerShell that ships with Windows is enough: <code>rename-project.ps1</code> and the <code>llm-workspace/</code> scripts run on 5.1 too. PowerShell 7 is recommended, not required</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>If missing, <strong>the one-liner installs it for you</strong> (SQL Server Express, or the engine chosen with <code>-Dbms</code>); not on Windows Server, see the box below. 2019 is enough for the packages without <code>.bak</code>; the <code>.bak</code> tutorial is a SQL Server 2022 backup and needs <strong>2022 or newer</strong></td></tr><tr><td>Linux (<code>install.sh</code> installer)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 or 24.04 LTS</strong> (24.04 recommended): they are the only Ubuntu releases Microsoft supports for SQL Server, and <strong>26.04 is not supported</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Same recommendation with MySQL, PostgreSQL and Oracle, because the installer is tested on those releases</td></tr><tr><td>NVIDIA GPU (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Optional: speeds up the RAG Chatbot (runs on CPU without it)</td></tr></tbody></table><p>&gt; <strong>On Windows Server the database must be installed first.</strong> The one-liner pulls missing</p><p>&gt; components through <code>winget</code>, which is not present on Windows Server: without an instance</p><p>&gt; already reachable the installation stops with `No SQL Server instance reachable and winget is</p><p>&gt; missing`. Install the engine before running the line - SQL Server must be <strong>2019 or newer</strong></p><p>&gt; (Express is enough; <strong>2022 or newer</strong> for the <code>.bak</code> tutorial), and a 2017 instance is not</p><p>&gt; accepted. If the instance is not the default one, pass it with</p><p>&gt; <code>-SqlServer &#39;localhost\\INSTANCENAME&#39;</code>. This applies to every engine, not just SQL Server.</p><h3>1. Download the project template</h3><p>On Windows one PowerShell line does all of it: it downloads the package, checks the .NET SDK 10</p><p>and Node.js 22 (installing what is missing), runs <code>dotnet restore</code> and <code>npm install</code>,</p><p>and installs the WUIC Assistant extension in VS Code.</p><p>Snippet 1:</p><p>Useful options on the same line: <code>-Dbms mysql</code> (or <code>postgres</code>, <code>oracle</code>) for an engine other</p><p>than SQL Server — the installer installs and configures it for you — and <code>-WithTutorial</code> to</p><p>download the sample database as well.</p><p>When it is done, the last screen is already your to-do list:</p><p>Snippet 2:</p><p>The connection string is ready to use: at first start the wizard asks for nothing else. With</p><p><code>-Dbms mysql</code> or <code>-Dbms postgres</code> that line changes accordingly and carries user and password</p><p>inside it, because the superuser password is <strong>generated by the installer</strong>; the same password</p><p>is saved in <code>wuic-secrets.json</code>, next to the installation, and that is the only place where it</p><p>stays written.</p><p>The manual path still works, on any operating system:</p><p>Start from the <code>WuicTest-src-*.zip</code> package (link in the <strong>Download</strong> section of the site).</p><p>Extract it to a working folder, e.g. <code>C:\\dev\\WuicTest</code> (files go directly at the root —</p><p>NOT inside a <code>src/</code> subfolder). The package contains:</p><ul><li><code>WuicTest.csproj</code> → host project with <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → Angular app whose <code>package.json</code> references <code>wuic-framework-lib</code> from the npm registry</li><li><code>appsettings.json</code> preconfigured in <code>firstRun = &quot;true&quot;</code> mode with <code>__SET_*__</code> placeholders</li><li><code>appsettings-samples/</code> → 6 ready templates (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>No need to clone the framework repository</strong>: <code>WuicCore</code> ships as a NuGet package from</p><p>&gt; <code>nuget.org</code> and <code>wuic-framework-lib</code> as an npm package from the npm registry. The framework</p><p>&gt; source is only needed if you want to modify it — typically not your case.</p><p>&gt; <strong>npm 10.9.x and packages older than 1.7.1</strong>: those packages carry no <code>package-lock.json</code>, and</p><p>&gt; without a lock npm 10.9.x (the version Node 22 LTS installs) stops with</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> while resolving peer dependencies.</p><p>&gt; Download the lock published next to the release —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;zip-name&gt;.package-lock.json</code> — drop it in <code>wwwroot/</code></p><p>&gt; and run <code>npm install</code> again. The <code>-Src</code> one-liner does it for you.</p><h3>2. Database</h3><p>You need a reachable engine instance. <strong>With the one-liner there is nothing to do</strong>: it</p><p>installs and configures the instance, and the connection string to paste into the wizard is the</p><p>last thing it prints. On the manual path the instance must already exist.</p><p>Connections in <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — metadata DB (menu, tables, columns, boards, users)</li><li><code>DataSQLConnection</code> — application data DB</li></ul><p>If this is the first installation (<code>AppSettings.firstRun = &quot;true&quot;</code> in the template),</p><p>the scaffolding wizard on first startup creates the metadata schema and populates the data DB.</p><p>See the <em>Initial Scaffolding</em> page for the guided flow.</p><p>#### What the first-run wizard asks for</p><p>On the first start the application does not show the login form but the **Initial project</p><p>setup** page. You fill it in once: when it finishes <code>firstRun</code> flips to <code>false</code> and the page</p><p>never comes back.</p><ul><li><strong>Setup mode</strong> — two entries: <strong>Existing database</strong>, which registers a database of your own,</li></ul><p> and <strong>Tutorial WideWorldImporters</strong>, which installs the sample database. The second one only</p><p> appears when the package ships the tutorial (<code>tutorialAvailable</code>): in packages without it the</p><p> dropdown is not rendered at all and the mode is &quot;Existing database&quot;.</p><ul><li><strong>DBMS</strong> — the engine. Changing it rewrites the connection string below in the shape of the</li></ul><p> chosen provider.</p><ul><li><strong>DataSQLConnection</strong> — the string to the data database, and the field everything else</li></ul><p> depends on: until you press <strong>Test connection and load databases</strong> the database list stays</p><p> disabled. The test validates the credentials, lists the databases on the server and, on SQL</p><p> Server, fixes the certificate parameters for you</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>), telling you it did. <code>Integrated Security</code></p><p> only works on SQL Server: the other engines need a user and a password.</p><ul><li><strong>Data database</strong> and <strong>Metadata database name</strong> — the first is picked from the list the test</li></ul><p> loaded (in tutorial mode it is a free-text field, <code>WideWorldImporters</code> by default); the</p><p> second is the name of the metadata database to create (<code>metadataDB</code> by default,</p><p> <code>MetadataCRM</code> in tutorial mode). If that database already exists, the wizard asks before</p><p> recreating it.</p><ul><li><strong>Scaffold the tables automatically</strong> (only in &quot;Existing database&quot;) — fills</li></ul><p> <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> from every table of the chosen database. It is</p><p> what lengthens provisioning the most on databases with many tables.</p><ul><li><strong>Initial admin user</strong> — username (<code>admin</code> by default), password (at least 4 characters) and</li></ul><p> language. There is no default password: the one you type here is the only one that will let</p><p> you in at the first login. The language you pick is applied to the admin user and to this</p><p> page itself.</p><ul><li><strong>RAG Chatbot</strong> — only the Anthropic key, and it is optional. Nothing to install: see step 5.</li></ul><p>Once you press <strong>Confirm and create the metadata database</strong> provisioning runs on its own, with</p><p>a progress bar and the number of batches processed. How long it takes, measured on the test</p><p>installations: <strong>30 s - 2 min</strong> in tutorial mode, <strong>1-4 min</strong> on an existing database with</p><p>automatic scaffolding enabled. When it ends, the page gives way to the login form.</p><h3>3. Backend (.NET)</h3><p>From the root of the extracted folder (e.g. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>The backend exposes APIs on <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>In a second shell, from the <code>wwwroot/</code> subfolder:</p><p>Snippet 4:</p><p>The frontend is available on <code>http://localhost:4200</code>.</p><p>The credentials are those of the admin user you chose in the first-run wizard: the username</p><p>is <code>admin</code> unless you changed it, and the password is the one you typed (there is no default</p><p>password).</p><h3>5. RAG Chatbot (optional)</h3><p>The RAG Chatbot (<code>Administration &gt; RAG Chat</code>) runs <strong>inside the .NET backend</strong>:</p><p>the retrieval engine (<code>rag-engine/WuicRagEngine.dll</code>, in-process ONNX Runtime)</p><p>is loaded by the backend when <code>AppSettings.rag-use-dotnet-engine</code> is <code>&quot;true&quot;</code></p><p>(the default in release packages). **No Python, no venv, no separate server</p><p>to start.**</p><p>The <strong>first</strong> time the chatbot is opened (or at the end of the first run) the</p><p>backend downloads by itself the ONNX models (<code>bge-m3</code> + reranker), tokenizer</p><p>and index (~4.5 GB) from <code>rag-engine-models-url</code> (default</p><p><code>https://wuic-framework.com/rag-models</code>) into <code>rag-engine/artifacts/</code>. An</p><p>internet connection is needed only once; the download runs in the background</p><p>(1-5 min) and an in-app notification is sent when it starts and when it ends.</p><p>Meanwhile the component shows the <strong>RAG offline</strong> status and the rest of the</p><p>application works normally.</p><p>Relevant <code>AppSettings</code> keys (all with ready defaults):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — enables the .NET engine (<code>false</code> = Python fallback, see below)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: with an NVIDIA GPU (CUDA 12.x + cuDNN 9) ~1 s/query, on CPU ~15-25 s/query</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = framework sources as signatures only; <code>internal</code> = full index</li><li><code>rag-engine-models-url</code> — download source on first start</li><li><code>rag-engine-cuda-path</code> = (empty) — folder of the CUDA/cuDNN DLLs when they are not installed system-wide</li></ul><p>For LLM chat set <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> (Claude via Anthropic,</p><p>or a local model via Ollama with <code>rag-llm-base-url</code>). Without a provider only</p><p>the retrieval mode (snippet search) works. Key details on the <em>AppSettings</em></p><p>page.</p><p>&gt; The legacy Python stack (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> on</p><p>&gt; <code>127.0.0.1:8765</code>) is only usable as a fallback with</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> and is not shipped in release packages.</p><h2>Services Summary</h2><table><thead><tr><th>Service</th><th>Port</th><th>Required</th><th>Quick command</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>yes</td><td>(Windows service)</td></tr><tr><td>.NET Backend</td><td>5000</td><td>yes</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Angular Frontend</td><td>4200</td><td>yes</td><td><code>npm run serve:dev</code></td></tr><tr><td>.NET RAG engine (ONNX)</td><td>in-process</td><td>no</td><td>loaded by the backend on the first chat</td></tr></tbody></table><h2>Quick Start — First Component</h2><p>1. Configure the AsmxProxy endpoint.</p><p>2. Define route metadata and columns.</p><p>3. Mount <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>The first lists show 20 records: that is Trial mode</h2><p>A fresh install runs <strong>without a license</strong>, that is, in Trial mode, and in that mode</p><p><strong>every query is capped at 20 records</strong>. It is neither a bug nor a database limit: a list</p><p>that reads &quot;20 of 20&quot; on a table with thousands of rows is simply running unlicensed.</p><p>How to confirm it in a second:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> confirms the Trial, and <code>licenseReason</code> says why</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>How to get out of it: paste the license from <strong>Administration &gt; AppSettings Editor</strong>, Licensing</p><p>tab, and it takes effect immediately - no backend restart needed. The details, including the</p><p>limits the Trial imposes beyond the record count, are in the <a href=\"licensing.md\">Licensing</a> page.</p>",
7892
+ "html": "<h1>Framework Overview</h1><p>WUIC Framework is a metadata-driven platform for building business interfaces quickly and consistently.</p><h2>What It Includes</h2><ul><li>DataSource and DataRepeater for orchestrating data and rendering.</li><li>Visual archetypes (list, map, scheduler, chart, carousel).</li><li>Runtime designer for dashboards and dynamic templates.</li><li>Workflow designer/runner for operational processes.</li><li>RAG Chatbot for querying the codebase in natural language.</li><li>Multi-DBMS: SQL Server, MySQL, PostgreSQL and Oracle via drop-in providers (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Starting the Dev Environment</h2><p>The WUIC stack is composed of several services. Start them in the order below.</p><h3>Prerequisites</h3><table><thead><tr><th>What</th><th>Check</th><th>Notes</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle for IIS in production</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>22 is the version the package is tested with; on npm 10.9.x it installs thanks to the bundled <code>package-lock.json</code></td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>The Windows PowerShell that ships with Windows is enough: <code>rename-project.ps1</code> and the <code>llm-workspace/</code> scripts run on 5.1 too. PowerShell 7 is recommended, not required</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>If missing, <strong>the one-liner installs it for you</strong> (SQL Server Express, or the engine chosen with <code>-Dbms</code>); not on Windows Server, see the box below. 2019 is enough for the packages without <code>.bak</code>; the <code>.bak</code> tutorial is a SQL Server 2022 backup and needs <strong>2022 or newer</strong></td></tr><tr><td>Linux (<code>install.sh</code> installer)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 or 24.04 LTS</strong> (24.04 recommended): they are the only Ubuntu releases Microsoft supports for SQL Server, and <strong>26.04 is not supported</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Same recommendation with MySQL, PostgreSQL and Oracle, because the installer is tested on those releases</td></tr><tr><td>NVIDIA GPU (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Optional: speeds up the RAG Chatbot (runs on CPU without it)</td></tr></tbody></table><p>&gt; <strong>On Windows Server the database must be installed first.</strong> The one-liner pulls missing</p><p>&gt; components through <code>winget</code>, which is not present on Windows Server: without an instance</p><p>&gt; already reachable the installation stops with `No SQL Server instance reachable and winget is</p><p>&gt; missing`. Install the engine before running the line - SQL Server must be <strong>2019 or newer</strong></p><p>&gt; (Express is enough; <strong>2022 or newer</strong> for the <code>.bak</code> tutorial), and a 2017 instance is not</p><p>&gt; accepted. If the instance is not the default one, pass it with</p><p>&gt; <code>-SqlServer &#39;localhost\\INSTANCENAME&#39;</code>. This applies to every engine, not just SQL Server.</p><h3>1. Download the project template</h3><p>On Windows one PowerShell line does all of it: it downloads the package, checks the .NET SDK 10</p><p>and Node.js 22 (installing what is missing), runs <code>dotnet restore</code> and <code>npm install</code>,</p><p>and installs the WUIC Assistant extension in VS Code.</p><p>Snippet 1:</p><p>Useful options on the same line: <code>-Dbms mysql</code> (or <code>postgres</code>, <code>oracle</code>) for an engine other</p><p>than SQL Server — the installer installs and configures it for you — and <code>-WithTutorial</code> to</p><p>download the sample database as well.</p><p>When it is done, the last screen is already your to-do list: open the workspace file</p><p><code>WuicTest.code-workspace</code> in VS Code and press <strong>F5</strong>, which starts backend and frontend</p><p>together through the <code>Fullstack: WuicTest + Chrome</code> launcher (the launchers live in</p><p><code>.vscode/launch.json</code>, next to the backend-only one). If you prefer the terminal, the</p><p>equivalent commands are further down this page.</p><p>Snippet 2:</p><p>The connection string is ready to use: at first start the wizard asks for nothing else. With</p><p><code>-Dbms mysql</code> or <code>-Dbms postgres</code> that line changes accordingly and carries user and password</p><p>inside it, because the superuser password is <strong>generated by the installer</strong>; the same password</p><p>is saved in <code>wuic-secrets.json</code>, next to the installation, and that is the only place where it</p><p>stays written.</p><p>The manual path still works, on any operating system:</p><p>Start from the <code>WuicTest-src-*.zip</code> package (link in the <strong>Download</strong> section of the site).</p><p>Extract it to a working folder, e.g. <code>C:\\dev\\WuicTest</code> (files go directly at the root —</p><p>NOT inside a <code>src/</code> subfolder). The package contains:</p><ul><li><code>WuicTest.csproj</code> → host project with <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → Angular app whose <code>package.json</code> references <code>wuic-framework-lib</code> from the npm registry</li><li><code>appsettings.json</code> preconfigured in <code>firstRun = &quot;true&quot;</code> mode with <code>__SET_*__</code> placeholders</li><li><code>appsettings-samples/</code> → 6 ready templates (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>No need to clone the framework repository</strong>: <code>WuicCore</code> ships as a NuGet package from</p><p>&gt; <code>nuget.org</code> and <code>wuic-framework-lib</code> as an npm package from the npm registry. The framework</p><p>&gt; source is only needed if you want to modify it — typically not your case.</p><p>&gt; <strong>npm 10.9.x and packages older than 1.7.1</strong>: those packages carry no <code>package-lock.json</code>, and</p><p>&gt; without a lock npm 10.9.x (the version Node 22 LTS installs) stops with</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> while resolving peer dependencies.</p><p>&gt; Download the lock published next to the release —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;zip-name&gt;.package-lock.json</code> — drop it in <code>wwwroot/</code></p><p>&gt; and run <code>npm install</code> again. The <code>-Src</code> one-liner does it for you.</p><h3>2. Database</h3><p>You need a reachable engine instance. <strong>With the one-liner there is nothing to do</strong>: it</p><p>installs and configures the instance, and the connection string to paste into the wizard is the</p><p>last thing it prints. On the manual path the instance must already exist.</p><p>Connections in <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — metadata DB (menu, tables, columns, boards, users)</li><li><code>DataSQLConnection</code> — application data DB</li></ul><p>If this is the first installation (<code>AppSettings.firstRun = &quot;true&quot;</code> in the template),</p><p>the scaffolding wizard on first startup creates the metadata schema and populates the data DB.</p><p>See the <em>Initial Scaffolding</em> page for the guided flow.</p><p>#### What the first-run wizard asks for</p><p>On the first start the application does not show the login form but the **Initial project</p><p>setup** page. You fill it in once: when it finishes <code>firstRun</code> flips to <code>false</code> and the page</p><p>never comes back.</p><ul><li><strong>Setup mode</strong> — two entries: <strong>Existing database</strong>, which registers a database of your own,</li></ul><p> and <strong>Tutorial WideWorldImporters</strong>, which installs the sample database. The second one only</p><p> appears when the package ships the tutorial (<code>tutorialAvailable</code>): in packages without it the</p><p> dropdown is not rendered at all and the mode is &quot;Existing database&quot;.</p><ul><li><strong>DBMS</strong> — the engine. Changing it rewrites the connection string below in the shape of the</li></ul><p> chosen provider.</p><ul><li><strong>DataSQLConnection</strong> — the string to the data database, and the field everything else</li></ul><p> depends on: until you press <strong>Test connection and load databases</strong> the database list stays</p><p> disabled. The test validates the credentials, lists the databases on the server and, on SQL</p><p> Server, fixes the certificate parameters for you</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>), telling you it did. <code>Integrated Security</code></p><p> only works on SQL Server: the other engines need a user and a password.</p><ul><li><strong>Data database</strong> and <strong>Metadata database name</strong> — the first is picked from the list the test</li></ul><p> loaded (in tutorial mode it is a free-text field, <code>WideWorldImporters</code> by default); the</p><p> second is the name of the metadata database to create (<code>metadataDB</code> by default,</p><p> <code>MetadataCRM</code> in tutorial mode). If that database already exists, the wizard asks before</p><p> recreating it.</p><ul><li><strong>Scaffold the tables automatically</strong> (only in &quot;Existing database&quot;) — fills</li></ul><p> <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> from every table of the chosen database. It is</p><p> what lengthens provisioning the most on databases with many tables.</p><ul><li><strong>Initial admin user</strong> — username (<code>admin</code> by default), password (at least 4 characters) and</li></ul><p> language. There is no default password: the one you type here is the only one that will let</p><p> you in at the first login. The language you pick is applied to the admin user and to this</p><p> page itself.</p><ul><li><strong>RAG Chatbot</strong> — only the Anthropic key, and it is optional. Nothing to install: see step 5.</li></ul><p>Once you press <strong>Confirm and create the metadata database</strong> provisioning runs on its own, with</p><p>a progress bar and the number of batches processed. How long it takes, measured on the test</p><p>installations: <strong>30 s - 2 min</strong> in tutorial mode, <strong>1-4 min</strong> on an existing database with</p><p>automatic scaffolding enabled. When it ends, the page gives way to the login form.</p><h3>3. Backend (.NET)</h3><p>From the root of the extracted folder (e.g. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>The backend exposes APIs on <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>In a second shell, from the <code>wwwroot/</code> subfolder:</p><p>Snippet 4:</p><p>The frontend is available on <code>http://localhost:4200</code>.</p><p>The credentials are those of the admin user you chose in the first-run wizard: the username</p><p>is <code>admin</code> unless you changed it, and the password is the one you typed (there is no default</p><p>password).</p><h3>5. RAG Chatbot (optional)</h3><p>The RAG Chatbot (<code>Administration &gt; RAG Chat</code>) runs <strong>inside the .NET backend</strong>:</p><p>the retrieval engine (<code>rag-engine/WuicRagEngine.dll</code>, in-process ONNX Runtime)</p><p>is loaded by the backend when <code>AppSettings.rag-use-dotnet-engine</code> is <code>&quot;true&quot;</code></p><p>(the default in release packages). **No Python, no venv, no separate server</p><p>to start.**</p><p>The <strong>first</strong> time the chatbot is opened (or at the end of the first run) the</p><p>backend downloads by itself the ONNX models (<code>bge-m3</code> + reranker), tokenizer</p><p>and index (~4.5 GB) from <code>rag-engine-models-url</code> (default</p><p><code>https://wuic-framework.com/rag-models</code>) into <code>rag-engine/artifacts/</code>. An</p><p>internet connection is needed only once; the download runs in the background</p><p>(1-5 min) and an in-app notification is sent when it starts and when it ends.</p><p>Meanwhile the component shows the <strong>RAG offline</strong> status and the rest of the</p><p>application works normally.</p><p>Relevant <code>AppSettings</code> keys (all with ready defaults):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — enables the .NET engine (<code>false</code> = Python fallback, see below)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: with an NVIDIA GPU (CUDA 12.x + cuDNN 9) ~1 s/query, on CPU ~15-25 s/query</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = framework sources as signatures only; <code>internal</code> = full index</li><li><code>rag-engine-models-url</code> — download source on first start</li><li><code>rag-engine-cuda-path</code> = (empty) — folder of the CUDA/cuDNN DLLs when they are not installed system-wide</li></ul><p>For LLM chat set <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> (Claude via Anthropic,</p><p>or a local model via Ollama with <code>rag-llm-base-url</code>). Without a provider only</p><p>the retrieval mode (snippet search) works. Key details on the <em>AppSettings</em></p><p>page.</p><p>&gt; The legacy Python stack (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> on</p><p>&gt; <code>127.0.0.1:8765</code>) is only usable as a fallback with</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> and is not shipped in release packages.</p><h2>Services Summary</h2><table><thead><tr><th>Service</th><th>Port</th><th>Required</th><th>Quick command</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>yes</td><td>(Windows service)</td></tr><tr><td>.NET Backend</td><td>5000</td><td>yes</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Angular Frontend</td><td>4200</td><td>yes</td><td><code>npm run serve:dev</code></td></tr><tr><td>.NET RAG engine (ONNX)</td><td>in-process</td><td>no</td><td>loaded by the backend on the first chat</td></tr></tbody></table><h2>Quick Start — First Component</h2><p>1. Configure the AsmxProxy endpoint.</p><p>2. Define route metadata and columns.</p><p>3. Mount <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>The first lists show 20 records: that is Trial mode</h2><p>A fresh install runs <strong>without a license</strong>, that is, in Trial mode, and in that mode</p><p><strong>every query is capped at 20 records</strong>. It is neither a bug nor a database limit: a list</p><p>that reads &quot;20 of 20&quot; on a table with thousands of rows is simply running unlicensed.</p><p>How to confirm it in a second:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> confirms the Trial, and <code>licenseReason</code> says why</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>How to get out of it: paste the license from <strong>Administration &gt; AppSettings Editor</strong>, Licensing</p><p>tab, and it takes effect immediately - no backend restart needed. The details, including the</p><p>limits the Trial imposes beyond the record count, are in the <a href=\"licensing.md\">Licensing</a> page.</p>",
7887
7893
  "codeSamples": [
7888
7894
  {
7889
7895
  "id": "code_1",
@@ -8246,7 +8252,7 @@ const frameworkDocsContent = {
8246
8252
  {
8247
8253
  "id": "overview",
8248
8254
  "title": "Overview",
8249
- "html": "<h1>Pattern: Framework component + Custom data</h1><p>The high-level UI components of the framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) accept a <strong>hand-built datasource</strong> (<code>hardcodedDatasource</code>) that does not go through the framework&#39;s data layer. Data comes from your custom backend (your own .NET Controller, external REST, static files, websocket, ...).</p><h2>When to Use It</h2><ul><li>You want the full UX of a list-grid/chart-list (filters, sort, client paging, export, edit dialog) but the data is produced by:</li></ul><p> - An external REST endpoint (3rd-party API, microservice).</p><p> - A .NET Controller of yours not integrated with the framework data layer.</p><p> - Static files, aggregate calculations, live data (websocket, polling).</p><ul><li>You have a legacy domain you don&#39;t want to model in the framework.</li><li>You are prototyping without yet having defined the data structure.</li></ul><h2>Architecture</h2><ul><li><strong>Developer</strong>: writes a small Angular component that fetches data from its backend and packages it in a local datasource.</li><li><strong>Framework</strong>: the list-grid behaves exactly as if the data came from the standard data layer (filters, sort, paging, export all work).</li><li><strong>Backend</strong>: total freedom. Classic REST endpoints, no metadata conventions.</li></ul><h2>What You Do (Frontend)</h2><p>You create a standalone Angular component that:</p><p>1. Calls your custom endpoint with <code>HttpClient</code>.</p><p>2. Defines the columns (name, label, type) for the local datasource.</p><p>3. Publishes rows + columns on the datasource and passes it to the list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>What You Do (Backend, Optional)</h2><p>If the data comes from your internal backend, a classic REST Controller is enough. No framework conventions, no metadata to write.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h2>Trade-offs</h2><table><thead><tr><th>Pros</th><th>Cons</th></tr></thead><tbody><tr><td><code>wuic-list-grid</code> UX with filters/sort/paging/export for free (client-side)</td><td>You maintain the consistency between rows and column definitions</td></tr><tr><td>Completely free backend</td><td>CRUD does not work out-of-the-box: edit/insert must be wired manually on your backend</td></tr><tr><td>Good for 3rd-party integrations</td><td>Server-side paging/sort/filter requires custom wiring (see below)</td></tr><tr><td>No scaffolding work</td><td>Type safety only via cast</td></tr></tbody></table><h2>Filters / Sort / Paging: Client-Side vs Server-Side</h2><p>The &quot;filters/sort/paging/export for free&quot; statement in the Trade-offs table applies <strong>only in client-side mode</strong>, and is subject to a metadata flag that must be explicitly set in the hardcoded datasource.</p><h3>The Key Flag: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (property of <code>MetadatiTabella</code>) controls <strong>where</strong> paging/sort/filter are executed:</p><table><thead><tr><th>Value</th><th>Meaning</th><th>When to use</th></tr></thead><tbody><tr><td><code>true</code> (default)</td><td>The list-grid sends paging/sort/filter events to the backend via the framework&#39;s standard CRUD endpoint. The backend returns only the requested page and applies sort/filter SQL-side.</td><td>Patterns 1 and 2 (with real metadata route and WUIC backend behind).</td></tr><tr><td><code>false</code></td><td>The list-grid executes paging/sort/filter in-memory on the already-loaded array. No server roundtrip.</td><td><strong>Always in Pattern 3 hardcoded datasources</strong> (and any time you publish all rows at once via <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Typical Pattern 3 pitfall</strong>: if you forget to force <code>md_server_side_operations: false</code>, the list-grid shows the 50/100 received rows, but <strong>clicking page 2, sorting a column, or typing in the filter does nothing</strong> — the grid sends the event to the &quot;framework backend&quot; which does not exist, and the UX appears stuck even without console errors.</p><p>&gt; <strong>Framework note</strong>: <code>DataSourceComponent.fetchData()</code> automatically detects the &quot;hardcoded datasource&quot; case (no <code>[hardcodedRoute]</code> set) and <strong>skips the backend call</strong> on every paging/sort/filter change, republishing the payload already in memory on <code>fetchInfo$</code>. This means that, once you populate <code>fetchInfo$.next(...)</code> the first time in your <code>ngAfterViewInit</code>, paging/sort/filter work client-side without any server roundtrip, even if the WUIC backend has not registered the route. See <code>data-source.component.ts</code> (short-circuit inside <code>fetchData()</code>).</p><h3>Client-side Mode (Recommended Default for Pattern 3)</h3><p>Snippet 4:</p><ul><li>You load <strong>all</strong> rows with a single <code>fetchInfo$.next</code>.</li><li>The list-grid applies filters/sort/paging/export on the already-present array.</li><li>Zero extra code.</li><li>Suited for small/medium datasets (a few thousand rows at most).</li></ul><h3>Server-side Mode (Manual Wiring)</h3><p>For large datasets (tens/hundreds of thousands of rows) you <strong>do not</strong> want to load everything in memory. Leave <code>md_server_side_operations: true</code> (default), subscribe to the <strong>@Outputs of `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) and re-call your REST endpoint on every state change. The list-grid updates <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>before</strong> emitting the event, so in the handler you just read the current state.</p><p>Snippet 5:</p><p>Complementary server endpoint (C# example, see <code>SamplesController.GetInventory</code>):</p><p>Snippet 6:</p><p><strong>Key points:</strong></p><ul><li>The <code>@Outputs</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> of <code>&lt;wuic-list-grid&gt;</code> expose the UI events <strong>after</strong> the list-grid handler has already updated the datasource state. Nothing to reimplement: read <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>The backend must return <code>{ rows, total }</code> where <code>total</code> is the <strong>POST-filter / PRE-page</strong> count. Without this, the UI pager doesn&#39;t know how many pages exist and does not work correctly.</li><li>The filter operator arrives in the <code>operatore</code> field of the filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, etc., see the matchMode table in <a href=\"./list-grid.md\">List Grid</a>). Map it consistently on the server side.</li></ul><h2>Variant: Consuming the Framework&#39;s OData Endpoint</h2><p>If the entity you want to display is already exposed by the framework as an OData entity set (<code>/odata/&lt;EntitySet&gt;</code>), you don&#39;t need to write ANY controller: just translate the list-grid UI state into a standard OData v4 query string.</p><p>&gt; <strong>100% framework-driven alternative (Pattern 1 with OData backend)</strong>: if you accept registering a standard metadata route for the entity, you can configure <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> and the datasource does <strong>everything by itself</strong> (filter/sort/paging/export) via the internal OData provider. No custom Angular code. See <a href=\"./odata.md\">OData</a> for the complete setup. Pattern 3 (this page) applies instead when you want explicit frontend control or you don&#39;t have metadata registered for the entity.</p><p>The framework exposes <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> which does all the WUIC operator -&gt; <code>$filter</code> OData mapping (contains/startswith/endswith/eq/ne/gt/ge/lt/le) with automatic quoting for string/numeric, support for nested filter groups (recursive AND/OR) and isnull/isnotnull. The return is a relative URL like <code>/odata/Cities?$filter=&lt;encoded expression&gt;</code>. You just need to prefix the base URL and add <code>$top / $skip / $orderby</code>.</p><p>Snippet 7:</p><p>&gt; <strong>Note on total/count</strong>: if the OData endpoint you use is configured to return the standard OData wrapper <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (via <code>$count=true</code>), you can read total directly from the response without the second query. The current WUIC framework endpoint returns a plain array and requires the parallel query.</p><h2>Live Examples in WuicTest</h2><p>The three examples cover the <strong>three main strategies</strong> of Pattern 3:</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; loads ALL 50 posts from an external endpoint (<code>jsonplaceholder.typicode.com/posts</code>) at once, paging/sort/filter applied in-memory by the list-grid (<code>md_server_side_operations: false</code>). Source folder: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Open demo</a>.</li><li><strong>Custom .NET grid (server-side, custom REST)</strong> &rarr; calls the <code>SamplesController.GetInventory</code> Controller with offset/limit/sort/filter as ad-hoc query params, reloads only the current page on every change (<code>md_server_side_operations: true</code> + explicit wiring on <code>(onPaging)/(onSorting)/(onFiltering)</code>). Source folder: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Open demo</a>.</li><li><strong>OData Cities grid (server-side, standard OData v4)</strong> &rarr; consumes the framework&#39;s generic OData endpoint (<code>GET /odata/Cities</code>) with standard query string <code>$top / $skip / $filter / $orderby</code>, no custom controller to write. Translates the list-grid UI events into OData queries (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, etc.). Source folder: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Open demo</a>.</li></ul><h3>When to Choose Which Variant</h3><table><thead><tr><th>Example</th><th>Strategy</th><th>Backend</th><th>When to use it</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Classic REST endpoint returning an array</td><td>Small-medium dataset (&lt; a few thousand rows), maximum simplicity, 3rd-party API with no server-side control</td></tr><tr><td><strong>3b</strong></td><td>Server-side custom REST</td><td>Your REST Controller with paging/sort/filter query params</td><td>Large dataset, you want full control over the query; the end-dev already has an existing endpoint with offset/limit/etc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Framework OData endpoint (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Large dataset automatically exposed by the framework as an OData set; zero backend code; standard syntax compatible with other clients</td></tr></tbody></table><h2>See Also</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full autogeneration</a>: if the standard UX is enough and data exists in the scaffolded model.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework data + Custom component</a>: the inverse (custom UI, framework data).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full custom</a>: if you don&#39;t even need the list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework component + Framework data (manual mount)</a>: the &quot;framework&quot; variant of this pattern: same manual widget composition, but metadata-driven data layer instead of custom backend.</li></ul>",
8255
+ "html": "<h1>Pattern: Framework component + Custom data</h1><p>The high-level UI components of the framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) accept a <strong>hand-built datasource</strong> (<code>hardcodedDatasource</code>) that does not go through the framework&#39;s data layer. Data comes from your custom backend (your own .NET Controller, external REST, static files, websocket, ...).</p><h2>When to Use It</h2><ul><li>You want the full UX of a list-grid/chart-list (filters, sort, client paging, export, edit dialog) but the data is produced by:</li></ul><p> - An external REST endpoint (3rd-party API, microservice).</p><p> - A .NET Controller of yours not integrated with the framework data layer.</p><p> - Static files, aggregate calculations, live data (websocket, polling).</p><ul><li>You have a legacy domain you don&#39;t want to model in the framework.</li><li>You are prototyping without yet having defined the data structure.</li></ul><h2>Architecture</h2><ul><li><strong>Developer</strong>: writes a small Angular component that fetches data from its backend and packages it in a local datasource.</li><li><strong>Framework</strong>: the list-grid behaves exactly as if the data came from the standard data layer (filters, sort, paging, export all work).</li><li><strong>Backend</strong>: total freedom. Classic REST endpoints, no metadata conventions.</li></ul><h2>What You Do (Frontend)</h2><p>You create a standalone Angular component that:</p><p>1. Calls your custom endpoint with <code>HttpClient</code>.</p><p>2. Defines the columns (name, label, type) for the local datasource.</p><p>3. Publishes rows + columns on the datasource and passes it to the list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>What You Do (Backend, Optional)</h2><p>If the data comes from your internal backend, a classic REST Controller is enough. No framework conventions, no metadata to write.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h3>If you open the connection yourself: on Oracle, move the session to your schema</h3><p>When your controller opens its own connection with the <code>DataSQLConnection</code> connection string</p><p>(ADO.NET, Dapper, EF Core with <code>UseOracle(...)</code>), <strong>Oracle</strong> needs one extra step the other</p><p>engines do not: that string carries no <code>Database</code> / <code>Initial Catalog</code>, because on Oracle the</p><p>schema is not part of the connection. <code>DataSQLConnection</code> only authenticates; the framework</p><p>moves the session to the data schema by itself, running</p><p><code>ALTER SESSION SET CURRENT_SCHEMA = &lt;schema&gt;</code> when it opens the connection.</p><p>A hand-opened connection does not get that: it stays on the schema of the user it authenticated</p><p>as, and the first query against your tables answers <code>ORA-00942: table or view does not exist</code></p><p>even though the table is there and the grants are in place. Two ways to do it right:</p><p>Snippet 4:</p><p>The other three engines do not need it: the database is in the connection string, and the</p><p>session starts inside it.</p><h2>Trade-offs</h2><table><thead><tr><th>Pros</th><th>Cons</th></tr></thead><tbody><tr><td><code>wuic-list-grid</code> UX with filters/sort/paging/export for free (client-side)</td><td>You maintain the consistency between rows and column definitions</td></tr><tr><td>Completely free backend</td><td>CRUD does not work out-of-the-box: edit/insert must be wired manually on your backend</td></tr><tr><td>Good for 3rd-party integrations</td><td>Server-side paging/sort/filter requires custom wiring (see below)</td></tr><tr><td>No scaffolding work</td><td>Type safety only via cast</td></tr></tbody></table><h2>Filters / Sort / Paging: Client-Side vs Server-Side</h2><p>The &quot;filters/sort/paging/export for free&quot; statement in the Trade-offs table applies <strong>only in client-side mode</strong>, and is subject to a metadata flag that must be explicitly set in the hardcoded datasource.</p><h3>The Key Flag: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (property of <code>MetadatiTabella</code>) controls <strong>where</strong> paging/sort/filter are executed:</p><table><thead><tr><th>Value</th><th>Meaning</th><th>When to use</th></tr></thead><tbody><tr><td><code>true</code> (default)</td><td>The list-grid sends paging/sort/filter events to the backend via the framework&#39;s standard CRUD endpoint. The backend returns only the requested page and applies sort/filter SQL-side.</td><td>Patterns 1 and 2 (with real metadata route and WUIC backend behind).</td></tr><tr><td><code>false</code></td><td>The list-grid executes paging/sort/filter in-memory on the already-loaded array. No server roundtrip.</td><td><strong>Always in Pattern 3 hardcoded datasources</strong> (and any time you publish all rows at once via <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Typical Pattern 3 pitfall</strong>: if you forget to force <code>md_server_side_operations: false</code>, the list-grid shows the 50/100 received rows, but <strong>clicking page 2, sorting a column, or typing in the filter does nothing</strong> — the grid sends the event to the &quot;framework backend&quot; which does not exist, and the UX appears stuck even without console errors.</p><p>&gt; <strong>Framework note</strong>: <code>DataSourceComponent.fetchData()</code> automatically detects the &quot;hardcoded datasource&quot; case (no <code>[hardcodedRoute]</code> set) and <strong>skips the backend call</strong> on every paging/sort/filter change, republishing the payload already in memory on <code>fetchInfo$</code>. This means that, once you populate <code>fetchInfo$.next(...)</code> the first time in your <code>ngAfterViewInit</code>, paging/sort/filter work client-side without any server roundtrip, even if the WUIC backend has not registered the route. See <code>data-source.component.ts</code> (short-circuit inside <code>fetchData()</code>).</p><h3>Client-side Mode (Recommended Default for Pattern 3)</h3><p>Snippet 5:</p><ul><li>You load <strong>all</strong> rows with a single <code>fetchInfo$.next</code>.</li><li>The list-grid applies filters/sort/paging/export on the already-present array.</li><li>Zero extra code.</li><li>Suited for small/medium datasets (a few thousand rows at most).</li></ul><h3>Server-side Mode (Manual Wiring)</h3><p>For large datasets (tens/hundreds of thousands of rows) you <strong>do not</strong> want to load everything in memory. Leave <code>md_server_side_operations: true</code> (default), subscribe to the <strong>@Outputs of `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) and re-call your REST endpoint on every state change. The list-grid updates <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>before</strong> emitting the event, so in the handler you just read the current state.</p><p>Snippet 6:</p><p>Complementary server endpoint (C# example, see <code>SamplesController.GetInventory</code>):</p><p>Snippet 7:</p><p><strong>Key points:</strong></p><ul><li>The <code>@Outputs</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> of <code>&lt;wuic-list-grid&gt;</code> expose the UI events <strong>after</strong> the list-grid handler has already updated the datasource state. Nothing to reimplement: read <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>The backend must return <code>{ rows, total }</code> where <code>total</code> is the <strong>POST-filter / PRE-page</strong> count. Without this, the UI pager doesn&#39;t know how many pages exist and does not work correctly.</li><li>The filter operator arrives in the <code>operatore</code> field of the filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, etc., see the matchMode table in <a href=\"./list-grid.md\">List Grid</a>). Map it consistently on the server side.</li></ul><h2>Variant: Consuming the Framework&#39;s OData Endpoint</h2><p>If the entity you want to display is already exposed by the framework as an OData entity set (<code>/odata/&lt;EntitySet&gt;</code>), you don&#39;t need to write ANY controller: just translate the list-grid UI state into a standard OData v4 query string.</p><p>&gt; <strong>100% framework-driven alternative (Pattern 1 with OData backend)</strong>: if you accept registering a standard metadata route for the entity, you can configure <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> and the datasource does <strong>everything by itself</strong> (filter/sort/paging/export) via the internal OData provider. No custom Angular code. See <a href=\"./odata.md\">OData</a> for the complete setup. Pattern 3 (this page) applies instead when you want explicit frontend control or you don&#39;t have metadata registered for the entity.</p><p>The framework exposes <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> which does all the WUIC operator -&gt; <code>$filter</code> OData mapping (contains/startswith/endswith/eq/ne/gt/ge/lt/le) with automatic quoting for string/numeric, support for nested filter groups (recursive AND/OR) and isnull/isnotnull. The return is a relative URL like <code>/odata/Cities?$filter=&lt;encoded expression&gt;</code>. You just need to prefix the base URL and add <code>$top / $skip / $orderby</code>.</p><p>Snippet 8:</p><p>&gt; <strong>Note on total/count</strong>: if the OData endpoint you use is configured to return the standard OData wrapper <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (via <code>$count=true</code>), you can read total directly from the response without the second query. The current WUIC framework endpoint returns a plain array and requires the parallel query.</p><h2>Live Examples in WuicTest</h2><p>The three examples cover the <strong>three main strategies</strong> of Pattern 3:</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; loads ALL 50 posts from an external endpoint (<code>jsonplaceholder.typicode.com/posts</code>) at once, paging/sort/filter applied in-memory by the list-grid (<code>md_server_side_operations: false</code>). Source folder: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Open demo</a>.</li><li><strong>Custom .NET grid (server-side, custom REST)</strong> &rarr; calls the <code>SamplesController.GetInventory</code> Controller with offset/limit/sort/filter as ad-hoc query params, reloads only the current page on every change (<code>md_server_side_operations: true</code> + explicit wiring on <code>(onPaging)/(onSorting)/(onFiltering)</code>). Source folder: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Open demo</a>.</li><li><strong>OData Cities grid (server-side, standard OData v4)</strong> &rarr; consumes the framework&#39;s generic OData endpoint (<code>GET /odata/Cities</code>) with standard query string <code>$top / $skip / $filter / $orderby</code>, no custom controller to write. Translates the list-grid UI events into OData queries (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, etc.). Source folder: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Open demo</a>.</li></ul><h3>When to Choose Which Variant</h3><table><thead><tr><th>Example</th><th>Strategy</th><th>Backend</th><th>When to use it</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Classic REST endpoint returning an array</td><td>Small-medium dataset (&lt; a few thousand rows), maximum simplicity, 3rd-party API with no server-side control</td></tr><tr><td><strong>3b</strong></td><td>Server-side custom REST</td><td>Your REST Controller with paging/sort/filter query params</td><td>Large dataset, you want full control over the query; the end-dev already has an existing endpoint with offset/limit/etc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Framework OData endpoint (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Large dataset automatically exposed by the framework as an OData set; zero backend code; standard syntax compatible with other clients</td></tr></tbody></table><h2>See Also</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full autogeneration</a>: if the standard UX is enough and data exists in the scaffolded model.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework data + Custom component</a>: the inverse (custom UI, framework data).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full custom</a>: if you don&#39;t even need the list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework component + Framework data (manual mount)</a>: the &quot;framework&quot; variant of this pattern: same manual widget composition, but metadata-driven data layer instead of custom backend.</li></ul>",
8250
8256
  "codeSamples": [
8251
8257
  {
8252
8258
  "id": "code_1",
@@ -8269,24 +8275,30 @@ const frameworkDocsContent = {
8269
8275
  {
8270
8276
  "id": "code_4",
8271
8277
  "title": "Snippet 4",
8272
- "language": "ts",
8273
- "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() to inherit defaults (md_sortable, etc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- key: force in-memory\r\ntableMeta.md_pageable = true; // enable UI pagination\r\ntableMeta.md_pagesize = 10; // rows per page\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
8278
+ "language": "csharp",
8279
+ "code": "await using var cn = new OracleConnection(cs);\r\nawait cn.OpenAsync();\r\n\r\n// 1) move the session to the data schema (what the framework does for you)\r\nawait using (var set = new OracleCommand(\"ALTER SESSION SET CURRENT_SCHEMA = MY_SCHEMA\", cn))\r\n await set.ExecuteNonQueryAsync();\r\n\r\n// 2) or qualify the tables: SELECT ... FROM MY_SCHEMA.PRODUCT"
8274
8280
  },
8275
8281
  {
8276
8282
  "id": "code_5",
8277
8283
  "title": "Snippet 5",
8278
8284
  "language": "ts",
8279
- "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Use the absolute backend URL or `WtoolboxService.appSettings.api_url`\r\n // if your frontend dev server is not co-located with the backend (typical\r\n // Angular `:4200` + ASP.NET `:5000` without proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations stays `true` (default) -> p-table in [lazy]=\"true\",\r\n // no internal sort/filter/paging: we reload the right page from the server.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
8285
+ "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() to inherit defaults (md_sortable, etc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- key: force in-memory\r\ntableMeta.md_pageable = true; // enable UI pagination\r\ntableMeta.md_pagesize = 10; // rows per page\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
8280
8286
  },
8281
8287
  {
8282
8288
  "id": "code_6",
8283
8289
  "title": "Snippet 6",
8284
- "language": "csharp",
8285
- "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter before count: total must be POST-filter / PRE-page,\r\n // otherwise the UI pager shows empty pages.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
8290
+ "language": "ts",
8291
+ "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Use the absolute backend URL or `WtoolboxService.appSettings.api_url`\r\n // if your frontend dev server is not co-located with the backend (typical\r\n // Angular `:4200` + ASP.NET `:5000` without proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations stays `true` (default) -> p-table in [lazy]=\"true\",\r\n // no internal sort/filter/paging: we reload the right page from the server.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
8286
8292
  },
8287
8293
  {
8288
8294
  "id": "code_7",
8289
8295
  "title": "Snippet 7",
8296
+ "language": "csharp",
8297
+ "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter before count: total must be POST-filter / PRE-page,\r\n // otherwise the UI pager shows empty pages.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
8298
+ },
8299
+ {
8300
+ "id": "code_8",
8301
+ "title": "Snippet 8",
8290
8302
  "language": "ts",
8291
8303
  "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient } from '@angular/common/http';\r\nimport { BehaviorSubject, forkJoin } from 'rxjs';\r\nimport {\r\n DataProviderOdataService,\r\n DataSourceComponent, ListGridComponent,\r\n FilterInfo, MetaInfo, MetadatiColonna, MetadatiTabella,\r\n WtoolboxService\r\n} from 'wuic-framework-lib';\r\n\r\n@Component({ /* ... */ })\r\nexport class ODataCitiesGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private odataSrv = inject(DataProviderOdataService);\r\n // ... meta, fd init (like 3b) ...\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = (this.ds?.filterInfo || new FilterInfo('AND', [])) as FilterInfo;\r\n const base = String(WtoolboxService.appSettings.file_path || '').replace(/\\/$/, '');\r\n\r\n // 1) URL with $filter already translated by the framework:\r\n // e.g. '/odata/Cities?$filter=contains(cityName%2C%20%27Mil%27)'\r\n const baseRelUrl = this.odataSrv.filterInfoToOdata(filter, 'Cities');\r\n const sep = baseRelUrl.includes('?') ? '&' : '?';\r\n\r\n // 2) Add $top/$skip/$orderby as extra query params\r\n const pageExtras = [ `$top=${pageSize}`, `$skip=${(page - 1) * pageSize}` ];\r\n if (sort?.field) {\r\n pageExtras.push(`$orderby=${encodeURIComponent(`${sort.field} ${sort.dir || 'asc'}`)}`);\r\n }\r\n const pageUrl = `${base}${baseRelUrl}${sep}${pageExtras.join('&')}`;\r\n\r\n // 3) Count query: same filter, $select=cityID for minimal payload\r\n const countUrl = `${base}${baseRelUrl}${sep}$select=cityID`;\r\n\r\n forkJoin({\r\n page: this.http.get<any[]>(pageUrl),\r\n all: this.http.get<any[]>(countUrl)\r\n }).subscribe(({ page, all }) => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: page, totalRowCount: all.length, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n}"
8292
8304
  }
@@ -14609,7 +14621,7 @@ const frameworkDocsContent = {
14609
14621
  {
14610
14622
  "id": "overview",
14611
14623
  "title": "Overview",
14612
- "html": "<h1>Panorama du Framework</h1><p>WUIC Framework est une plateforme pilotee par metadata pour creer des interfaces metier de maniere rapide et coherente.</p><h2>Contenu</h2><ul><li>DataSource et DataRepeater pour orchestrer les donnees et le rendu.</li><li>Archetypes visuels (list, map, scheduler, chart, carousel).</li><li>Designer runtime pour les dashboards et les templates dynamiques.</li><li>Workflow designer/runner pour les processus operationnels.</li><li>RAG Chatbot pour interroger la codebase en langage naturel.</li><li>Multi-SGBD : SQL Server, MySQL, PostgreSQL et Oracle via des providers drop-in (<code>dbms</code> : <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Demarrage de l&#39;environnement dev</h2><p>La stack WUIC est composee de plusieurs services. Demarrez-les dans l&#39;ordre indique.</p><h3>Prerequis</h3><table><thead><tr><th>Element</th><th>Verification</th><th>Notes</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle pour IIS en production</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>La 22 est la version avec laquelle le paquet est teste ; avec npm 10.9.x l&#39;installation passe grace au <code>package-lock.json</code> inclus</td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>Le Windows PowerShell livre avec Windows suffit : <code>rename-project.ps1</code> et les scripts <code>llm-workspace/</code> fonctionnent aussi sur 5.1. PowerShell 7 est recommande, pas requis</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>S&#39;il manque, <strong>la ligne unique l&#39;installe elle-meme</strong> (SQL Server Express, ou le moteur choisi avec <code>-Dbms</code>) ; pas sur Windows Server, voir l&#39;encadre ci-dessous. 2019 suffit pour les packages sans <code>.bak</code> ; le tutoriel <code>.bak</code> est une sauvegarde SQL Server 2022 et exige <strong>2022 ou plus recent</strong></td></tr><tr><td>Linux (installeur <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 ou 24.04 LTS</strong> (24.04 recommandee) : ce sont les seules versions d&#39;Ubuntu supportees par Microsoft pour SQL Server, la <strong>26.04 n&#39;est pas supportee</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Meme recommandation avec MySQL, PostgreSQL et Oracle, car l&#39;installeur est teste sur ces versions</td></tr><tr><td>GPU NVIDIA (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Optionnel : accelere le RAG Chatbot (sans GPU il tourne sur CPU)</td></tr></tbody></table><p>&gt; <strong>Sur Windows Server la base de donnees doit etre installee avant.</strong> La ligne unique recupere</p><p>&gt; les composants manquants via <code>winget</code>, absent sur Windows Server : sans une instance deja</p><p>&gt; joignable, l&#39;installation s&#39;arrete avec `No SQL Server instance reachable and winget is</p><p>&gt; missing`. Installe donc le moteur avant de lancer la ligne - SQL Server doit etre **2019 ou</p><p>&gt; superieur<strong> (Express suffit ; </strong>2022 ou superieur** pour le tutoriel <code>.bak</code>), et une 2017</p><p>&gt; n&#39;est pas acceptee. Si l&#39;instance n&#39;est pas celle par defaut, passe-la avec</p><p>&gt; <code>-SqlServer &#39;localhost\\NOMINSTANCE&#39;</code>. Cela vaut pour tous les moteurs, pas seulement SQL Server.</p><h3>1. Telecharger le template de projet</h3><p>Sous Windows, une seule ligne PowerShell fait tout : elle telecharge le paquet, verifie le SDK .NET 10</p><p>et Node.js 22 (en installant ce qui manque), execute <code>dotnet restore</code> et <code>npm install</code>,</p><p>et installe l&#39;extension WUIC Assistant dans VS Code.</p><p>Snippet 1:</p><p>Options utiles sur la meme ligne : <code>-Dbms mysql</code> (ou <code>postgres</code>, <code>oracle</code>) pour un moteur</p><p>autre que SQL Server — l&#39;installeur l&#39;installe et le configure lui-meme — et <code>-WithTutorial</code></p><p>pour telecharger aussi la base de donnees d&#39;exemple.</p><p>Quand c&#39;est termine, le dernier ecran est deja votre liste de taches :</p><p>Snippet 2:</p><p>La chaine de connexion est prete a l&#39;emploi : au premier demarrage le wizard ne demande rien</p><p>d&#39;autre. Avec <code>-Dbms mysql</code> ou <code>-Dbms postgres</code> cette ligne change en consequence et porte en</p><p>elle l&#39;utilisateur et le mot de passe, parce que le mot de passe du superutilisateur est</p><p><strong>genere par l&#39;installeur</strong> ; ce meme mot de passe est enregistre dans <code>wuic-secrets.json</code>, a</p><p>cote de l&#39;installation, et c&#39;est le seul endroit ou il reste ecrit.</p><p>Le chemin manuel reste valable, sur n&#39;importe quel systeme :</p><p>Partez du paquet <code>WuicTest-src-*.zip</code> (lien dans la section <strong>Download</strong> du site).</p><p>Extrayez-le dans un dossier de travail, ex. <code>C:\\dev\\WuicTest</code> (les fichiers vont directement</p><p>a la racine, PAS dans un sous-dossier <code>src/</code>). Le paquet contient :</p><ul><li><code>WuicTest.csproj</code> → projet host avec <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → app Angular avec <code>package.json</code> qui reference <code>wuic-framework-lib</code> depuis le registre npm</li><li><code>appsettings.json</code> preconfigure en mode <code>firstRun = &quot;true&quot;</code> avec placeholders <code>__SET_*__</code></li><li><code>appsettings-samples/</code> → 6 templates prets (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>Pas besoin de cloner le repository du framework</strong> : <code>WuicCore</code> arrive comme paquet NuGet</p><p>&gt; depuis <code>nuget.org</code> et <code>wuic-framework-lib</code> comme paquet npm depuis le registre npm. Le source</p><p>&gt; du framework n&#39;est necessaire que si vous voulez le modifier — typiquement pas votre cas.</p><p>&gt; <strong>npm 10.9.x et les paquets anterieurs a la 1.7.1</strong> : ces paquets ne contiennent pas de</p><p>&gt; <code>package-lock.json</code>, et sans lock npm 10.9.x (la version installee par Node 22 LTS) s&#39;arrete avec</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> pendant la resolution des peers.</p><p>&gt; Telechargez le lock publie a cote de la release —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;nom-du-zip&gt;.package-lock.json</code> — placez-le dans</p><p>&gt; <code>wwwroot/</code> et relancez <code>npm install</code>. La ligne unique <code>-Src</code> le fait pour vous.</p><h3>2. Base de donnees</h3><p>Il faut une instance du moteur joignable. <strong>Avec la ligne unique, rien a faire</strong> : elle</p><p>installe et configure l&#39;instance, et la chaine de connexion a coller dans le wizard est la</p><p>derniere chose qu&#39;elle affiche. Sur le chemin manuel, l&#39;instance doit deja exister.</p><p>Connexions dans <code>appsettings.json</code> :</p><ul><li><code>MetaDataSQLConnection</code> — DB metadata (menu, tables, colonnes, board, utilisateurs)</li><li><code>DataSQLConnection</code> — DB donnees applicatives</li></ul><p>S&#39;il s&#39;agit de la premiere installation (<code>AppSettings.firstRun = &quot;true&quot;</code> dans le template),</p><p>le wizard de scaffolding au premier demarrage cree le schema metadata et peuple le DB donnees.</p><p>Voir la page <em>Scaffolding initial</em> pour le flux guide.</p><p>#### Ce que demande le wizard de premier demarrage</p><p>Au premier demarrage l&#39;application n&#39;affiche pas le formulaire de connexion mais la page</p><p><strong>Configuration initiale du projet</strong>. On la remplit une seule fois : a la fin <code>firstRun</code> passe</p><p>a <code>false</code> et la page ne revient plus.</p><ul><li><strong>Mode d&#39;installation</strong> — deux entrees : <strong>Base existante</strong>, qui enregistre une base a vous,</li></ul><p> et <strong>Tutorial WideWorldImporters</strong>, qui installe la base d&#39;exemple. La seconde n&#39;apparait que</p><p> si le paquet embarque le tutoriel (<code>tutorialAvailable</code>) : dans les paquets sans tutoriel la</p><p> liste deroulante n&#39;existe pas et le mode est &quot;Base existante&quot;.</p><ul><li><strong>SGBD</strong> — le moteur. En le changeant, la chaine de connexion ci-dessous est reecrite dans la</li></ul><p> forme du provider choisi.</p><ul><li><strong>DataSQLConnection</strong> — la chaine vers la base de donnees, et le champ dont depend tout le</li></ul><p> reste : tant que vous n&#39;avez pas appuye sur <strong>Tester la connexion et charger les bases</strong>, la</p><p> liste des bases reste desactivee. Le test valide les identifiants, liste les bases du serveur</p><p> et, sur SQL Server, corrige lui-meme les parametres de certificat</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) en vous le signalant. <code>Integrated Security</code> ne</p><p> vaut que sur SQL Server : les autres moteurs exigent un utilisateur et un mot de passe.</p><ul><li><strong>Base de donnees</strong> et <strong>Nom de la base de metadonnees</strong> — la premiere se choisit dans la</li></ul><p> liste chargee par le test (en mode tutoriel c&#39;est un champ libre, <code>WideWorldImporters</code> par</p><p> defaut) ; la seconde est le nom de la base de metadonnees a creer (<code>metadataDB</code> par defaut,</p><p> <code>MetadataCRM</code> en mode tutoriel). Si cette base existe deja, le wizard demande confirmation</p><p> avant de la recreer.</p><ul><li><strong>Generer automatiquement les metadonnees des tables</strong> (uniquement en &quot;Base existante&quot;) —</li></ul><p> remplit <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> a partir de toutes les tables de la base</p><p> choisie. C&#39;est ce qui allonge le plus le provisioning sur une base comptant beaucoup de</p><p> tables.</p><ul><li><strong>Utilisateur administrateur initial</strong> — nom d&#39;utilisateur (<code>admin</code> par defaut), mot de passe</li></ul><p> (4 caracteres minimum) et langue. Il n&#39;y a pas de mot de passe par defaut : celui saisi ici</p><p> est le seul qui ouvrira la premiere session. La langue choisie est appliquee a</p><p> l&#39;administrateur et a cette page elle-meme.</p><ul><li><strong>RAG Chatbot</strong> — uniquement la cle Anthropic, facultative. Rien a installer : voir l&#39;etape 5.</li></ul><p>Une fois <strong>Confirmer et creer la base de metadonnees</strong> presse, le provisioning se deroule seul,</p><p>avec une barre de progression et le nombre de lots traites. Duree mesuree sur les installations</p><p>de test : <strong>30 s - 2 min</strong> en mode tutoriel, <strong>1-4 min</strong> sur une base existante avec la</p><p>generation automatique activee. A la fin, la page laisse place au formulaire de connexion.</p><h3>3. Backend (.NET)</h3><p>Depuis la racine du dossier extrait (ex. <code>C:\\dev\\WuicTest</code>) :</p><p>Snippet 3:</p><p>Le backend expose les API sur <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>Dans un second shell, depuis le sous-dossier <code>wwwroot/</code> :</p><p>Snippet 4:</p><p>Le frontend est disponible sur <code>http://localhost:4200</code>.</p><p>Les identifiants sont ceux de l&#39;administrateur choisi dans le wizard de premier demarrage :</p><p>le nom d&#39;utilisateur est <code>admin</code> si vous ne l&#39;avez pas change, et le mot de passe est celui que</p><p>vous avez saisi (il n&#39;y a pas de mot de passe par defaut).</p><h3>5. RAG Chatbot (optionnel)</h3><p>Le RAG Chatbot (<code>Administration &gt; RAG Chat</code>) tourne <strong>dans le backend .NET</strong> :</p><p>le moteur de retrieval (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime</p><p>in-process) est charge par le backend quand <code>AppSettings.rag-use-dotnet-engine</code></p><p>vaut <code>&quot;true&quot;</code> (valeur par defaut des packages de release). **Pas de Python, pas</p><p>de venv, aucun serveur separe a demarrer.**</p><p>A la <strong>premiere</strong> ouverture du chatbot (ou a la fin du first-run) le backend</p><p>telecharge seul les modeles ONNX (<code>bge-m3</code> + reranker), le tokenizer et</p><p>l&#39;index (~4,5 Go) depuis <code>rag-engine-models-url</code> (par defaut</p><p><code>https://wuic-framework.com/rag-models</code>) dans <code>rag-engine/artifacts/</code>. Une</p><p>connexion internet n&#39;est necessaire qu&#39;une seule fois ; le telechargement</p><p>tourne en arriere-plan (1-5 min) et une notification in-app arrive au debut</p><p>et a la fin. Entre-temps le composant affiche l&#39;etat <strong>RAG offline</strong> et le</p><p>reste de l&#39;application fonctionne normalement.</p><p>Cles <code>AppSettings</code> pertinentes (toutes avec des valeurs par defaut pretes) :</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — active le moteur .NET (<code>false</code> = fallback Python, voir ci-dessous)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code> : avec GPU NVIDIA (CUDA 12.x + cuDNN 9) ~1 s/requete, sur CPU ~15-25 s/requete</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = sources du framework en signature seulement ; <code>internal</code> = index integral</li><li><code>rag-engine-models-url</code> — source du telechargement au premier demarrage</li><li><code>rag-engine-cuda-path</code> = (vide) — dossier des DLL CUDA/cuDNN si elles ne sont pas installees au niveau systeme</li></ul><p>Pour le chat avec LLM, renseignez <code>rag-llm-provider</code> + <code>rag-llm-api-key</code></p><p>(Claude via Anthropic, ou un modele local via Ollama avec <code>rag-llm-base-url</code>).</p><p>Sans provider, seul le mode retrieval (recherche de snippets) fonctionne.</p><p>Detail des cles sur la page <em>AppSettings</em>.</p><p>&gt; L&#39;ancien stack Python (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> sur</p><p>&gt; <code>127.0.0.1:8765</code>) n&#39;est utilisable qu&#39;en fallback avec</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> et n&#39;est pas inclus dans les packages de release.</p><h2>Recapitulatif des services</h2><table><thead><tr><th>Service</th><th>Port</th><th>Obligatoire</th><th>Commande rapide</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>oui</td><td>(service Windows)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>oui</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>oui</td><td><code>npm run serve:dev</code></td></tr><tr><td>Engine RAG .NET (ONNX)</td><td>in-process</td><td>non</td><td>charge par le backend au premier chat</td></tr></tbody></table><h2>Quick Start — premier composant</h2><p>1. Configurez l&#39;endpoint AsmxProxy.</p><p>2. Definissez la route metadata et les colonnes.</p><p>3. Montez <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>Les premieres listes affichent 20 enregistrements : c&#39;est le mode Trial</h2><p>Une installation neuve fonctionne <strong>sans licence</strong>, donc en mode Trial, et dans ce mode</p><p><strong>chaque requete est limitee a 20 enregistrements</strong>. Ce n&#39;est ni une anomalie ni une limite</p><p>de la base de donnees : une liste qui affiche &quot;20 sur 20&quot; sur une table de plusieurs milliers</p><p>de lignes tourne tout simplement sans licence.</p><p>Comment le confirmer en une seconde :</p><p>Snippet 6:</p><p><code>licenseValid: false</code> confirme le mode Trial, et <code>licenseReason</code> en donne la raison</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>Comment en sortir : on colle la licence depuis <strong>Administration &gt; Editeur AppSettings</strong>, onglet</p><p>Licensing, et elle prend effet immediatement - aucun redemarrage du backend n&#39;est necessaire.</p><p>Les details, y compris les limites que le mode Trial impose au-dela du nombre</p><p>d&#39;enregistrements, se trouvent sur la page <a href=\"licensing.md\">Licensing</a>.</p>",
14624
+ "html": "<h1>Panorama du Framework</h1><p>WUIC Framework est une plateforme pilotee par metadata pour creer des interfaces metier de maniere rapide et coherente.</p><h2>Contenu</h2><ul><li>DataSource et DataRepeater pour orchestrer les donnees et le rendu.</li><li>Archetypes visuels (list, map, scheduler, chart, carousel).</li><li>Designer runtime pour les dashboards et les templates dynamiques.</li><li>Workflow designer/runner pour les processus operationnels.</li><li>RAG Chatbot pour interroger la codebase en langage naturel.</li><li>Multi-SGBD : SQL Server, MySQL, PostgreSQL et Oracle via des providers drop-in (<code>dbms</code> : <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Demarrage de l&#39;environnement dev</h2><p>La stack WUIC est composee de plusieurs services. Demarrez-les dans l&#39;ordre indique.</p><h3>Prerequis</h3><table><thead><tr><th>Element</th><th>Verification</th><th>Notes</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle pour IIS en production</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>La 22 est la version avec laquelle le paquet est teste ; avec npm 10.9.x l&#39;installation passe grace au <code>package-lock.json</code> inclus</td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>Le Windows PowerShell livre avec Windows suffit : <code>rename-project.ps1</code> et les scripts <code>llm-workspace/</code> fonctionnent aussi sur 5.1. PowerShell 7 est recommande, pas requis</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>S&#39;il manque, <strong>la ligne unique l&#39;installe elle-meme</strong> (SQL Server Express, ou le moteur choisi avec <code>-Dbms</code>) ; pas sur Windows Server, voir l&#39;encadre ci-dessous. 2019 suffit pour les packages sans <code>.bak</code> ; le tutoriel <code>.bak</code> est une sauvegarde SQL Server 2022 et exige <strong>2022 ou plus recent</strong></td></tr><tr><td>Linux (installeur <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 ou 24.04 LTS</strong> (24.04 recommandee) : ce sont les seules versions d&#39;Ubuntu supportees par Microsoft pour SQL Server, la <strong>26.04 n&#39;est pas supportee</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Meme recommandation avec MySQL, PostgreSQL et Oracle, car l&#39;installeur est teste sur ces versions</td></tr><tr><td>GPU NVIDIA (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Optionnel : accelere le RAG Chatbot (sans GPU il tourne sur CPU)</td></tr></tbody></table><p>&gt; <strong>Sur Windows Server la base de donnees doit etre installee avant.</strong> La ligne unique recupere</p><p>&gt; les composants manquants via <code>winget</code>, absent sur Windows Server : sans une instance deja</p><p>&gt; joignable, l&#39;installation s&#39;arrete avec `No SQL Server instance reachable and winget is</p><p>&gt; missing`. Installe donc le moteur avant de lancer la ligne - SQL Server doit etre **2019 ou</p><p>&gt; superieur<strong> (Express suffit ; </strong>2022 ou superieur** pour le tutoriel <code>.bak</code>), et une 2017</p><p>&gt; n&#39;est pas acceptee. Si l&#39;instance n&#39;est pas celle par defaut, passe-la avec</p><p>&gt; <code>-SqlServer &#39;localhost\\NOMINSTANCE&#39;</code>. Cela vaut pour tous les moteurs, pas seulement SQL Server.</p><h3>1. Telecharger le template de projet</h3><p>Sous Windows, une seule ligne PowerShell fait tout : elle telecharge le paquet, verifie le SDK .NET 10</p><p>et Node.js 22 (en installant ce qui manque), execute <code>dotnet restore</code> et <code>npm install</code>,</p><p>et installe l&#39;extension WUIC Assistant dans VS Code.</p><p>Snippet 1:</p><p>Options utiles sur la meme ligne : <code>-Dbms mysql</code> (ou <code>postgres</code>, <code>oracle</code>) pour un moteur</p><p>autre que SQL Server — l&#39;installeur l&#39;installe et le configure lui-meme — et <code>-WithTutorial</code></p><p>pour telecharger aussi la base de donnees d&#39;exemple.</p><p>Quand c&#39;est termine, le dernier ecran est deja votre liste de taches : ouvrez le fichier de</p><p>workspace <code>WuicTest.code-workspace</code> dans VS Code et appuyez sur <strong>F5</strong>, qui demarre backend et</p><p>frontend ensemble via le lanceur <code>Fullstack: WuicTest + Chrome</code> (les lanceurs sont dans</p><p><code>.vscode/launch.json</code>, a cote de celui pour le backend seul). Si vous preferez le terminal, les</p><p>commandes equivalentes se trouvent plus bas.</p><p>Snippet 2:</p><p>La chaine de connexion est prete a l&#39;emploi : au premier demarrage le wizard ne demande rien</p><p>d&#39;autre. Avec <code>-Dbms mysql</code> ou <code>-Dbms postgres</code> cette ligne change en consequence et porte en</p><p>elle l&#39;utilisateur et le mot de passe, parce que le mot de passe du superutilisateur est</p><p><strong>genere par l&#39;installeur</strong> ; ce meme mot de passe est enregistre dans <code>wuic-secrets.json</code>, a</p><p>cote de l&#39;installation, et c&#39;est le seul endroit ou il reste ecrit.</p><p>Le chemin manuel reste valable, sur n&#39;importe quel systeme :</p><p>Partez du paquet <code>WuicTest-src-*.zip</code> (lien dans la section <strong>Download</strong> du site).</p><p>Extrayez-le dans un dossier de travail, ex. <code>C:\\dev\\WuicTest</code> (les fichiers vont directement</p><p>a la racine, PAS dans un sous-dossier <code>src/</code>). Le paquet contient :</p><ul><li><code>WuicTest.csproj</code> → projet host avec <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → app Angular avec <code>package.json</code> qui reference <code>wuic-framework-lib</code> depuis le registre npm</li><li><code>appsettings.json</code> preconfigure en mode <code>firstRun = &quot;true&quot;</code> avec placeholders <code>__SET_*__</code></li><li><code>appsettings-samples/</code> → 6 templates prets (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>Pas besoin de cloner le repository du framework</strong> : <code>WuicCore</code> arrive comme paquet NuGet</p><p>&gt; depuis <code>nuget.org</code> et <code>wuic-framework-lib</code> comme paquet npm depuis le registre npm. Le source</p><p>&gt; du framework n&#39;est necessaire que si vous voulez le modifier — typiquement pas votre cas.</p><p>&gt; <strong>npm 10.9.x et les paquets anterieurs a la 1.7.1</strong> : ces paquets ne contiennent pas de</p><p>&gt; <code>package-lock.json</code>, et sans lock npm 10.9.x (la version installee par Node 22 LTS) s&#39;arrete avec</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> pendant la resolution des peers.</p><p>&gt; Telechargez le lock publie a cote de la release —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;nom-du-zip&gt;.package-lock.json</code> — placez-le dans</p><p>&gt; <code>wwwroot/</code> et relancez <code>npm install</code>. La ligne unique <code>-Src</code> le fait pour vous.</p><h3>2. Base de donnees</h3><p>Il faut une instance du moteur joignable. <strong>Avec la ligne unique, rien a faire</strong> : elle</p><p>installe et configure l&#39;instance, et la chaine de connexion a coller dans le wizard est la</p><p>derniere chose qu&#39;elle affiche. Sur le chemin manuel, l&#39;instance doit deja exister.</p><p>Connexions dans <code>appsettings.json</code> :</p><ul><li><code>MetaDataSQLConnection</code> — DB metadata (menu, tables, colonnes, board, utilisateurs)</li><li><code>DataSQLConnection</code> — DB donnees applicatives</li></ul><p>S&#39;il s&#39;agit de la premiere installation (<code>AppSettings.firstRun = &quot;true&quot;</code> dans le template),</p><p>le wizard de scaffolding au premier demarrage cree le schema metadata et peuple le DB donnees.</p><p>Voir la page <em>Scaffolding initial</em> pour le flux guide.</p><p>#### Ce que demande le wizard de premier demarrage</p><p>Au premier demarrage l&#39;application n&#39;affiche pas le formulaire de connexion mais la page</p><p><strong>Configuration initiale du projet</strong>. On la remplit une seule fois : a la fin <code>firstRun</code> passe</p><p>a <code>false</code> et la page ne revient plus.</p><ul><li><strong>Mode d&#39;installation</strong> — deux entrees : <strong>Base existante</strong>, qui enregistre une base a vous,</li></ul><p> et <strong>Tutorial WideWorldImporters</strong>, qui installe la base d&#39;exemple. La seconde n&#39;apparait que</p><p> si le paquet embarque le tutoriel (<code>tutorialAvailable</code>) : dans les paquets sans tutoriel la</p><p> liste deroulante n&#39;existe pas et le mode est &quot;Base existante&quot;.</p><ul><li><strong>SGBD</strong> — le moteur. En le changeant, la chaine de connexion ci-dessous est reecrite dans la</li></ul><p> forme du provider choisi.</p><ul><li><strong>DataSQLConnection</strong> — la chaine vers la base de donnees, et le champ dont depend tout le</li></ul><p> reste : tant que vous n&#39;avez pas appuye sur <strong>Tester la connexion et charger les bases</strong>, la</p><p> liste des bases reste desactivee. Le test valide les identifiants, liste les bases du serveur</p><p> et, sur SQL Server, corrige lui-meme les parametres de certificat</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) en vous le signalant. <code>Integrated Security</code> ne</p><p> vaut que sur SQL Server : les autres moteurs exigent un utilisateur et un mot de passe.</p><ul><li><strong>Base de donnees</strong> et <strong>Nom de la base de metadonnees</strong> — la premiere se choisit dans la</li></ul><p> liste chargee par le test (en mode tutoriel c&#39;est un champ libre, <code>WideWorldImporters</code> par</p><p> defaut) ; la seconde est le nom de la base de metadonnees a creer (<code>metadataDB</code> par defaut,</p><p> <code>MetadataCRM</code> en mode tutoriel). Si cette base existe deja, le wizard demande confirmation</p><p> avant de la recreer.</p><ul><li><strong>Generer automatiquement les metadonnees des tables</strong> (uniquement en &quot;Base existante&quot;) —</li></ul><p> remplit <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> a partir de toutes les tables de la base</p><p> choisie. C&#39;est ce qui allonge le plus le provisioning sur une base comptant beaucoup de</p><p> tables.</p><ul><li><strong>Utilisateur administrateur initial</strong> — nom d&#39;utilisateur (<code>admin</code> par defaut), mot de passe</li></ul><p> (4 caracteres minimum) et langue. Il n&#39;y a pas de mot de passe par defaut : celui saisi ici</p><p> est le seul qui ouvrira la premiere session. La langue choisie est appliquee a</p><p> l&#39;administrateur et a cette page elle-meme.</p><ul><li><strong>RAG Chatbot</strong> — uniquement la cle Anthropic, facultative. Rien a installer : voir l&#39;etape 5.</li></ul><p>Une fois <strong>Confirmer et creer la base de metadonnees</strong> presse, le provisioning se deroule seul,</p><p>avec une barre de progression et le nombre de lots traites. Duree mesuree sur les installations</p><p>de test : <strong>30 s - 2 min</strong> en mode tutoriel, <strong>1-4 min</strong> sur une base existante avec la</p><p>generation automatique activee. A la fin, la page laisse place au formulaire de connexion.</p><h3>3. Backend (.NET)</h3><p>Depuis la racine du dossier extrait (ex. <code>C:\\dev\\WuicTest</code>) :</p><p>Snippet 3:</p><p>Le backend expose les API sur <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>Dans un second shell, depuis le sous-dossier <code>wwwroot/</code> :</p><p>Snippet 4:</p><p>Le frontend est disponible sur <code>http://localhost:4200</code>.</p><p>Les identifiants sont ceux de l&#39;administrateur choisi dans le wizard de premier demarrage :</p><p>le nom d&#39;utilisateur est <code>admin</code> si vous ne l&#39;avez pas change, et le mot de passe est celui que</p><p>vous avez saisi (il n&#39;y a pas de mot de passe par defaut).</p><h3>5. RAG Chatbot (optionnel)</h3><p>Le RAG Chatbot (<code>Administration &gt; RAG Chat</code>) tourne <strong>dans le backend .NET</strong> :</p><p>le moteur de retrieval (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime</p><p>in-process) est charge par le backend quand <code>AppSettings.rag-use-dotnet-engine</code></p><p>vaut <code>&quot;true&quot;</code> (valeur par defaut des packages de release). **Pas de Python, pas</p><p>de venv, aucun serveur separe a demarrer.**</p><p>A la <strong>premiere</strong> ouverture du chatbot (ou a la fin du first-run) le backend</p><p>telecharge seul les modeles ONNX (<code>bge-m3</code> + reranker), le tokenizer et</p><p>l&#39;index (~4,5 Go) depuis <code>rag-engine-models-url</code> (par defaut</p><p><code>https://wuic-framework.com/rag-models</code>) dans <code>rag-engine/artifacts/</code>. Une</p><p>connexion internet n&#39;est necessaire qu&#39;une seule fois ; le telechargement</p><p>tourne en arriere-plan (1-5 min) et une notification in-app arrive au debut</p><p>et a la fin. Entre-temps le composant affiche l&#39;etat <strong>RAG offline</strong> et le</p><p>reste de l&#39;application fonctionne normalement.</p><p>Cles <code>AppSettings</code> pertinentes (toutes avec des valeurs par defaut pretes) :</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — active le moteur .NET (<code>false</code> = fallback Python, voir ci-dessous)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code> : avec GPU NVIDIA (CUDA 12.x + cuDNN 9) ~1 s/requete, sur CPU ~15-25 s/requete</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = sources du framework en signature seulement ; <code>internal</code> = index integral</li><li><code>rag-engine-models-url</code> — source du telechargement au premier demarrage</li><li><code>rag-engine-cuda-path</code> = (vide) — dossier des DLL CUDA/cuDNN si elles ne sont pas installees au niveau systeme</li></ul><p>Pour le chat avec LLM, renseignez <code>rag-llm-provider</code> + <code>rag-llm-api-key</code></p><p>(Claude via Anthropic, ou un modele local via Ollama avec <code>rag-llm-base-url</code>).</p><p>Sans provider, seul le mode retrieval (recherche de snippets) fonctionne.</p><p>Detail des cles sur la page <em>AppSettings</em>.</p><p>&gt; L&#39;ancien stack Python (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> sur</p><p>&gt; <code>127.0.0.1:8765</code>) n&#39;est utilisable qu&#39;en fallback avec</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> et n&#39;est pas inclus dans les packages de release.</p><h2>Recapitulatif des services</h2><table><thead><tr><th>Service</th><th>Port</th><th>Obligatoire</th><th>Commande rapide</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>oui</td><td>(service Windows)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>oui</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>oui</td><td><code>npm run serve:dev</code></td></tr><tr><td>Engine RAG .NET (ONNX)</td><td>in-process</td><td>non</td><td>charge par le backend au premier chat</td></tr></tbody></table><h2>Quick Start — premier composant</h2><p>1. Configurez l&#39;endpoint AsmxProxy.</p><p>2. Definissez la route metadata et les colonnes.</p><p>3. Montez <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>Les premieres listes affichent 20 enregistrements : c&#39;est le mode Trial</h2><p>Une installation neuve fonctionne <strong>sans licence</strong>, donc en mode Trial, et dans ce mode</p><p><strong>chaque requete est limitee a 20 enregistrements</strong>. Ce n&#39;est ni une anomalie ni une limite</p><p>de la base de donnees : une liste qui affiche &quot;20 sur 20&quot; sur une table de plusieurs milliers</p><p>de lignes tourne tout simplement sans licence.</p><p>Comment le confirmer en une seconde :</p><p>Snippet 6:</p><p><code>licenseValid: false</code> confirme le mode Trial, et <code>licenseReason</code> en donne la raison</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>Comment en sortir : on colle la licence depuis <strong>Administration &gt; Editeur AppSettings</strong>, onglet</p><p>Licensing, et elle prend effet immediatement - aucun redemarrage du backend n&#39;est necessaire.</p><p>Les details, y compris les limites que le mode Trial impose au-dela du nombre</p><p>d&#39;enregistrements, se trouvent sur la page <a href=\"licensing.md\">Licensing</a>.</p>",
14613
14625
  "codeSamples": [
14614
14626
  {
14615
14627
  "id": "code_1",
@@ -14972,7 +14984,7 @@ const frameworkDocsContent = {
14972
14984
  {
14973
14985
  "id": "overview",
14974
14986
  "title": "Overview",
14975
- "html": "<h1>Pattern : Framework component + Custom data</h1><p>Les composants UI de haut niveau du framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) acceptent un <strong>datasource construit a la main</strong> (<code>hardcodedDatasource</code>) qui ne passe pas par le data layer du framework. Les donnees proviennent de votre backend custom (Controller .NET perso, REST externe, fichiers statiques, websocket, ...).</p><h2>Quand l&#39;utiliser</h2><ul><li>Vous voulez l&#39;UX complete d&#39;une list-grid/chart-list (filtres, tri, pagination client, export, edit dialog) mais les donnees sont produites par :</li></ul><p> - Un endpoint REST externe (API tierce, microservice).</p><p> - Un Controller .NET perso non integre avec le data layer du framework.</p><p> - Des fichiers statiques, des calculs agreges, des donnees live (websocket, polling).</p><ul><li>Vous avez un domaine legacy que vous ne voulez pas modeliser dans le framework.</li><li>Vous etes en prototypage sans avoir encore defini la structure de donnees.</li></ul><h2>Architecture</h2><ul><li><strong>Developpeur</strong> : ecrit un petit composant Angular qui recupere les donnees depuis son propre backend et les empaquete dans un datasource local.</li><li><strong>Framework</strong> : la list-grid se comporte exactement comme si les donnees venaient du data layer standard (filtres, tri, pagination, export fonctionnent).</li><li><strong>Backend</strong> : liberte totale. Endpoints REST classiques, aucune convention de metadata.</li></ul><h2>Ce que vous faites (frontend)</h2><p>Vous creez un composant Angular standalone qui :</p><p>1. Appelle votre endpoint custom avec <code>HttpClient</code>.</p><p>2. Definit les colonnes (nom, label, type) pour le datasource local.</p><p>3. Publie les lignes + colonnes sur le datasource et le passe a la list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Ce que vous faites (backend, optionnel)</h2><p>Si les donnees viennent de votre backend interne, un Controller REST classique suffit. Aucune convention du framework, aucune metadata a ecrire.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contra</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> avec filtres/tri/pagination/export gratuits (client-side)</td><td>Vous maintenez vous-meme la coherence entre les lignes et la definition des colonnes</td></tr><tr><td>Backend completement libre</td><td>CRUD ne fonctionne pas out-of-the-box : edit/insert a wirer manuellement sur votre backend</td></tr><tr><td>Bon pour les integrations tierces</td><td>Pagination/tri/filtre server-side requiert un wiring custom (voir ci-dessous)</td></tr><tr><td>Aucun travail de scaffolding</td><td>Type safety uniquement via cast</td></tr></tbody></table><h2>Filtres / tri / pagination : client-side vs server-side</h2><p>La phrase &quot;filtres/tri/pagination/export gratuits&quot; du tableau Trade-off vaut <strong>uniquement en mode client-side</strong>, et est subordonnee a un flag metadata qui doit etre defini explicitement dans le datasource hardcoded.</p><h3>Le flag cle : <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (propriete de <code>MetadatiTabella</code>) controle <strong>ou</strong> sont executees pagination/tri/filtre :</p><table><thead><tr><th>Valeur</th><th>Signification</th><th>Quand l&#39;utiliser</th></tr></thead><tbody><tr><td><code>true</code> (defaut)</td><td>La list-grid envoie les evenements pagination/tri/filtre au backend via l&#39;endpoint CRUD standard du framework. Le backend retourne uniquement la page demandee et applique tri/filtre SQL-side.</td><td>Pattern 1 et 2 (avec route metadata reelle et backend WUIC derriere).</td></tr><tr><td><code>false</code></td><td>La list-grid execute pagination/tri/filtre in-memory sur l&#39;array deja charge. Aucun roundtrip serveur.</td><td><strong>Toujours dans les datasources hardcoded du Pattern 3</strong> (et a chaque fois que vous publiez toutes les lignes d&#39;un coup via <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Piege typique du Pattern 3</strong> : si vous oubliez de forcer <code>md_server_side_operations: false</code>, la list-grid affiche les 50/100 lignes recues, mais <strong>cliquer sur la page 2, trier une colonne ou taper dans le filtre ne fait rien</strong> — la grid envoie l&#39;evenement au &quot;backend du framework&quot; qui n&#39;existe pas, et l&#39;UX semble bloquee sans pour autant generer d&#39;erreurs dans la console.</p><p>&gt; <strong>Note framework</strong> : <code>DataSourceComponent.fetchData()</code> detecte automatiquement le cas &quot;hardcoded datasource&quot; (pas de <code>[hardcodedRoute]</code> defini) et <strong>saute l&#39;appel au backend</strong> a chaque changement de pagination/tri/filtre, republiant le payload deja present en memoire sur <code>fetchInfo$</code>. Cela signifie qu&#39;une fois <code>fetchInfo$.next(...)</code> populated la premiere fois dans votre <code>ngAfterViewInit</code>, pagination/tri/filtre fonctionnent client-side sans aucun roundtrip serveur, meme si le backend WUIC n&#39;a pas enregistre la route. Voir <code>data-source.component.ts</code> (short-circuit dans <code>fetchData()</code>).</p><h3>Mode client-side (defaut recommande pour le Pattern 3)</h3><p>Snippet 4:</p><ul><li>Vous chargez <strong>toutes</strong> les lignes avec un seul <code>fetchInfo$.next</code>.</li><li>La list-grid applique filtres/tri/pagination/export sur l&#39;array deja present.</li><li>Zero code additionnel.</li><li>Indique pour des datasets petits/moyens (de l&#39;ordre de quelques milliers de lignes).</li></ul><h3>Mode server-side (wiring manuel)</h3><p>Pour les gros datasets (dizaines/centaines de milliers de lignes) vous <strong>ne voulez pas</strong> charger tout en memoire. Laissez <code>md_server_side_operations: true</code> (defaut), abonnez-vous aux <strong>@Output de `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) et rappelez votre endpoint REST a chaque changement d&#39;etat. La list-grid met a jour <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>avant</strong> d&#39;emettre l&#39;evenement, il suffit donc de lire l&#39;etat courant dans le handler.</p><p>Snippet 5:</p><p>Endpoint serveur complementaire (exemple C#, voir <code>SamplesController.GetInventory</code>) :</p><p>Snippet 6:</p><p><strong>Points cles :</strong></p><ul><li>Les <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> de <code>&lt;wuic-list-grid&gt;</code> exposent les evenements UI <strong>apres</strong> que le handler list-grid a deja mis a jour l&#39;etat du datasource. Rien a reimplementer : lisez <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>Le backend doit retourner <code>{ rows, total }</code> ou <code>total</code> est le count <strong>POST-filter / PRE-page</strong>. Sans cela, le pager UI ne sait pas combien de pages existent et ne fonctionne pas correctement.</li><li>L&#39;operateur de filtre arrive dans le champ <code>operatore</code> de la filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, etc., voir tableau matchMode dans <a href=\"./list-grid.md\">List Grid</a>). Mappez-le de maniere coherente cote serveur.</li></ul><h2>Variante : consommer l&#39;endpoint OData du framework</h2><p>Si l&#39;entite que vous voulez afficher est deja exposee par le framework comme entity set OData (<code>/odata/&lt;EntitySet&gt;</code>), il n&#39;est pas necessaire d&#39;ecrire AUCUN controleur : il suffit de traduire l&#39;etat UI de la list-grid en query string OData v4 standard.</p><p>&gt; <strong>Alternative 100% framework-driven (Pattern 1 avec backend OData)</strong> : si vous acceptez d&#39;enregistrer une route metadata standard pour l&#39;entite, vous pouvez configurer <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> et la datasource fait <strong>tout toute seule</strong> (filter/sort/paging/export) via le provider OData interne. Aucun code Angular custom. Voir <a href=\"./odata.md\">OData</a> pour le setup complet. Le Pattern 3 (cette page) s&#39;applique en revanche quand vous voulez un controle explicite cote frontend ou n&#39;avez pas de metadata enregistres pour l&#39;entite.</p><p>Le framework expose <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> qui fait tout le mapping operator WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) avec quoting automatique pour string/numeric, support des nested filter groups (AND/OR recursifs) et isnull/isnotnull. Le return est une URL relative type <code>/odata/Cities?$filter=&lt;expression encoded&gt;</code>. Il suffit de prefixer avec la base URL et d&#39;ajouter <code>$top / $skip / $orderby</code>.</p><p>Snippet 7:</p><p>&gt; <strong>Note sur total/count</strong> : si l&#39;endpoint OData que vous utilisez est configure pour retourner le wrapper OData standard <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (via <code>$count=true</code>), vous pouvez lire le total directement depuis la response sans la seconde query. L&#39;endpoint du framework WUIC retourne actuellement l&#39;array plat et requiert la query parallele.</p><h2>Exemples vivants dans WuicTest</h2><p>Les trois exemples couvrent les <strong>trois strategies principales</strong> du Pattern 3 :</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; charge TOUS les 50 posts d&#39;un endpoint externe (<code>jsonplaceholder.typicode.com/posts</code>) d&#39;un coup, pagination/tri/filtre appliques in-memory par la list-grid (<code>md_server_side_operations: false</code>). Dossier source : <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Ouvrir la demo</a>.</li><li><strong>Custom .NET grid (server-side, REST custom)</strong> &rarr; appelle le Controller <code>SamplesController.GetInventory</code> avec offset/limit/sort/filter comme query params ad hoc, recharge uniquement la page courante a chaque changement (<code>md_server_side_operations: true</code> + wiring explicite sur <code>(onPaging)/(onSorting)/(onFiltering)</code>). Dossier source : <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Ouvrir la demo</a>.</li><li><strong>OData Cities grid (server-side, OData v4 standard)</strong> &rarr; consomme l&#39;endpoint OData generique du framework (<code>GET /odata/Cities</code>) avec query string standard <code>$top / $skip / $filter / $orderby</code>, aucun controleur custom a ecrire. Traduit les evenements UI de la list-grid en queries OData (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, etc.). Dossier source : <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Ouvrir la demo</a>.</li></ul><h3>Quand choisir quelle variante</h3><table><thead><tr><th>Exemple</th><th>Strategie</th><th>Backend</th><th>Quand l&#39;utiliser</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Endpoint REST classique retournant un array</td><td>Dataset petit-moyen (&lt; quelques milliers de lignes), simplicite maximale, API tierce sans controle server-side</td></tr><tr><td><strong>3b</strong></td><td>Server-side REST custom</td><td>Controller REST perso avec query params de pagination/tri/filtre</td><td>Dataset grand, vous voulez le controle total sur la query ; l&#39;end-dev a deja un endpoint existant avec offset/limit/etc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Endpoint OData du framework (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Dataset grand expose automatiquement par le framework comme OData set ; zero code backend ; syntaxe standard compatible avec d&#39;autres clients</td></tr></tbody></table><h2>Voir aussi</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full autogeneration</a> : si l&#39;UX standard suffit et les donnees existent dans le modele scaffolde.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework data + Custom component</a> : l&#39;inverse (UI custom, donnees framework).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full custom</a> : si vous n&#39;avez meme pas besoin de la list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework component + Framework data (manual mount)</a> : variante &quot;framework&quot; de ce pattern : meme composition manuelle des widgets, mais data layer metadata-driven au lieu d&#39;un backend custom.</li></ul>",
14987
+ "html": "<h1>Pattern : Framework component + Custom data</h1><p>Les composants UI de haut niveau du framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) acceptent un <strong>datasource construit a la main</strong> (<code>hardcodedDatasource</code>) qui ne passe pas par le data layer du framework. Les donnees proviennent de votre backend custom (Controller .NET perso, REST externe, fichiers statiques, websocket, ...).</p><h2>Quand l&#39;utiliser</h2><ul><li>Vous voulez l&#39;UX complete d&#39;une list-grid/chart-list (filtres, tri, pagination client, export, edit dialog) mais les donnees sont produites par :</li></ul><p> - Un endpoint REST externe (API tierce, microservice).</p><p> - Un Controller .NET perso non integre avec le data layer du framework.</p><p> - Des fichiers statiques, des calculs agreges, des donnees live (websocket, polling).</p><ul><li>Vous avez un domaine legacy que vous ne voulez pas modeliser dans le framework.</li><li>Vous etes en prototypage sans avoir encore defini la structure de donnees.</li></ul><h2>Architecture</h2><ul><li><strong>Developpeur</strong> : ecrit un petit composant Angular qui recupere les donnees depuis son propre backend et les empaquete dans un datasource local.</li><li><strong>Framework</strong> : la list-grid se comporte exactement comme si les donnees venaient du data layer standard (filtres, tri, pagination, export fonctionnent).</li><li><strong>Backend</strong> : liberte totale. Endpoints REST classiques, aucune convention de metadata.</li></ul><h2>Ce que vous faites (frontend)</h2><p>Vous creez un composant Angular standalone qui :</p><p>1. Appelle votre endpoint custom avec <code>HttpClient</code>.</p><p>2. Definit les colonnes (nom, label, type) pour le datasource local.</p><p>3. Publie les lignes + colonnes sur le datasource et le passe a la list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Ce que vous faites (backend, optionnel)</h2><p>Si les donnees viennent de votre backend interne, un Controller REST classique suffit. Aucune convention du framework, aucune metadata a ecrire.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h3>Si vous ouvrez vous-meme la connexion : sur Oracle, amenez la session sur votre schema</h3><p>Quand votre controleur ouvre sa propre connexion avec la chaine <code>DataSQLConnection</code> (ADO.NET,</p><p>Dapper, EF Core avec <code>UseOracle(...)</code>), <strong>Oracle</strong> demande une etape de plus que les autres</p><p>moteurs : cette chaine ne contient aucun <code>Database</code> / <code>Initial Catalog</code>, parce que sur Oracle</p><p>le schema ne fait pas partie de la connexion. <code>DataSQLConnection</code> ne fait qu&#39;authentifier ; c&#39;est</p><p>le framework qui amene la session sur le schema des donnees, en executant</p><p><code>ALTER SESSION SET CURRENT_SCHEMA = &lt;schema&gt;</code> a l&#39;ouverture.</p><p>Une connexion ouverte a la main n&#39;a pas cette etape : elle reste sur le schema de l&#39;utilisateur</p><p>authentifie, et la premiere requete sur vos tables repond `ORA-00942: table or view does not</p><p>exist` alors meme que la table existe et que les droits sont la. Deux facons de bien faire :</p><p>Snippet 4:</p><p>Les trois autres moteurs n&#39;en ont pas besoin : la base est dans la chaine de connexion et la</p><p>session y demarre.</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contra</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> avec filtres/tri/pagination/export gratuits (client-side)</td><td>Vous maintenez vous-meme la coherence entre les lignes et la definition des colonnes</td></tr><tr><td>Backend completement libre</td><td>CRUD ne fonctionne pas out-of-the-box : edit/insert a wirer manuellement sur votre backend</td></tr><tr><td>Bon pour les integrations tierces</td><td>Pagination/tri/filtre server-side requiert un wiring custom (voir ci-dessous)</td></tr><tr><td>Aucun travail de scaffolding</td><td>Type safety uniquement via cast</td></tr></tbody></table><h2>Filtres / tri / pagination : client-side vs server-side</h2><p>La phrase &quot;filtres/tri/pagination/export gratuits&quot; du tableau Trade-off vaut <strong>uniquement en mode client-side</strong>, et est subordonnee a un flag metadata qui doit etre defini explicitement dans le datasource hardcoded.</p><h3>Le flag cle : <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (propriete de <code>MetadatiTabella</code>) controle <strong>ou</strong> sont executees pagination/tri/filtre :</p><table><thead><tr><th>Valeur</th><th>Signification</th><th>Quand l&#39;utiliser</th></tr></thead><tbody><tr><td><code>true</code> (defaut)</td><td>La list-grid envoie les evenements pagination/tri/filtre au backend via l&#39;endpoint CRUD standard du framework. Le backend retourne uniquement la page demandee et applique tri/filtre SQL-side.</td><td>Pattern 1 et 2 (avec route metadata reelle et backend WUIC derriere).</td></tr><tr><td><code>false</code></td><td>La list-grid execute pagination/tri/filtre in-memory sur l&#39;array deja charge. Aucun roundtrip serveur.</td><td><strong>Toujours dans les datasources hardcoded du Pattern 3</strong> (et a chaque fois que vous publiez toutes les lignes d&#39;un coup via <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Piege typique du Pattern 3</strong> : si vous oubliez de forcer <code>md_server_side_operations: false</code>, la list-grid affiche les 50/100 lignes recues, mais <strong>cliquer sur la page 2, trier une colonne ou taper dans le filtre ne fait rien</strong> — la grid envoie l&#39;evenement au &quot;backend du framework&quot; qui n&#39;existe pas, et l&#39;UX semble bloquee sans pour autant generer d&#39;erreurs dans la console.</p><p>&gt; <strong>Note framework</strong> : <code>DataSourceComponent.fetchData()</code> detecte automatiquement le cas &quot;hardcoded datasource&quot; (pas de <code>[hardcodedRoute]</code> defini) et <strong>saute l&#39;appel au backend</strong> a chaque changement de pagination/tri/filtre, republiant le payload deja present en memoire sur <code>fetchInfo$</code>. Cela signifie qu&#39;une fois <code>fetchInfo$.next(...)</code> populated la premiere fois dans votre <code>ngAfterViewInit</code>, pagination/tri/filtre fonctionnent client-side sans aucun roundtrip serveur, meme si le backend WUIC n&#39;a pas enregistre la route. Voir <code>data-source.component.ts</code> (short-circuit dans <code>fetchData()</code>).</p><h3>Mode client-side (defaut recommande pour le Pattern 3)</h3><p>Snippet 5:</p><ul><li>Vous chargez <strong>toutes</strong> les lignes avec un seul <code>fetchInfo$.next</code>.</li><li>La list-grid applique filtres/tri/pagination/export sur l&#39;array deja present.</li><li>Zero code additionnel.</li><li>Indique pour des datasets petits/moyens (de l&#39;ordre de quelques milliers de lignes).</li></ul><h3>Mode server-side (wiring manuel)</h3><p>Pour les gros datasets (dizaines/centaines de milliers de lignes) vous <strong>ne voulez pas</strong> charger tout en memoire. Laissez <code>md_server_side_operations: true</code> (defaut), abonnez-vous aux <strong>@Output de `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) et rappelez votre endpoint REST a chaque changement d&#39;etat. La list-grid met a jour <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>avant</strong> d&#39;emettre l&#39;evenement, il suffit donc de lire l&#39;etat courant dans le handler.</p><p>Snippet 6:</p><p>Endpoint serveur complementaire (exemple C#, voir <code>SamplesController.GetInventory</code>) :</p><p>Snippet 7:</p><p><strong>Points cles :</strong></p><ul><li>Les <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> de <code>&lt;wuic-list-grid&gt;</code> exposent les evenements UI <strong>apres</strong> que le handler list-grid a deja mis a jour l&#39;etat du datasource. Rien a reimplementer : lisez <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>Le backend doit retourner <code>{ rows, total }</code> ou <code>total</code> est le count <strong>POST-filter / PRE-page</strong>. Sans cela, le pager UI ne sait pas combien de pages existent et ne fonctionne pas correctement.</li><li>L&#39;operateur de filtre arrive dans le champ <code>operatore</code> de la filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, etc., voir tableau matchMode dans <a href=\"./list-grid.md\">List Grid</a>). Mappez-le de maniere coherente cote serveur.</li></ul><h2>Variante : consommer l&#39;endpoint OData du framework</h2><p>Si l&#39;entite que vous voulez afficher est deja exposee par le framework comme entity set OData (<code>/odata/&lt;EntitySet&gt;</code>), il n&#39;est pas necessaire d&#39;ecrire AUCUN controleur : il suffit de traduire l&#39;etat UI de la list-grid en query string OData v4 standard.</p><p>&gt; <strong>Alternative 100% framework-driven (Pattern 1 avec backend OData)</strong> : si vous acceptez d&#39;enregistrer une route metadata standard pour l&#39;entite, vous pouvez configurer <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> et la datasource fait <strong>tout toute seule</strong> (filter/sort/paging/export) via le provider OData interne. Aucun code Angular custom. Voir <a href=\"./odata.md\">OData</a> pour le setup complet. Le Pattern 3 (cette page) s&#39;applique en revanche quand vous voulez un controle explicite cote frontend ou n&#39;avez pas de metadata enregistres pour l&#39;entite.</p><p>Le framework expose <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> qui fait tout le mapping operator WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) avec quoting automatique pour string/numeric, support des nested filter groups (AND/OR recursifs) et isnull/isnotnull. Le return est une URL relative type <code>/odata/Cities?$filter=&lt;expression encoded&gt;</code>. Il suffit de prefixer avec la base URL et d&#39;ajouter <code>$top / $skip / $orderby</code>.</p><p>Snippet 8:</p><p>&gt; <strong>Note sur total/count</strong> : si l&#39;endpoint OData que vous utilisez est configure pour retourner le wrapper OData standard <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (via <code>$count=true</code>), vous pouvez lire le total directement depuis la response sans la seconde query. L&#39;endpoint du framework WUIC retourne actuellement l&#39;array plat et requiert la query parallele.</p><h2>Exemples vivants dans WuicTest</h2><p>Les trois exemples couvrent les <strong>trois strategies principales</strong> du Pattern 3 :</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; charge TOUS les 50 posts d&#39;un endpoint externe (<code>jsonplaceholder.typicode.com/posts</code>) d&#39;un coup, pagination/tri/filtre appliques in-memory par la list-grid (<code>md_server_side_operations: false</code>). Dossier source : <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Ouvrir la demo</a>.</li><li><strong>Custom .NET grid (server-side, REST custom)</strong> &rarr; appelle le Controller <code>SamplesController.GetInventory</code> avec offset/limit/sort/filter comme query params ad hoc, recharge uniquement la page courante a chaque changement (<code>md_server_side_operations: true</code> + wiring explicite sur <code>(onPaging)/(onSorting)/(onFiltering)</code>). Dossier source : <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Ouvrir la demo</a>.</li><li><strong>OData Cities grid (server-side, OData v4 standard)</strong> &rarr; consomme l&#39;endpoint OData generique du framework (<code>GET /odata/Cities</code>) avec query string standard <code>$top / $skip / $filter / $orderby</code>, aucun controleur custom a ecrire. Traduit les evenements UI de la list-grid en queries OData (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, etc.). Dossier source : <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Ouvrir la demo</a>.</li></ul><h3>Quand choisir quelle variante</h3><table><thead><tr><th>Exemple</th><th>Strategie</th><th>Backend</th><th>Quand l&#39;utiliser</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Endpoint REST classique retournant un array</td><td>Dataset petit-moyen (&lt; quelques milliers de lignes), simplicite maximale, API tierce sans controle server-side</td></tr><tr><td><strong>3b</strong></td><td>Server-side REST custom</td><td>Controller REST perso avec query params de pagination/tri/filtre</td><td>Dataset grand, vous voulez le controle total sur la query ; l&#39;end-dev a deja un endpoint existant avec offset/limit/etc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Endpoint OData du framework (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Dataset grand expose automatiquement par le framework comme OData set ; zero code backend ; syntaxe standard compatible avec d&#39;autres clients</td></tr></tbody></table><h2>Voir aussi</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full autogeneration</a> : si l&#39;UX standard suffit et les donnees existent dans le modele scaffolde.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework data + Custom component</a> : l&#39;inverse (UI custom, donnees framework).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full custom</a> : si vous n&#39;avez meme pas besoin de la list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework component + Framework data (manual mount)</a> : variante &quot;framework&quot; de ce pattern : meme composition manuelle des widgets, mais data layer metadata-driven au lieu d&#39;un backend custom.</li></ul>",
14976
14988
  "codeSamples": [
14977
14989
  {
14978
14990
  "id": "code_1",
@@ -14995,24 +15007,30 @@ const frameworkDocsContent = {
14995
15007
  {
14996
15008
  "id": "code_4",
14997
15009
  "title": "Snippet 4",
14998
- "language": "ts",
14999
- "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() pour heriter des defauts (md_sortable, etc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- cle : force in-memory\r\ntableMeta.md_pageable = true; // active la pagination UI\r\ntableMeta.md_pagesize = 10; // lignes par page\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
15010
+ "language": "csharp",
15011
+ "code": "await using var cn = new OracleConnection(cs);\nawait cn.OpenAsync();\n\n// 1) amener la session sur le schema des donnees (ce que fait le framework)\nawait using (var set = new OracleCommand(\"ALTER SESSION SET CURRENT_SCHEMA = MON_SCHEMA\", cn))\n await set.ExecuteNonQueryAsync();\n\n// 2) ou qualifier les tables : SELECT ... FROM MON_SCHEMA.PRODUCT"
15000
15012
  },
15001
15013
  {
15002
15014
  "id": "code_5",
15003
15015
  "title": "Snippet 5",
15004
15016
  "language": "ts",
15005
- "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Utilisez l'URL absolue du backend ou `WtoolboxService.appSettings.api_url`\r\n // si votre dev server frontend n'est pas co-localise avec le backend (cas typique\r\n // Angular `:4200` + ASP.NET `:5000` sans proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations reste `true` (defaut) -> p-table en [lazy]=\"true\",\r\n // pas de tri/filtre/pagination interne : on recharge la bonne page depuis le serveur.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
15017
+ "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() pour heriter des defauts (md_sortable, etc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- cle : force in-memory\r\ntableMeta.md_pageable = true; // active la pagination UI\r\ntableMeta.md_pagesize = 10; // lignes par page\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
15006
15018
  },
15007
15019
  {
15008
15020
  "id": "code_6",
15009
15021
  "title": "Snippet 6",
15010
- "language": "csharp",
15011
- "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter avant le count : total doit etre POST-filter / PRE-page,\r\n // sinon le pager UI affiche des pages vides.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
15022
+ "language": "ts",
15023
+ "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Utilisez l'URL absolue du backend ou `WtoolboxService.appSettings.api_url`\r\n // si votre dev server frontend n'est pas co-localise avec le backend (cas typique\r\n // Angular `:4200` + ASP.NET `:5000` sans proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations reste `true` (defaut) -> p-table en [lazy]=\"true\",\r\n // pas de tri/filtre/pagination interne : on recharge la bonne page depuis le serveur.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
15012
15024
  },
15013
15025
  {
15014
15026
  "id": "code_7",
15015
15027
  "title": "Snippet 7",
15028
+ "language": "csharp",
15029
+ "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter avant le count : total doit etre POST-filter / PRE-page,\r\n // sinon le pager UI affiche des pages vides.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
15030
+ },
15031
+ {
15032
+ "id": "code_8",
15033
+ "title": "Snippet 8",
15016
15034
  "language": "ts",
15017
15035
  "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient } from '@angular/common/http';\r\nimport { BehaviorSubject, forkJoin } from 'rxjs';\r\nimport {\r\n DataProviderOdataService,\r\n DataSourceComponent, ListGridComponent,\r\n FilterInfo, MetaInfo, MetadatiColonna, MetadatiTabella,\r\n WtoolboxService\r\n} from 'wuic-framework-lib';\r\n\r\n@Component({ /* ... */ })\r\nexport class ODataCitiesGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private odataSrv = inject(DataProviderOdataService);\r\n // ... init meta, fd (comme 3b) ...\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = (this.ds?.filterInfo || new FilterInfo('AND', [])) as FilterInfo;\r\n const base = String(WtoolboxService.appSettings.file_path || '').replace(/\\/$/, '');\r\n\r\n // 1) URL avec $filter deja traduit par le framework :\r\n // ex. '/odata/Cities?$filter=contains(cityName%2C%20%27Mil%27)'\r\n const baseRelUrl = this.odataSrv.filterInfoToOdata(filter, 'Cities');\r\n const sep = baseRelUrl.includes('?') ? '&' : '?';\r\n\r\n // 2) Ajoute $top/$skip/$orderby comme extra query params\r\n const pageExtras = [ `$top=${pageSize}`, `$skip=${(page - 1) * pageSize}` ];\r\n if (sort?.field) {\r\n pageExtras.push(`$orderby=${encodeURIComponent(`${sort.field} ${sort.dir || 'asc'}`)}`);\r\n }\r\n const pageUrl = `${base}${baseRelUrl}${sep}${pageExtras.join('&')}`;\r\n\r\n // 3) Count query : meme filter, $select=cityID pour payload minimal\r\n const countUrl = `${base}${baseRelUrl}${sep}$select=cityID`;\r\n\r\n forkJoin({\r\n page: this.http.get<any[]>(pageUrl),\r\n all: this.http.get<any[]>(countUrl)\r\n }).subscribe(({ page, all }) => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: page, totalRowCount: all.length, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n}"
15018
15036
  }
@@ -21335,7 +21353,7 @@ const frameworkDocsContent = {
21335
21353
  {
21336
21354
  "id": "overview",
21337
21355
  "title": "Overview",
21338
- "html": "<h1>Panorámica del Framework</h1><p>WUIC Framework es una plataforma metadata-driven para crear interfaces de negocio de forma rápida y coherente.</p><h2>Qué incluye</h2><ul><li>DataSource y DataRepeater para orquestar datos y renderizado.</li><li>Archetypes visuales (list, map, scheduler, chart, carousel).</li><li>Designer runtime para dashboards y templates dinámicos.</li><li>Workflow designer/runner para procesos operativos.</li><li>RAG Chatbot para consultar la codebase en lenguaje natural.</li><li>Multi-SGBD: SQL Server, MySQL, PostgreSQL y Oracle mediante providers drop-in (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Arranque del entorno dev</h2><p>El stack WUIC está compuesto por varios servicios. Arráncalos en el orden indicado.</p><h3>Prerrequisitos</h3><table><thead><tr><th>Qué</th><th>Verificación</th><th>Notas</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle para IIS en producción</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>La 22 es la version con la que se prueba el paquete; con npm 10.9.x la instalacion funciona gracias al <code>package-lock.json</code> incluido</td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>El Windows PowerShell preinstalado es suficiente: <code>rename-project.ps1</code> y los scripts <code>llm-workspace/</code> tambien funcionan en 5.1. PowerShell 7 es recomendable, no obligatorio</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>Si falta, <strong>la línea única lo instala por su cuenta</strong> (SQL Server Express, o el motor elegido con <code>-Dbms</code>); en Windows Server no, ver el recuadro de abajo. 2019 basta para los paquetes sin <code>.bak</code>; el tutorial <code>.bak</code> es una copia de SQL Server 2022 y requiere <strong>2022 o superior</strong></td></tr><tr><td>Linux (instalador <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 o 24.04 LTS</strong> (24.04 recomendada): son las unicas versiones de Ubuntu que Microsoft soporta para SQL Server, la <strong>26.04 no esta soportada</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Misma recomendacion con MySQL, PostgreSQL y Oracle, porque el instalador esta probado en esas versiones</td></tr><tr><td>GPU NVIDIA (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Opcional: acelera el RAG Chatbot (sin GPU funciona en CPU)</td></tr></tbody></table><p>&gt; <strong>En Windows Server la base de datos debe instalarse antes.</strong> La linea unica obtiene los</p><p>&gt; componentes que faltan mediante <code>winget</code>, que no esta presente en Windows Server: sin una</p><p>&gt; instancia ya accesible la instalacion se detiene con `No SQL Server instance reachable and</p><p>&gt; winget is missing`. Instala el motor antes de ejecutar la linea - SQL Server debe ser **2019 o</p><p>&gt; superior<strong> (Express es suficiente; </strong>2022 o superior** para el tutorial <code>.bak</code>), y una 2017 no</p><p>&gt; se acepta. Si la instancia no es la predeterminada, pasala con</p><p>&gt; <code>-SqlServer &#39;localhost\\NOMBREINSTANCIA&#39;</code>. Vale para todos los motores, no solo SQL Server.</p><h3>1. Descarga el template de proyecto</h3><p>En Windows una sola linea de PowerShell lo hace todo: descarga el paquete, comprueba el SDK de .NET 10</p><p>y Node.js 22 (instalando lo que falte), ejecuta <code>dotnet restore</code> y <code>npm install</code>,</p><p>e instala la extension WUIC Assistant en VS Code.</p><p>Snippet 1:</p><p>Opciones útiles en la misma línea: <code>-Dbms mysql</code> (o <code>postgres</code>, <code>oracle</code>) para un motor</p><p>distinto de SQL Server — el instalador lo instala y lo configura él mismo — y <code>-WithTutorial</code></p><p>para descargar también la base de datos de ejemplo.</p><p>Cuando termina, la última pantalla ya es tu lista de tareas:</p><p>Snippet 2:</p><p>La cadena de conexión ya está lista: en el primer arranque el wizard no pide nada más. Con</p><p><code>-Dbms mysql</code> o <code>-Dbms postgres</code> esa línea cambia en consecuencia y lleva dentro usuario y</p><p>contraseña, porque la contraseña del superusuario <strong>la genera el instalador</strong>; esa misma</p><p>contraseña se guarda en <code>wuic-secrets.json</code>, junto a la instalación, y es el único sitio donde</p><p>queda escrita.</p><p>La via manual sigue siendo valida, en cualquier sistema:</p><p>Parte del paquete <code>WuicTest-src-*.zip</code> (enlace en la sección <strong>Download</strong> del sitio).</p><p>Extráelo en una carpeta de trabajo, p. ej. <code>C:\\dev\\WuicTest</code> (los archivos van directamente</p><p>en la raíz, NO dentro de una subcarpeta <code>src/</code>). El paquete contiene:</p><ul><li><code>WuicTest.csproj</code> → proyecto host con <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → app Angular cuyo <code>package.json</code> referencia <code>wuic-framework-lib</code> desde el registry npm</li><li><code>appsettings.json</code> preconfigurado en modo <code>firstRun = &quot;true&quot;</code> con placeholders <code>__SET_*__</code></li><li><code>appsettings-samples/</code> → 6 templates listos (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>No hace falta clonar el repositorio del framework</strong>: <code>WuicCore</code> llega como paquete NuGet</p><p>&gt; desde <code>nuget.org</code> y <code>wuic-framework-lib</code> como paquete npm desde el registry npm. El código</p><p>&gt; fuente del framework solo se necesita si quieres modificarlo — típicamente no es tu caso.</p><p>&gt; <strong>npm 10.9.x y los paquetes anteriores a 1.7.1</strong>: esos paquetes no incluyen <code>package-lock.json</code>, y</p><p>&gt; sin lock npm 10.9.x (la version que instala Node 22 LTS) se detiene con</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> al resolver las dependencias peer.</p><p>&gt; Descarga el lock publicado junto a la release —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;nombre-del-zip&gt;.package-lock.json</code> — colocalo en</p><p>&gt; <code>wwwroot/</code> y ejecuta <code>npm install</code> otra vez. La linea unica <code>-Src</code> lo hace por ti.</p><h3>2. Database</h3><p>Hace falta una instancia del motor accesible. <strong>Con la línea única no hay nada que hacer</strong>:</p><p>la instala y la configura ella, y la cadena de conexión que hay que pegar en el wizard es lo</p><p>último que imprime. En la vía manual la instancia debe existir ya.</p><p>Conexiones en <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — DB metadata (menú, tablas, columnas, boards, usuarios)</li><li><code>DataSQLConnection</code> — DB datos de la aplicación</li></ul><p>Si es la primera instalación (<code>AppSettings.firstRun = &quot;true&quot;</code> en el template),</p><p>el wizard de scaffolding en el primer arranque crea el esquema metadata y puebla el DB datos.</p><p>Ver la página <em>Scaffolding inicial</em> para el flujo guiado.</p><p>#### Qué pide el asistente de primer arranque</p><p>En el primer arranque la aplicación no muestra el formulario de acceso sino la página</p><p><strong>Configuración inicial del proyecto</strong>. Se rellena una sola vez: al terminar <code>firstRun</code> pasa a</p><p><code>false</code> y la página no vuelve a aparecer.</p><ul><li><strong>Modo de instalación</strong> — dos entradas: <strong>Base de datos existente</strong>, que registra una base de</li></ul><p> datos tuya, y <strong>Tutorial WideWorldImporters</strong>, que instala la base de ejemplo. La segunda solo</p><p> aparece si el paquete trae el tutorial (<code>tutorialAvailable</code>): en los paquetes sin tutorial el</p><p> desplegable no se muestra y el modo es &quot;Base de datos existente&quot;.</p><ul><li><strong>SGBD</strong> — el motor. Al cambiarlo, la cadena de conexión de abajo se reescribe con la forma</li></ul><p> del proveedor elegido.</p><ul><li><strong>DataSQLConnection</strong> — la cadena hacia la base de datos, y el campo del que depende todo lo</li></ul><p> demás: hasta que no pulses <strong>Probar conexión y cargar bases de datos</strong> la lista de bases</p><p> permanece deshabilitada. La prueba valida las credenciales, enumera las bases del servidor y,</p><p> en SQL Server, corrige por su cuenta los parámetros de certificado</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) avisándote. <code>Integrated Security</code> solo vale en</p><p> SQL Server: los demás motores necesitan usuario y contraseña.</p><ul><li><strong>Base de datos</strong> y <strong>Nombre de la base de metadatos</strong> — la primera se elige de la lista que</li></ul><p> cargó la prueba (en modo tutorial es un campo libre, <code>WideWorldImporters</code> por defecto); la</p><p> segunda es el nombre de la base de metadatos a crear (<code>metadataDB</code> por defecto, <code>MetadataCRM</code></p><p> en modo tutorial). Si esa base ya existe, el asistente pide confirmación antes de recrearla.</p><ul><li><strong>Generar automáticamente los metadatos de las tablas</strong> (solo en &quot;Base de datos existente&quot;) —</li></ul><p> rellena <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> a partir de todas las tablas de la base</p><p> elegida. Es lo que más alarga el aprovisionamiento en bases con muchas tablas.</p><ul><li><strong>Usuario admin inicial</strong> — nombre de usuario (<code>admin</code> por defecto), contraseña (mínimo 4</li></ul><p> caracteres) e idioma. No hay contraseña predeterminada: la que escribas aquí es la única con</p><p> la que entrarás en el primer acceso. El idioma elegido se aplica al usuario admin y a esta</p><p> misma página.</p><ul><li><strong>RAG Chatbot</strong> — solo la clave de Anthropic, opcional. Nada que instalar: ver el paso 5.</li></ul><p>Al pulsar <strong>Confirmar y crear la base de metadatos</strong> el aprovisionamiento continúa solo, con una</p><p>barra de progreso y el número de lotes procesados. Cuánto dura, medido en las instalaciones de</p><p>prueba: <strong>30 s - 2 min</strong> en modo tutorial, <strong>1-4 min</strong> sobre una base existente con el scaffold</p><p>automático activado. Al terminar, la página deja paso al formulario de acceso.</p><h3>3. Backend (.NET)</h3><p>Desde la raíz de la carpeta extraída (p. ej. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>El backend expone las API en <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>En una segunda shell, desde la subcarpeta <code>wwwroot/</code>:</p><p>Snippet 4:</p><p>El frontend está disponible en <code>http://localhost:4200</code>.</p><p>Las credenciales son las del usuario admin elegido en el asistente de primer arranque: el</p><p>nombre de usuario es <code>admin</code> si no lo cambiaste, y la contraseña es la que escribiste (no hay</p><p>contraseña predeterminada).</p><h3>5. RAG Chatbot (opcional)</h3><p>El RAG Chatbot (<code>Administración &gt; RAG Chat</code>) se ejecuta **dentro del backend</p><p>.NET**: el motor de retrieval (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime</p><p>in-process) lo carga el backend cuando <code>AppSettings.rag-use-dotnet-engine</code> es</p><p><code>&quot;true&quot;</code> (valor por defecto en los paquetes de release). **Sin Python, sin</p><p>venv, ningún servidor separado que arrancar.**</p><p>La <strong>primera</strong> vez que se abre el chatbot (o al final del first-run) el</p><p>backend descarga por sí solo los modelos ONNX (<code>bge-m3</code> + reranker), el</p><p>tokenizer y el índice (~4,5 GB) desde <code>rag-engine-models-url</code> (por defecto</p><p><code>https://wuic-framework.com/rag-models</code>) en <code>rag-engine/artifacts/</code>. Solo hace</p><p>falta conexión a internet una vez; la descarga corre en segundo plano (1-5 min)</p><p>y llega una notificación in-app al inicio y al final. Mientras tanto el</p><p>componente muestra el estado <strong>RAG offline</strong> y el resto de la aplicación</p><p>funciona con normalidad.</p><p>Claves <code>AppSettings</code> relevantes (todas con valores por defecto listos):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — activa el motor .NET (<code>false</code> = fallback Python, ver abajo)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: con GPU NVIDIA (CUDA 12.x + cuDNN 9) ~1 s/consulta, en CPU ~15-25 s/consulta</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = fuentes del framework solo como firma; <code>internal</code> = índice completo</li><li><code>rag-engine-models-url</code> — origen de la descarga en el primer arranque</li><li><code>rag-engine-cuda-path</code> = (vacío) — carpeta de las DLL CUDA/cuDNN si no están instaladas a nivel de sistema</li></ul><p>Para el chat con LLM configura <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> (Claude</p><p>vía Anthropic, o un modelo local vía Ollama con <code>rag-llm-base-url</code>). Sin</p><p>provider solo funciona la modalidad retrieval (búsqueda de snippets). Detalle</p><p>de las claves en la página <em>AppSettings</em>.</p><p>&gt; El stack histórico Python (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> en</p><p>&gt; <code>127.0.0.1:8765</code>) solo puede usarse como fallback con</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> y no se incluye en los paquetes de release.</p><h2>Resumen de servicios</h2><table><thead><tr><th>Servicio</th><th>Puerto</th><th>Obligatorio</th><th>Comando rápido</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>sí</td><td>(servicio Windows)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>sí</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>sí</td><td><code>npm run serve:dev</code></td></tr><tr><td>Engine RAG .NET (ONNX)</td><td>in-process</td><td>no</td><td>cargado por el backend en el primer chat</td></tr></tbody></table><h2>Quick Start — primer componente</h2><p>1. Configura el endpoint AsmxProxy.</p><p>2. Define route metadata y columnas.</p><p>3. Monta <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>Las primeras listas muestran 20 registros: es el modo Trial</h2><p>Una instalacion recien hecha funciona <strong>sin licencia</strong>, es decir, en modo Trial, y en ese</p><p>modo <strong>cada consulta esta limitada a 20 registros</strong>. No es un error ni un limite de la base</p><p>de datos: una lista que indica &quot;20 de 20&quot; sobre una tabla con miles de filas simplemente</p><p>esta funcionando sin licencia.</p><p>Como confirmarlo en un segundo:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> confirma el modo Trial, y <code>licenseReason</code> dice por que</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>Como salir: se pega la licencia desde <strong>Administracion &gt; Editor AppSettings</strong>, pestana Licensing,</p><p>y tiene efecto de inmediato: no hace falta reiniciar el backend. Los detalles, incluidos los</p><p>limites que el modo Trial impone mas alla del numero de registros, estan en la pagina</p><p><a href=\"licensing.md\">Licensing</a>.</p>",
21356
+ "html": "<h1>Panorámica del Framework</h1><p>WUIC Framework es una plataforma metadata-driven para crear interfaces de negocio de forma rápida y coherente.</p><h2>Qué incluye</h2><ul><li>DataSource y DataRepeater para orquestar datos y renderizado.</li><li>Archetypes visuales (list, map, scheduler, chart, carousel).</li><li>Designer runtime para dashboards y templates dinámicos.</li><li>Workflow designer/runner para procesos operativos.</li><li>RAG Chatbot para consultar la codebase en lenguaje natural.</li><li>Multi-SGBD: SQL Server, MySQL, PostgreSQL y Oracle mediante providers drop-in (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Arranque del entorno dev</h2><p>El stack WUIC está compuesto por varios servicios. Arráncalos en el orden indicado.</p><h3>Prerrequisitos</h3><table><thead><tr><th>Qué</th><th>Verificación</th><th>Notas</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle para IIS en producción</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>La 22 es la version con la que se prueba el paquete; con npm 10.9.x la instalacion funciona gracias al <code>package-lock.json</code> incluido</td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>El Windows PowerShell preinstalado es suficiente: <code>rename-project.ps1</code> y los scripts <code>llm-workspace/</code> tambien funcionan en 5.1. PowerShell 7 es recomendable, no obligatorio</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>Si falta, <strong>la línea única lo instala por su cuenta</strong> (SQL Server Express, o el motor elegido con <code>-Dbms</code>); en Windows Server no, ver el recuadro de abajo. 2019 basta para los paquetes sin <code>.bak</code>; el tutorial <code>.bak</code> es una copia de SQL Server 2022 y requiere <strong>2022 o superior</strong></td></tr><tr><td>Linux (instalador <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 o 24.04 LTS</strong> (24.04 recomendada): son las unicas versiones de Ubuntu que Microsoft soporta para SQL Server, la <strong>26.04 no esta soportada</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Misma recomendacion con MySQL, PostgreSQL y Oracle, porque el instalador esta probado en esas versiones</td></tr><tr><td>GPU NVIDIA (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Opcional: acelera el RAG Chatbot (sin GPU funciona en CPU)</td></tr></tbody></table><p>&gt; <strong>En Windows Server la base de datos debe instalarse antes.</strong> La linea unica obtiene los</p><p>&gt; componentes que faltan mediante <code>winget</code>, que no esta presente en Windows Server: sin una</p><p>&gt; instancia ya accesible la instalacion se detiene con `No SQL Server instance reachable and</p><p>&gt; winget is missing`. Instala el motor antes de ejecutar la linea - SQL Server debe ser **2019 o</p><p>&gt; superior<strong> (Express es suficiente; </strong>2022 o superior** para el tutorial <code>.bak</code>), y una 2017 no</p><p>&gt; se acepta. Si la instancia no es la predeterminada, pasala con</p><p>&gt; <code>-SqlServer &#39;localhost\\NOMBREINSTANCIA&#39;</code>. Vale para todos los motores, no solo SQL Server.</p><h3>1. Descarga el template de proyecto</h3><p>En Windows una sola linea de PowerShell lo hace todo: descarga el paquete, comprueba el SDK de .NET 10</p><p>y Node.js 22 (instalando lo que falte), ejecuta <code>dotnet restore</code> y <code>npm install</code>,</p><p>e instala la extension WUIC Assistant en VS Code.</p><p>Snippet 1:</p><p>Opciones útiles en la misma línea: <code>-Dbms mysql</code> (o <code>postgres</code>, <code>oracle</code>) para un motor</p><p>distinto de SQL Server — el instalador lo instala y lo configura él mismo — y <code>-WithTutorial</code></p><p>para descargar también la base de datos de ejemplo.</p><p>Cuando termina, la última pantalla ya es tu lista de tareas: abre el archivo de workspace</p><p><code>WuicTest.code-workspace</code> en VS Code y pulsa <strong>F5</strong>, que arranca backend y frontend juntos con</p><p>el lanzador <code>Fullstack: WuicTest + Chrome</code> (los lanzadores están en <code>.vscode/launch.json</code>,</p><p>junto al que arranca solo el backend). Si prefieres el terminal, los comandos equivalentes</p><p>están más abajo.</p><p>Snippet 2:</p><p>La cadena de conexión ya está lista: en el primer arranque el wizard no pide nada más. Con</p><p><code>-Dbms mysql</code> o <code>-Dbms postgres</code> esa línea cambia en consecuencia y lleva dentro usuario y</p><p>contraseña, porque la contraseña del superusuario <strong>la genera el instalador</strong>; esa misma</p><p>contraseña se guarda en <code>wuic-secrets.json</code>, junto a la instalación, y es el único sitio donde</p><p>queda escrita.</p><p>La via manual sigue siendo valida, en cualquier sistema:</p><p>Parte del paquete <code>WuicTest-src-*.zip</code> (enlace en la sección <strong>Download</strong> del sitio).</p><p>Extráelo en una carpeta de trabajo, p. ej. <code>C:\\dev\\WuicTest</code> (los archivos van directamente</p><p>en la raíz, NO dentro de una subcarpeta <code>src/</code>). El paquete contiene:</p><ul><li><code>WuicTest.csproj</code> → proyecto host con <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → app Angular cuyo <code>package.json</code> referencia <code>wuic-framework-lib</code> desde el registry npm</li><li><code>appsettings.json</code> preconfigurado en modo <code>firstRun = &quot;true&quot;</code> con placeholders <code>__SET_*__</code></li><li><code>appsettings-samples/</code> → 6 templates listos (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>No hace falta clonar el repositorio del framework</strong>: <code>WuicCore</code> llega como paquete NuGet</p><p>&gt; desde <code>nuget.org</code> y <code>wuic-framework-lib</code> como paquete npm desde el registry npm. El código</p><p>&gt; fuente del framework solo se necesita si quieres modificarlo — típicamente no es tu caso.</p><p>&gt; <strong>npm 10.9.x y los paquetes anteriores a 1.7.1</strong>: esos paquetes no incluyen <code>package-lock.json</code>, y</p><p>&gt; sin lock npm 10.9.x (la version que instala Node 22 LTS) se detiene con</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> al resolver las dependencias peer.</p><p>&gt; Descarga el lock publicado junto a la release —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;nombre-del-zip&gt;.package-lock.json</code> — colocalo en</p><p>&gt; <code>wwwroot/</code> y ejecuta <code>npm install</code> otra vez. La linea unica <code>-Src</code> lo hace por ti.</p><h3>2. Database</h3><p>Hace falta una instancia del motor accesible. <strong>Con la línea única no hay nada que hacer</strong>:</p><p>la instala y la configura ella, y la cadena de conexión que hay que pegar en el wizard es lo</p><p>último que imprime. En la vía manual la instancia debe existir ya.</p><p>Conexiones en <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — DB metadata (menú, tablas, columnas, boards, usuarios)</li><li><code>DataSQLConnection</code> — DB datos de la aplicación</li></ul><p>Si es la primera instalación (<code>AppSettings.firstRun = &quot;true&quot;</code> en el template),</p><p>el wizard de scaffolding en el primer arranque crea el esquema metadata y puebla el DB datos.</p><p>Ver la página <em>Scaffolding inicial</em> para el flujo guiado.</p><p>#### Qué pide el asistente de primer arranque</p><p>En el primer arranque la aplicación no muestra el formulario de acceso sino la página</p><p><strong>Configuración inicial del proyecto</strong>. Se rellena una sola vez: al terminar <code>firstRun</code> pasa a</p><p><code>false</code> y la página no vuelve a aparecer.</p><ul><li><strong>Modo de instalación</strong> — dos entradas: <strong>Base de datos existente</strong>, que registra una base de</li></ul><p> datos tuya, y <strong>Tutorial WideWorldImporters</strong>, que instala la base de ejemplo. La segunda solo</p><p> aparece si el paquete trae el tutorial (<code>tutorialAvailable</code>): en los paquetes sin tutorial el</p><p> desplegable no se muestra y el modo es &quot;Base de datos existente&quot;.</p><ul><li><strong>SGBD</strong> — el motor. Al cambiarlo, la cadena de conexión de abajo se reescribe con la forma</li></ul><p> del proveedor elegido.</p><ul><li><strong>DataSQLConnection</strong> — la cadena hacia la base de datos, y el campo del que depende todo lo</li></ul><p> demás: hasta que no pulses <strong>Probar conexión y cargar bases de datos</strong> la lista de bases</p><p> permanece deshabilitada. La prueba valida las credenciales, enumera las bases del servidor y,</p><p> en SQL Server, corrige por su cuenta los parámetros de certificado</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) avisándote. <code>Integrated Security</code> solo vale en</p><p> SQL Server: los demás motores necesitan usuario y contraseña.</p><ul><li><strong>Base de datos</strong> y <strong>Nombre de la base de metadatos</strong> — la primera se elige de la lista que</li></ul><p> cargó la prueba (en modo tutorial es un campo libre, <code>WideWorldImporters</code> por defecto); la</p><p> segunda es el nombre de la base de metadatos a crear (<code>metadataDB</code> por defecto, <code>MetadataCRM</code></p><p> en modo tutorial). Si esa base ya existe, el asistente pide confirmación antes de recrearla.</p><ul><li><strong>Generar automáticamente los metadatos de las tablas</strong> (solo en &quot;Base de datos existente&quot;) —</li></ul><p> rellena <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> a partir de todas las tablas de la base</p><p> elegida. Es lo que más alarga el aprovisionamiento en bases con muchas tablas.</p><ul><li><strong>Usuario admin inicial</strong> — nombre de usuario (<code>admin</code> por defecto), contraseña (mínimo 4</li></ul><p> caracteres) e idioma. No hay contraseña predeterminada: la que escribas aquí es la única con</p><p> la que entrarás en el primer acceso. El idioma elegido se aplica al usuario admin y a esta</p><p> misma página.</p><ul><li><strong>RAG Chatbot</strong> — solo la clave de Anthropic, opcional. Nada que instalar: ver el paso 5.</li></ul><p>Al pulsar <strong>Confirmar y crear la base de metadatos</strong> el aprovisionamiento continúa solo, con una</p><p>barra de progreso y el número de lotes procesados. Cuánto dura, medido en las instalaciones de</p><p>prueba: <strong>30 s - 2 min</strong> en modo tutorial, <strong>1-4 min</strong> sobre una base existente con el scaffold</p><p>automático activado. Al terminar, la página deja paso al formulario de acceso.</p><h3>3. Backend (.NET)</h3><p>Desde la raíz de la carpeta extraída (p. ej. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>El backend expone las API en <code>http://localhost:5000</code>.</p><h3>4. Frontend (Angular)</h3><p>En una segunda shell, desde la subcarpeta <code>wwwroot/</code>:</p><p>Snippet 4:</p><p>El frontend está disponible en <code>http://localhost:4200</code>.</p><p>Las credenciales son las del usuario admin elegido en el asistente de primer arranque: el</p><p>nombre de usuario es <code>admin</code> si no lo cambiaste, y la contraseña es la que escribiste (no hay</p><p>contraseña predeterminada).</p><h3>5. RAG Chatbot (opcional)</h3><p>El RAG Chatbot (<code>Administración &gt; RAG Chat</code>) se ejecuta **dentro del backend</p><p>.NET**: el motor de retrieval (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime</p><p>in-process) lo carga el backend cuando <code>AppSettings.rag-use-dotnet-engine</code> es</p><p><code>&quot;true&quot;</code> (valor por defecto en los paquetes de release). **Sin Python, sin</p><p>venv, ningún servidor separado que arrancar.**</p><p>La <strong>primera</strong> vez que se abre el chatbot (o al final del first-run) el</p><p>backend descarga por sí solo los modelos ONNX (<code>bge-m3</code> + reranker), el</p><p>tokenizer y el índice (~4,5 GB) desde <code>rag-engine-models-url</code> (por defecto</p><p><code>https://wuic-framework.com/rag-models</code>) en <code>rag-engine/artifacts/</code>. Solo hace</p><p>falta conexión a internet una vez; la descarga corre en segundo plano (1-5 min)</p><p>y llega una notificación in-app al inicio y al final. Mientras tanto el</p><p>componente muestra el estado <strong>RAG offline</strong> y el resto de la aplicación</p><p>funciona con normalidad.</p><p>Claves <code>AppSettings</code> relevantes (todas con valores por defecto listos):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — activa el motor .NET (<code>false</code> = fallback Python, ver abajo)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: con GPU NVIDIA (CUDA 12.x + cuDNN 9) ~1 s/consulta, en CPU ~15-25 s/consulta</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = fuentes del framework solo como firma; <code>internal</code> = índice completo</li><li><code>rag-engine-models-url</code> — origen de la descarga en el primer arranque</li><li><code>rag-engine-cuda-path</code> = (vacío) — carpeta de las DLL CUDA/cuDNN si no están instaladas a nivel de sistema</li></ul><p>Para el chat con LLM configura <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> (Claude</p><p>vía Anthropic, o un modelo local vía Ollama con <code>rag-llm-base-url</code>). Sin</p><p>provider solo funciona la modalidad retrieval (búsqueda de snippets). Detalle</p><p>de las claves en la página <em>AppSettings</em>.</p><p>&gt; El stack histórico Python (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> en</p><p>&gt; <code>127.0.0.1:8765</code>) solo puede usarse como fallback con</p><p>&gt; <code>rag-use-dotnet-engine = &quot;false&quot;</code> y no se incluye en los paquetes de release.</p><h2>Resumen de servicios</h2><table><thead><tr><th>Servicio</th><th>Puerto</th><th>Obligatorio</th><th>Comando rápido</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>sí</td><td>(servicio Windows)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>sí</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>sí</td><td><code>npm run serve:dev</code></td></tr><tr><td>Engine RAG .NET (ONNX)</td><td>in-process</td><td>no</td><td>cargado por el backend en el primer chat</td></tr></tbody></table><h2>Quick Start — primer componente</h2><p>1. Configura el endpoint AsmxProxy.</p><p>2. Define route metadata y columnas.</p><p>3. Monta <code>wuic-data-source</code> + <code>wuic-data-repeater</code>.</p><p>Snippet 5:</p><h2>Las primeras listas muestran 20 registros: es el modo Trial</h2><p>Una instalacion recien hecha funciona <strong>sin licencia</strong>, es decir, en modo Trial, y en ese</p><p>modo <strong>cada consulta esta limitada a 20 registros</strong>. No es un error ni un limite de la base</p><p>de datos: una lista que indica &quot;20 de 20&quot; sobre una tabla con miles de filas simplemente</p><p>esta funcionando sin licencia.</p><p>Como confirmarlo en un segundo:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> confirma el modo Trial, y <code>licenseReason</code> dice por que</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>Como salir: se pega la licencia desde <strong>Administracion &gt; Editor AppSettings</strong>, pestana Licensing,</p><p>y tiene efecto de inmediato: no hace falta reiniciar el backend. Los detalles, incluidos los</p><p>limites que el modo Trial impone mas alla del numero de registros, estan en la pagina</p><p><a href=\"licensing.md\">Licensing</a>.</p>",
21339
21357
  "codeSamples": [
21340
21358
  {
21341
21359
  "id": "code_1",
@@ -21698,7 +21716,7 @@ const frameworkDocsContent = {
21698
21716
  {
21699
21717
  "id": "overview",
21700
21718
  "title": "Overview",
21701
- "html": "<h1>Patrón: Framework component + Custom data</h1><p>Los componentes UI de alto nivel del framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) aceptan un <strong>datasource construido a mano</strong> (<code>hardcodedDatasource</code>) que no pasa por el data layer del framework. Los datos llegan desde tu backend custom (Controller .NET propio, REST externa, ficheros estáticos, websocket, ...).</p><h2>Cuándo usarlo</h2><ul><li>Quieres la UX completa de un list-grid/chart-list (filtros, sort, paging cliente, export, edit dialog) pero los datos son producidos por:</li></ul><p> - Un endpoint REST externo (3rd-party API, microservicio).</p><p> - Un Controller .NET propio no integrado con el data layer del framework.</p><p> - Ficheros estáticos, cálculos agregados, datos live (websocket, polling).</p><ul><li>Tienes un dominio legacy que no quieres modelar en el framework.</li><li>Estás prototipando sin haber definido aún la estructura de datos.</li></ul><h2>Arquitectura</h2><ul><li><strong>Desarrollador</strong>: escribe un pequeño componente Angular que recupera los datos desde su propio backend y los empaqueta en un datasource local.</li><li><strong>Framework</strong>: el list-grid se comporta exactamente como si los datos vinieran del data layer estándar (filtros, sort, paging, export funcionan).</li><li><strong>Backend</strong>: libertad total. Endpoints REST clásicos, sin convenciones de metadata.</li></ul><h2>Qué haces tú (frontend)</h2><p>Creas un componente Angular standalone que:</p><p>1. Llama a tu endpoint custom con <code>HttpClient</code>.</p><p>2. Define las columnas (nombre, label, tipo) para el datasource local.</p><p>3. Publica filas + columnas en el datasource y lo pasa al list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Qué haces tú (backend, opcional)</h2><p>Si los datos vienen de tu backend interno, basta un Controller REST clásico. Sin convenciones del framework, sin metadata que escribir.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contra</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> con filtros/sort/paging/export gratis (client-side)</td><td>Mantienes tú la coherencia entre las filas y la definición de columnas</td></tr><tr><td>Backend completamente libre</td><td>CRUD no funciona out-of-the-box: edit/insert hay que wirarlos a mano contra tu backend</td></tr><tr><td>Bueno para integraciones 3rd-party</td><td>Paging/sort/filter server-side requiere wiring custom (ver abajo)</td></tr><tr><td>Sin trabajo de scaffolding</td><td>Type safety solo vía cast</td></tr></tbody></table><h2>Filtros / sort / paging: client-side vs server-side</h2><p>La frase &quot;filtros/sort/paging/export gratis&quot; de la tabla Trade-off vale <strong>solo en modalidad client-side</strong>, y está subordinada a un flag metadata que debe configurarse explícitamente en el datasource hardcoded.</p><h3>El flag clave: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (propiedad de <code>MetadatiTabella</code>) controla <strong>dónde</strong> se ejecutan paging/sort/filter:</p><table><thead><tr><th>Valor</th><th>Significado</th><th>Cuándo usarlo</th></tr></thead><tbody><tr><td><code>true</code> (default)</td><td>El list-grid envía los eventos paging/sort/filter al backend mediante el endpoint CRUD estándar del framework. El backend devuelve solo la página solicitada y aplica sort/filter SQL-side.</td><td>Patrones 1 y 2 (con route metadata real y backend WUIC detrás).</td></tr><tr><td><code>false</code></td><td>El list-grid ejecuta paging/sort/filter in-memory sobre el array ya cargado. Sin roundtrip al servidor.</td><td><strong>Siempre en los datasource hardcoded del Patrón 3</strong> (y cada vez que publicas todas las filas de una sola vez vía <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Trampa típica del Patrón 3</strong>: si olvidas forzar <code>md_server_side_operations: false</code>, el list-grid muestra las 50/100 filas recibidas, pero <strong>clicar la página 2, ordenar una columna o escribir en el filtro no hace nada</strong> — el grid envía el evento al &quot;backend del framework&quot; que no existe, y la UX parece bloqueada aunque sin errores en consola.</p><p>&gt; <strong>Nota framework</strong>: <code>DataSourceComponent.fetchData()</code> detecta automáticamente el caso &quot;hardcoded datasource&quot; (sin <code>[hardcodedRoute]</code> configurado) y <strong>salta la llamada al backend</strong> en cada cambio de paging/sort/filter, republicando el payload ya presente en memoria en <code>fetchInfo$</code>. Significa que, una vez poblado <code>fetchInfo$.next(...)</code> la primera vez en tu <code>ngAfterViewInit</code>, paging/sort/filter funcionan client-side sin ningún roundtrip al servidor, aun cuando el backend WUIC no tenga registrada la route. Ver <code>data-source.component.ts</code> (short-circuit dentro de <code>fetchData()</code>).</p><h3>Modalidad client-side (default recomendado para Patrón 3)</h3><p>Snippet 4:</p><ul><li>Cargas <strong>todas</strong> las filas con un único <code>fetchInfo$.next</code>.</li><li>El list-grid aplica filtros/sort/paging/export sobre el array ya presente.</li><li>Cero código adicional.</li><li>Indicado para datasets pequeños/medianos (del orden de algunos miles de filas).</li></ul><h3>Modalidad server-side (wiring manual)</h3><p>Para datasets grandes (decenas/centenas de miles de filas) <strong>no quieres</strong> cargar todo en memoria. Dejas <code>md_server_side_operations: true</code> (default), te suscribes a los <strong>@Output de `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) y re-invocas tu endpoint REST en cada cambio de estado. El list-grid actualiza <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>antes</strong> de emitir el evento, por lo que en el handler basta con leer el estado actual.</p><p>Snippet 5:</p><p>Endpoint servidor complementario (ejemplo C#, ver <code>SamplesController.GetInventory</code>):</p><p>Snippet 6:</p><p><strong>Puntos clave:</strong></p><ul><li>Los <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> de <code>&lt;wuic-list-grid&gt;</code> exponen los eventos UI <strong>después</strong> de que el handler del list-grid ya haya actualizado el estado del datasource. Nada que reimplementar: lees <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>El backend debe devolver <code>{ rows, total }</code> donde <code>total</code> es el count <strong>POST-filter / PRE-page</strong>. Sin esto el pager UI no sabe cuántas páginas existen y no funciona correctamente.</li><li>El operador de filtro llega en el campo <code>operatore</code> de la filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, etc., ver tabla matchMode en <a href=\"./list-grid.md\">List Grid</a>). Mapéalo coherentemente del lado servidor.</li></ul><h2>Variante: consumir el endpoint OData del framework</h2><p>Si la entidad que quieres visualizar ya está expuesta por el framework como entity set OData (<code>/odata/&lt;EntitySet&gt;</code>), no necesitas escribir NINGÚN controller: basta traducir el estado UI del list-grid en query string OData v4 estándar.</p><p>&gt; <strong>Alternativa 100% framework-driven (Patrón 1 con backend OData)</strong>: si aceptas registrar una route metadata estándar para la entidad, puedes configurar <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> y el datasource hace <strong>todo solo</strong> (filter/sort/paging/export) mediante el provider OData interno. Sin código Angular custom. Ver <a href=\"./odata.md\">OData</a> para el setup completo. El Patrón 3 (esta página) se aplica en cambio cuando quieres control explícito del lado frontend o no tienes metadata registrados para la entidad.</p><p>El framework expone <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> que hace todo el mapping operator WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) con quoting automático para string/numeric, soporte de nested filter groups (AND/OR recursivos) e isnull/isnotnull. El return es una URL relativa tipo <code>/odata/Cities?$filter=&lt;expresión encoded&gt;</code>. Te basta prefijar la base URL y añadir <code>$top / $skip / $orderby</code>.</p><p>Snippet 7:</p><p>&gt; <strong>Nota sobre total/count</strong>: si el endpoint OData que usas está configurado para devolver el wrapper OData estándar <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (vía <code>$count=true</code>), puedes leer el total directamente desde la response sin la segunda query. El endpoint del framework WUIC actualmente devuelve el plain array y requiere la query paralela.</p><h2>Ejemplos vivos en WuicTest</h2><p>Los tres ejemplos cubren las <strong>tres estrategias principales</strong> del Patrón 3:</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; carga TODAS las 50 posts desde un endpoint externo (<code>jsonplaceholder.typicode.com/posts</code>) de una sola vez, paging/sort/filter aplicados in-memory por el list-grid (<code>md_server_side_operations: false</code>). Carpeta fuente: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Abrir demo</a>.</li><li><strong>Custom .NET grid (server-side, REST custom)</strong> &rarr; llama al Controller <code>SamplesController.GetInventory</code> con offset/limit/sort/filter como query params ad hoc, recarga solo la página actual en cada cambio (<code>md_server_side_operations: true</code> + wiring explícito en <code>(onPaging)/(onSorting)/(onFiltering)</code>). Carpeta fuente: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Abrir demo</a>.</li><li><strong>OData Cities grid (server-side, OData v4 estándar)</strong> &rarr; consume el endpoint OData genérico del framework (<code>GET /odata/Cities</code>) con query string estándar <code>$top / $skip / $filter / $orderby</code>, sin controller custom que escribir. Traduce los eventos UI del list-grid en query OData (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, etc.). Carpeta fuente: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Abrir demo</a>.</li></ul><h3>Cuándo elegir cada variante</h3><table><thead><tr><th>Ejemplo</th><th>Estrategia</th><th>Backend</th><th>Cuándo usarla</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Endpoint REST clásico que devuelve un array</td><td>Dataset pequeño-mediano (&lt; algunos miles de filas), simplicidad máxima, API 3rd-party sin control server-side</td></tr><tr><td><strong>3b</strong></td><td>Server-side REST custom</td><td>Controller REST propio con query params de paging/sort/filter</td><td>Dataset grande, quieres control total sobre la query; el end-dev ya tiene un endpoint existente con offset/limit/etc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Endpoint OData del framework (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Dataset grande expuesto automáticamente por el framework como OData set; cero código backend; sintaxis estándar compatible con otros clientes</td></tr></tbody></table><h2>Véase también</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Patrón 1 — Full autogeneration</a>: si la UX estándar basta y los datos existen en el modelo scaffoldeado.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Patrón 2 — Framework data + Custom component</a>: inverso (UI custom, datos del framework).</li><li><a href=\"./pattern-full-custom.md\">Patrón 4 — Full custom</a>: si ni siquiera necesitas el list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Patrón 5 — Framework component + Framework data (manual mount)</a>: variante &quot;framework&quot; de este patrón: misma composición manual de widgets, pero data layer metadata-driven en lugar de backend custom.</li></ul>",
21719
+ "html": "<h1>Patrón: Framework component + Custom data</h1><p>Los componentes UI de alto nivel del framework (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) aceptan un <strong>datasource construido a mano</strong> (<code>hardcodedDatasource</code>) que no pasa por el data layer del framework. Los datos llegan desde tu backend custom (Controller .NET propio, REST externa, ficheros estáticos, websocket, ...).</p><h2>Cuándo usarlo</h2><ul><li>Quieres la UX completa de un list-grid/chart-list (filtros, sort, paging cliente, export, edit dialog) pero los datos son producidos por:</li></ul><p> - Un endpoint REST externo (3rd-party API, microservicio).</p><p> - Un Controller .NET propio no integrado con el data layer del framework.</p><p> - Ficheros estáticos, cálculos agregados, datos live (websocket, polling).</p><ul><li>Tienes un dominio legacy que no quieres modelar en el framework.</li><li>Estás prototipando sin haber definido aún la estructura de datos.</li></ul><h2>Arquitectura</h2><ul><li><strong>Desarrollador</strong>: escribe un pequeño componente Angular que recupera los datos desde su propio backend y los empaqueta en un datasource local.</li><li><strong>Framework</strong>: el list-grid se comporta exactamente como si los datos vinieran del data layer estándar (filtros, sort, paging, export funcionan).</li><li><strong>Backend</strong>: libertad total. Endpoints REST clásicos, sin convenciones de metadata.</li></ul><h2>Qué haces tú (frontend)</h2><p>Creas un componente Angular standalone que:</p><p>1. Llama a tu endpoint custom con <code>HttpClient</code>.</p><p>2. Define las columnas (nombre, label, tipo) para el datasource local.</p><p>3. Publica filas + columnas en el datasource y lo pasa al list-grid.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Qué haces tú (backend, opcional)</h2><p>Si los datos vienen de tu backend interno, basta un Controller REST clásico. Sin convenciones del framework, sin metadata que escribir.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h3>Si abres tu la conexion: en Oracle lleva la sesion a tu esquema</h3><p>Cuando tu controlador abre su propia conexion con la cadena <code>DataSQLConnection</code> (ADO.NET,</p><p>Dapper, EF Core con <code>UseOracle(...)</code>), en <strong>Oracle</strong> hay un paso mas que los demas motores no</p><p>tienen: esa cadena no lleva <code>Database</code> / <code>Initial Catalog</code>, porque en Oracle el esquema no</p><p>forma parte de la conexion. <code>DataSQLConnection</code> solo autentica; al esquema de datos lleva la</p><p>sesion el framework, ejecutando <code>ALTER SESSION SET CURRENT_SCHEMA = &lt;esquema&gt;</code> al abrirla.</p><p>Una conexion abierta a mano no tiene ese paso: se queda en el esquema del usuario con el que se</p><p>autentico, y la primera consulta contra tus tablas responde `ORA-00942: table or view does not</p><p>exist` aunque la tabla exista y los permisos esten. Dos formas de hacerlo bien:</p><p>Snippet 4:</p><p>Los otros tres motores no lo necesitan: la base de datos esta en la cadena de conexion y la</p><p>sesion nace dentro.</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contra</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> con filtros/sort/paging/export gratis (client-side)</td><td>Mantienes tú la coherencia entre las filas y la definición de columnas</td></tr><tr><td>Backend completamente libre</td><td>CRUD no funciona out-of-the-box: edit/insert hay que wirarlos a mano contra tu backend</td></tr><tr><td>Bueno para integraciones 3rd-party</td><td>Paging/sort/filter server-side requiere wiring custom (ver abajo)</td></tr><tr><td>Sin trabajo de scaffolding</td><td>Type safety solo vía cast</td></tr></tbody></table><h2>Filtros / sort / paging: client-side vs server-side</h2><p>La frase &quot;filtros/sort/paging/export gratis&quot; de la tabla Trade-off vale <strong>solo en modalidad client-side</strong>, y está subordinada a un flag metadata que debe configurarse explícitamente en el datasource hardcoded.</p><h3>El flag clave: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (propiedad de <code>MetadatiTabella</code>) controla <strong>dónde</strong> se ejecutan paging/sort/filter:</p><table><thead><tr><th>Valor</th><th>Significado</th><th>Cuándo usarlo</th></tr></thead><tbody><tr><td><code>true</code> (default)</td><td>El list-grid envía los eventos paging/sort/filter al backend mediante el endpoint CRUD estándar del framework. El backend devuelve solo la página solicitada y aplica sort/filter SQL-side.</td><td>Patrones 1 y 2 (con route metadata real y backend WUIC detrás).</td></tr><tr><td><code>false</code></td><td>El list-grid ejecuta paging/sort/filter in-memory sobre el array ya cargado. Sin roundtrip al servidor.</td><td><strong>Siempre en los datasource hardcoded del Patrón 3</strong> (y cada vez que publicas todas las filas de una sola vez vía <code>fetchInfo$.next</code>).</td></tr></tbody></table><p><strong>Trampa típica del Patrón 3</strong>: si olvidas forzar <code>md_server_side_operations: false</code>, el list-grid muestra las 50/100 filas recibidas, pero <strong>clicar la página 2, ordenar una columna o escribir en el filtro no hace nada</strong> — el grid envía el evento al &quot;backend del framework&quot; que no existe, y la UX parece bloqueada aunque sin errores en consola.</p><p>&gt; <strong>Nota framework</strong>: <code>DataSourceComponent.fetchData()</code> detecta automáticamente el caso &quot;hardcoded datasource&quot; (sin <code>[hardcodedRoute]</code> configurado) y <strong>salta la llamada al backend</strong> en cada cambio de paging/sort/filter, republicando el payload ya presente en memoria en <code>fetchInfo$</code>. Significa que, una vez poblado <code>fetchInfo$.next(...)</code> la primera vez en tu <code>ngAfterViewInit</code>, paging/sort/filter funcionan client-side sin ningún roundtrip al servidor, aun cuando el backend WUIC no tenga registrada la route. Ver <code>data-source.component.ts</code> (short-circuit dentro de <code>fetchData()</code>).</p><h3>Modalidad client-side (default recomendado para Patrón 3)</h3><p>Snippet 5:</p><ul><li>Cargas <strong>todas</strong> las filas con un único <code>fetchInfo$.next</code>.</li><li>El list-grid aplica filtros/sort/paging/export sobre el array ya presente.</li><li>Cero código adicional.</li><li>Indicado para datasets pequeños/medianos (del orden de algunos miles de filas).</li></ul><h3>Modalidad server-side (wiring manual)</h3><p>Para datasets grandes (decenas/centenas de miles de filas) <strong>no quieres</strong> cargar todo en memoria. Dejas <code>md_server_side_operations: true</code> (default), te suscribes a los <strong>@Output de `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) y re-invocas tu endpoint REST en cada cambio de estado. El list-grid actualiza <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>antes</strong> de emitir el evento, por lo que en el handler basta con leer el estado actual.</p><p>Snippet 6:</p><p>Endpoint servidor complementario (ejemplo C#, ver <code>SamplesController.GetInventory</code>):</p><p>Snippet 7:</p><p><strong>Puntos clave:</strong></p><ul><li>Los <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> de <code>&lt;wuic-list-grid&gt;</code> exponen los eventos UI <strong>después</strong> de que el handler del list-grid ya haya actualizado el estado del datasource. Nada que reimplementar: lees <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>El backend debe devolver <code>{ rows, total }</code> donde <code>total</code> es el count <strong>POST-filter / PRE-page</strong>. Sin esto el pager UI no sabe cuántas páginas existen y no funciona correctamente.</li><li>El operador de filtro llega en el campo <code>operatore</code> de la filter entry (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code>, etc., ver tabla matchMode en <a href=\"./list-grid.md\">List Grid</a>). Mapéalo coherentemente del lado servidor.</li></ul><h2>Variante: consumir el endpoint OData del framework</h2><p>Si la entidad que quieres visualizar ya está expuesta por el framework como entity set OData (<code>/odata/&lt;EntitySet&gt;</code>), no necesitas escribir NINGÚN controller: basta traducir el estado UI del list-grid en query string OData v4 estándar.</p><p>&gt; <strong>Alternativa 100% framework-driven (Patrón 1 con backend OData)</strong>: si aceptas registrar una route metadata estándar para la entidad, puedes configurar <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> y el datasource hace <strong>todo solo</strong> (filter/sort/paging/export) mediante el provider OData interno. Sin código Angular custom. Ver <a href=\"./odata.md\">OData</a> para el setup completo. El Patrón 3 (esta página) se aplica en cambio cuando quieres control explícito del lado frontend o no tienes metadata registrados para la entidad.</p><p>El framework expone <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> que hace todo el mapping operator WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) con quoting automático para string/numeric, soporte de nested filter groups (AND/OR recursivos) e isnull/isnotnull. El return es una URL relativa tipo <code>/odata/Cities?$filter=&lt;expresión encoded&gt;</code>. Te basta prefijar la base URL y añadir <code>$top / $skip / $orderby</code>.</p><p>Snippet 8:</p><p>&gt; <strong>Nota sobre total/count</strong>: si el endpoint OData que usas está configurado para devolver el wrapper OData estándar <code>{ value: [...], &quot;@odata.count&quot;: N }</code> (vía <code>$count=true</code>), puedes leer el total directamente desde la response sin la segunda query. El endpoint del framework WUIC actualmente devuelve el plain array y requiere la query paralela.</p><h2>Ejemplos vivos en WuicTest</h2><p>Los tres ejemplos cubren las <strong>tres estrategias principales</strong> del Patrón 3:</p><ul><li><strong>External REST grid (client-side)</strong> &rarr; carga TODAS las 50 posts desde un endpoint externo (<code>jsonplaceholder.typicode.com/posts</code>) de una sola vez, paging/sort/filter aplicados in-memory por el list-grid (<code>md_server_side_operations: false</code>). Carpeta fuente: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Abrir demo</a>.</li><li><strong>Custom .NET grid (server-side, REST custom)</strong> &rarr; llama al Controller <code>SamplesController.GetInventory</code> con offset/limit/sort/filter como query params ad hoc, recarga solo la página actual en cada cambio (<code>md_server_side_operations: true</code> + wiring explícito en <code>(onPaging)/(onSorting)/(onFiltering)</code>). Carpeta fuente: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Abrir demo</a>.</li><li><strong>OData Cities grid (server-side, OData v4 estándar)</strong> &rarr; consume el endpoint OData genérico del framework (<code>GET /odata/Cities</code>) con query string estándar <code>$top / $skip / $filter / $orderby</code>, sin controller custom que escribir. Traduce los eventos UI del list-grid en query OData (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code>, etc.). Carpeta fuente: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Abrir demo</a>.</li></ul><h3>Cuándo elegir cada variante</h3><table><thead><tr><th>Ejemplo</th><th>Estrategia</th><th>Backend</th><th>Cuándo usarla</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-side</td><td>Endpoint REST clásico que devuelve un array</td><td>Dataset pequeño-mediano (&lt; algunos miles de filas), simplicidad máxima, API 3rd-party sin control server-side</td></tr><tr><td><strong>3b</strong></td><td>Server-side REST custom</td><td>Controller REST propio con query params de paging/sort/filter</td><td>Dataset grande, quieres control total sobre la query; el end-dev ya tiene un endpoint existente con offset/limit/etc.</td></tr><tr><td><strong>3c</strong></td><td>Server-side OData</td><td>Endpoint OData del framework (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Dataset grande expuesto automáticamente por el framework como OData set; cero código backend; sintaxis estándar compatible con otros clientes</td></tr></tbody></table><h2>Véase también</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Patrón 1 — Full autogeneration</a>: si la UX estándar basta y los datos existen en el modelo scaffoldeado.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Patrón 2 — Framework data + Custom component</a>: inverso (UI custom, datos del framework).</li><li><a href=\"./pattern-full-custom.md\">Patrón 4 — Full custom</a>: si ni siquiera necesitas el list-grid.</li><li><a href=\"./pattern-framework-manual.md\">Patrón 5 — Framework component + Framework data (manual mount)</a>: variante &quot;framework&quot; de este patrón: misma composición manual de widgets, pero data layer metadata-driven en lugar de backend custom.</li></ul>",
21702
21720
  "codeSamples": [
21703
21721
  {
21704
21722
  "id": "code_1",
@@ -21721,24 +21739,30 @@ const frameworkDocsContent = {
21721
21739
  {
21722
21740
  "id": "code_4",
21723
21741
  "title": "Snippet 4",
21724
- "language": "ts",
21725
- "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() para heredar los defaults (md_sortable, etc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- clave: fuerza in-memory\r\ntableMeta.md_pageable = true; // habilita paginación UI\r\ntableMeta.md_pagesize = 10; // filas por página\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
21742
+ "language": "csharp",
21743
+ "code": "await using var cn = new OracleConnection(cs);\nawait cn.OpenAsync();\n\n// 1) lleva la sesion al esquema de datos (lo que hace el framework)\nawait using (var set = new OracleCommand(\"ALTER SESSION SET CURRENT_SCHEMA = MI_ESQUEMA\", cn))\n await set.ExecuteNonQueryAsync();\n\n// 2) o califica las tablas: SELECT ... FROM MI_ESQUEMA.PRODUCT"
21726
21744
  },
21727
21745
  {
21728
21746
  "id": "code_5",
21729
21747
  "title": "Snippet 5",
21730
21748
  "language": "ts",
21731
- "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Usa la URL absoluta del backend o `WtoolboxService.appSettings.api_url`\r\n // si tu dev server frontend no está co-localizado con el backend (caso típico\r\n // Angular `:4200` + ASP.NET `:5000` sin proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations queda `true` (default) -> p-table en [lazy]=\"true\",\r\n // sin sort/filter/paging interno: recargamos la página correcta desde el servidor.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
21749
+ "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() para heredar los defaults (md_sortable, etc.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- clave: fuerza in-memory\r\ntableMeta.md_pageable = true; // habilita paginación UI\r\ntableMeta.md_pagesize = 10; // filas por página\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
21732
21750
  },
21733
21751
  {
21734
21752
  "id": "code_6",
21735
21753
  "title": "Snippet 6",
21736
- "language": "csharp",
21737
- "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter antes del count: total debe ser POST-filter / PRE-page,\r\n // de lo contrario el pager UI muestra páginas vacías.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
21754
+ "language": "ts",
21755
+ "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Usa la URL absoluta del backend o `WtoolboxService.appSettings.api_url`\r\n // si tu dev server frontend no está co-localizado con el backend (caso típico\r\n // Angular `:4200` + ASP.NET `:5000` sin proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations queda `true` (default) -> p-table en [lazy]=\"true\",\r\n // sin sort/filter/paging interno: recargamos la página correcta desde el servidor.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
21738
21756
  },
21739
21757
  {
21740
21758
  "id": "code_7",
21741
21759
  "title": "Snippet 7",
21760
+ "language": "csharp",
21761
+ "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) filter antes del count: total debe ser POST-filter / PRE-page,\r\n // de lo contrario el pager UI muestra páginas vacías.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
21762
+ },
21763
+ {
21764
+ "id": "code_8",
21765
+ "title": "Snippet 8",
21742
21766
  "language": "ts",
21743
21767
  "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient } from '@angular/common/http';\r\nimport { BehaviorSubject, forkJoin } from 'rxjs';\r\nimport {\r\n DataProviderOdataService,\r\n DataSourceComponent, ListGridComponent,\r\n FilterInfo, MetaInfo, MetadatiColonna, MetadatiTabella,\r\n WtoolboxService\r\n} from 'wuic-framework-lib';\r\n\r\n@Component({ /* ... */ })\r\nexport class ODataCitiesGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private odataSrv = inject(DataProviderOdataService);\r\n // ... meta, fd init (como 3b) ...\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = (this.ds?.filterInfo || new FilterInfo('AND', [])) as FilterInfo;\r\n const base = String(WtoolboxService.appSettings.file_path || '').replace(/\\/$/, '');\r\n\r\n // 1) URL con $filter ya traducido por el framework:\r\n // ej. '/odata/Cities?$filter=contains(cityName%2C%20%27Mil%27)'\r\n const baseRelUrl = this.odataSrv.filterInfoToOdata(filter, 'Cities');\r\n const sep = baseRelUrl.includes('?') ? '&' : '?';\r\n\r\n // 2) Agregar $top/$skip/$orderby como extra query params\r\n const pageExtras = [ `$top=${pageSize}`, `$skip=${(page - 1) * pageSize}` ];\r\n if (sort?.field) {\r\n pageExtras.push(`$orderby=${encodeURIComponent(`${sort.field} ${sort.dir || 'asc'}`)}`);\r\n }\r\n const pageUrl = `${base}${baseRelUrl}${sep}${pageExtras.join('&')}`;\r\n\r\n // 3) Count query: mismo filter, $select=cityID para payload mínimo\r\n const countUrl = `${base}${baseRelUrl}${sep}$select=cityID`;\r\n\r\n forkJoin({\r\n page: this.http.get<any[]>(pageUrl),\r\n all: this.http.get<any[]>(countUrl)\r\n }).subscribe(({ page, all }) => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: page, totalRowCount: all.length, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n}"
21744
21768
  }
@@ -28061,7 +28085,7 @@ const frameworkDocsContent = {
28061
28085
  {
28062
28086
  "id": "overview",
28063
28087
  "title": "Overview",
28064
- "html": "<h1>Framework-Uebersicht</h1><p>WUIC Framework ist eine metadata-gesteuerte Plattform zum schnellen und konsistenten Erstellen von Business-Oberflaechen.</p><h2>Was ist enthalten</h2><ul><li>DataSource und DataRepeater zur Orchestrierung von Daten und Rendering.</li><li>Visuelle Archetypes (list, map, scheduler, chart, carousel).</li><li>Runtime-Designer fuer Dashboards und dynamische Templates.</li><li>Workflow-Designer/Runner fuer operative Prozesse.</li><li>RAG Chatbot zum Abfragen der Codebasis in natuerlicher Sprache.</li><li>Multi-DBMS: SQL Server, MySQL, PostgreSQL und Oracle ueber Drop-in-Provider (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Dev-Umgebung starten</h2><p>Der WUIC-Stack besteht aus mehreren Diensten. Starte sie in der angegebenen Reihenfolge.</p><h3>Voraussetzungen</h3><table><thead><tr><th>Was</th><th>Verifikation</th><th>Hinweise</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle fuer IIS in Produktion</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>Mit 22 wird das Paket getestet; unter npm 10.9.x gelingt die Installation dank der mitgelieferten <code>package-lock.json</code></td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>Die vorinstallierte Windows PowerShell genuegt: <code>rename-project.ps1</code> und die <code>llm-workspace/</code>-Skripte laufen auch unter 5.1. PowerShell 7 wird empfohlen, ist nicht erforderlich</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>Fehlt er, <strong>installiert ihn der Einzeiler selbst</strong> (SQL Server Express, oder die mit <code>-Dbms</code> gewaehlte Engine); auf Windows Server nicht, siehe Kasten unten. 2019 reicht fuer die Pakete ohne <code>.bak</code>; das <code>.bak</code>-Tutorial ist ein SQL Server 2022-Backup und benoetigt <strong>2022 oder neuer</strong></td></tr><tr><td>Linux (Installer <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 oder 24.04 LTS</strong> (24.04 empfohlen): nur diese Ubuntu-Versionen werden von Microsoft fuer SQL Server unterstuetzt, die <strong>26.04 wird nicht unterstuetzt</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Gleiche Empfehlung mit MySQL, PostgreSQL und Oracle, da der Installer auf diesen Versionen getestet ist</td></tr><tr><td>NVIDIA-GPU (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Optional: beschleunigt den RAG Chatbot (ohne GPU laeuft er auf CPU)</td></tr></tbody></table><p>&gt; <strong>Auf Windows Server muss die Datenbank vorher installiert werden.</strong> Der Einzeiler beschafft</p><p>&gt; fehlende Komponenten ueber <code>winget</code>, das auf Windows Server nicht vorhanden ist: ohne eine</p><p>&gt; bereits erreichbare Instanz bricht die Installation mit `No SQL Server instance reachable and</p><p>&gt; winget is missing` ab. Installiere die Engine also vor dem Ausfuehren der Zeile - SQL Server</p><p>&gt; muss <strong>2019 oder neuer</strong> sein (Express genuegt; <strong>2022 oder neuer</strong> fuer das <code>.bak</code>-Tutorial),</p><p>&gt; eine 2017 wird nicht akzeptiert. Ist es nicht die Standardinstanz, uebergib sie mit</p><p>&gt; <code>-SqlServer &#39;localhost\\INSTANZNAME&#39;</code>. Das gilt fuer alle Engines, nicht nur SQL Server.</p><h3>1. Projekt-Template herunterladen</h3><p>Unter Windows erledigt eine PowerShell-Zeile alles: sie laedt das Paket, prueft .NET SDK 10</p><p>und Node.js 22 (und installiert, was fehlt), fuehrt <code>dotnet restore</code> und <code>npm install</code> aus</p><p>und installiert die Erweiterung WUIC Assistant in VS Code.</p><p>Snippet 1:</p><p>Nuetzliche Optionen auf derselben Zeile: <code>-Dbms mysql</code> (oder <code>postgres</code>, <code>oracle</code>) fuer eine</p><p>andere Engine als SQL Server — der Installer installiert und konfiguriert sie selbst — und</p><p><code>-WithTutorial</code>, um auch die Beispieldatenbank herunterzuladen.</p><p>Wenn er fertig ist, ist der letzte Bildschirm bereits die Aufgabenliste:</p><p>Snippet 2:</p><p>Die Verbindungszeichenfolge ist fertig: beim ersten Start fragt der Wizard nach nichts</p><p>weiterem. Mit <code>-Dbms mysql</code> oder <code>-Dbms postgres</code> aendert sich diese Zeile entsprechend und</p><p>traegt Benutzer und Passwort in sich, denn das Superuser-Passwort <strong>erzeugt der Installer</strong>;</p><p>dasselbe Passwort wird in <code>wuic-secrets.json</code> neben der Installation gespeichert, und das ist</p><p>der einzige Ort, an dem es geschrieben bleibt.</p><p>Der manuelle Weg bleibt gueltig, auf jedem System:</p><p>Starten Sie mit dem Paket <code>WuicTest-src-*.zip</code> (Link im <strong>Download</strong>-Bereich der Website).</p><p>Entpacken Sie es in einen Arbeitsordner, z.B. <code>C:\\dev\\WuicTest</code> (die Dateien kommen direkt</p><p>in den Root, NICHT in einen <code>src/</code>-Unterordner). Das Paket enthaelt:</p><ul><li><code>WuicTest.csproj</code> → Host-Projekt mit <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → Angular-App, deren <code>package.json</code> <code>wuic-framework-lib</code> aus der npm-Registry referenziert</li><li><code>appsettings.json</code> vorkonfiguriert im <code>firstRun = &quot;true&quot;</code>-Modus mit <code>__SET_*__</code>-Platzhaltern</li><li><code>appsettings-samples/</code> → 6 fertige Templates (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>Kein Klonen des Framework-Repositories noetig</strong>: <code>WuicCore</code> kommt als NuGet-Paket von</p><p>&gt; <code>nuget.org</code> und <code>wuic-framework-lib</code> als npm-Paket aus der npm-Registry. Der Framework-</p><p>&gt; Quellcode wird nur benoetigt, wenn Sie ihn modifizieren wollen — typischerweise nicht Ihr Fall.</p><p>&gt; <strong>npm 10.9.x und Pakete aelter als 1.7.1</strong>: diese Pakete enthalten keine <code>package-lock.json</code>, und</p><p>&gt; ohne Lock bricht npm 10.9.x (die Version, die Node 22 LTS mitbringt) mit</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> bei der Peer-Aufloesung ab.</p><p>&gt; Laden Sie den neben der Release veroeffentlichten Lock herunter —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;zip-name&gt;.package-lock.json</code> — legen Sie ihn in</p><p>&gt; <code>wwwroot/</code> und starten Sie <code>npm install</code> erneut. Der <code>-Src</code>-Einzeiler erledigt das selbst.</p><h3>2. Datenbank</h3><p>Es braucht eine erreichbare Instanz der Engine. <strong>Mit dem Einzeiler ist nichts zu tun</strong>: er</p><p>installiert und konfiguriert die Instanz, und die Verbindungszeichenfolge zum Einfuegen in den</p><p>Wizard ist das Letzte, was er ausgibt. Auf dem manuellen Weg muss die Instanz bereits</p><p>vorhanden sein.</p><p>Verbindungen in <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — Metadata-DB (Menue, Tabellen, Spalten, Board, Benutzer)</li><li><code>DataSQLConnection</code> — Anwendungsdaten-DB</li></ul><p>Bei Erstinstallation (<code>AppSettings.firstRun = &quot;true&quot;</code> im Template) erstellt der Scaffolding-</p><p>Wizard beim ersten Start das Metadata-Schema und befuellt die Daten-DB.</p><p>Siehe Seite <em>Initial Scaffolding</em> fuer den gefuehrten Flow.</p><p>#### Was der First-Run-Wizard abfragt</p><p>Beim ersten Start zeigt die Anwendung nicht das Login, sondern die Seite **Initiale</p><p>Projektkonfiguration**. Sie wird genau einmal ausgefuellt: danach steht <code>firstRun</code> auf <code>false</code></p><p>und die Seite erscheint nicht wieder.</p><ul><li><strong>Setup-Modus</strong> — zwei Eintraege: <strong>Bestehende Datenbank</strong>, die eine eigene Datenbank</li></ul><p> registriert, und <strong>Tutorial WideWorldImporters</strong>, das die Beispieldatenbank installiert. Der</p><p> zweite erscheint nur, wenn das Paket das Tutorial mitbringt (<code>tutorialAvailable</code>): in Paketen</p><p> ohne Tutorial gibt es die Auswahl gar nicht, und der Modus ist &quot;Bestehende Datenbank&quot;.</p><ul><li><strong>DBMS</strong> — die Engine. Beim Wechsel wird die Verbindungszeichenfolge darunter in der Form des</li></ul><p> gewaehlten Providers neu geschrieben.</p><ul><li><strong>DataSQLConnection</strong> — die Zeichenfolge zur Daten-DB und das Feld, von dem alles Weitere</li></ul><p> abhaengt: bis <strong>Verbindung testen und Datenbanken laden</strong> gedrueckt wurde, bleibt die</p><p> Datenbankliste deaktiviert. Der Test prueft die Zugangsdaten, listet die Datenbanken des</p><p> Servers und korrigiert auf SQL Server selbsttaetig die Zertifikatsparameter</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) und sagt es Ihnen. <code>Integrated Security</code> gilt</p><p> nur auf SQL Server: die anderen Engines brauchen Benutzer und Passwort.</p><ul><li><strong>Daten-Datenbank</strong> und <strong>Name der Metadaten-DB</strong> — die erste wird aus der vom Test geladenen</li></ul><p> Liste gewaehlt (im Tutorial-Modus ist es ein freies Textfeld, Standard <code>WideWorldImporters</code>);</p><p> die zweite ist der Name der anzulegenden Metadaten-DB (Standard <code>metadataDB</code>, im</p><p> Tutorial-Modus <code>MetadataCRM</code>). Existiert diese Datenbank bereits, fragt der Wizard vor dem</p><p> Neuanlegen nach.</p><ul><li><strong>Tabellen automatisch scaffolden</strong> (nur bei &quot;Bestehende Datenbank&quot;) — befuellt</li></ul><p> <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> aus allen Tabellen der gewaehlten Datenbank. Das</p><p> verlaengert das Provisioning bei Datenbanken mit vielen Tabellen am staerksten.</p><ul><li><strong>Initialer Admin-Benutzer</strong> — Benutzername (Standard <code>admin</code>), Passwort (mindestens 4</li></ul><p> Zeichen) und Sprache. Es gibt kein Standardpasswort: das hier eingegebene ist das einzige, mit</p><p> dem die erste Anmeldung gelingt. Die gewaehlte Sprache gilt fuer den Admin-Benutzer und fuer</p><p> diese Seite selbst.</p><ul><li><strong>RAG Chatbot</strong> — nur der Anthropic-Schluessel, optional. Nichts zu installieren: siehe</li></ul><p> Schritt 5.</p><p>Nach <strong>Bestaetigen und Metadaten-DB erzeugen</strong> laeuft das Provisioning selbstaendig weiter, mit</p><p>Fortschrittsbalken und der Zahl verarbeiteter Batches. Dauer, gemessen auf den</p><p>Test-Installationen: <strong>30 s - 2 min</strong> im Tutorial-Modus, <strong>1-4 min</strong> auf einer bestehenden</p><p>Datenbank mit aktiviertem automatischem Scaffolding. Am Ende weicht die Seite dem Login-Formular.</p><h3>3. Backend (.NET)</h3><p>Aus dem Root des entpackten Ordners (z.B. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>Das Backend stellt die APIs auf <code>http://localhost:5000</code> bereit.</p><h3>4. Frontend (Angular)</h3><p>In einer zweiten Shell, aus dem Unterordner <code>wwwroot/</code>:</p><p>Snippet 4:</p><p>Das Frontend ist auf <code>http://localhost:4200</code> verfuegbar.</p><p>Die Zugangsdaten sind die des im First-Run-Wizard gewaehlten Admin-Benutzers: der</p><p>Benutzername ist <code>admin</code>, sofern nicht geaendert, und das Passwort ist das selbst gesetzte (ein</p><p>Standardpasswort gibt es nicht).</p><h3>5. RAG Chatbot (optional)</h3><p>Der RAG Chatbot (<code>Amministrazione &gt; RAG Chat</code>) laeuft <strong>im .NET-Backend</strong>: die</p><p>Retrieval-Engine (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime in-process) wird</p><p>vom Backend geladen, wenn <code>AppSettings.rag-use-dotnet-engine</code> auf <code>&quot;true&quot;</code></p><p>steht (Default in den Release-Paketen). **Kein Python, keine venv, kein</p><p>separater Server, der gestartet werden muss.**</p><p>Beim <strong>ersten</strong> Oeffnen des Chatbots (oder am Ende des First-Run) laedt das</p><p>Backend selbststaendig die ONNX-Modelle (<code>bge-m3</code> + Reranker), den Tokenizer</p><p>und den Index (~4,5 GB) von <code>rag-engine-models-url</code> (Default</p><p><code>https://wuic-framework.com/rag-models</code>) nach <code>rag-engine/artifacts/</code>. Eine</p><p>Internetverbindung wird nur einmal benoetigt; der Download laeuft im</p><p>Hintergrund (1-5 Min.) und eine In-App-Benachrichtigung erscheint zu Beginn</p><p>und am Ende. Solange zeigt die Komponente den Status <strong>RAG offline</strong>, der Rest</p><p>der Anwendung funktioniert normal.</p><p>Relevante <code>AppSettings</code>-Schluessel (alle mit fertigen Defaults):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — aktiviert die .NET-Engine (<code>false</code> = Python-Fallback, siehe unten)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: mit NVIDIA-GPU (CUDA 12.x + cuDNN 9) ~1 s/Abfrage, auf CPU ~15-25 s/Abfrage</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = Framework-Quellen nur als Signatur; <code>internal</code> = vollstaendiger Index</li><li><code>rag-engine-models-url</code> — Download-Quelle beim ersten Start</li><li><code>rag-engine-cuda-path</code> = (leer) — Ordner der CUDA/cuDNN-DLLs, falls nicht systemweit installiert</li></ul><p>Fuer den Chat mit LLM <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> setzen (Claude</p><p>via Anthropic oder ein lokales Modell via Ollama mit <code>rag-llm-base-url</code>). Ohne</p><p>Provider funktioniert nur der Retrieval-Modus (Snippet-Suche). Details zu den</p><p>Schluesseln auf der Seite <em>AppSettings</em>.</p><p>&gt; Der historische Python-Stack (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> auf</p><p>&gt; <code>127.0.0.1:8765</code>) ist nur als Fallback mit <code>rag-use-dotnet-engine = &quot;false&quot;</code></p><p>&gt; nutzbar und ist in den Release-Paketen nicht enthalten.</p><h2>Dienste-Zusammenfassung</h2><table><thead><tr><th>Dienst</th><th>Port</th><th>Erforderlich</th><th>Schnellbefehl</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>ja</td><td>(Windows-Dienst)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>ja</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>ja</td><td><code>npm run serve:dev</code></td></tr><tr><td>RAG-Engine .NET (ONNX)</td><td>in-process</td><td>nein</td><td>wird vom Backend beim ersten Chat geladen</td></tr></tbody></table><h2>Quick Start — erste Komponente</h2><p>1. AsmxProxy-Endpoint konfigurieren.</p><p>2. Route-Metadata und Spalten definieren.</p><p>3. <code>wuic-data-source</code> + <code>wuic-data-repeater</code> montieren.</p><p>Snippet 5:</p><h2>Die ersten Listen zeigen 20 Datensaetze: das ist der Trial-Modus</h2><p>Eine frische Installation laeuft <strong>ohne Lizenz</strong>, also im Trial-Modus, und in diesem Modus</p><p>ist <strong>jede Abfrage auf 20 Datensaetze begrenzt</strong>. Das ist weder ein Fehler noch eine Grenze</p><p>der Datenbank: Eine Liste, die bei einer Tabelle mit Tausenden von Zeilen &quot;20 von 20&quot;</p><p>anzeigt, laeuft schlicht ohne Lizenz.</p><p>So laesst es sich in einer Sekunde bestaetigen:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> bestaetigt den Trial-Modus, und <code>licenseReason</code> nennt den Grund</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>So kommt man heraus: Die Lizenz unter <strong>Verwaltung &gt; AppSettings-Editor</strong>, Reiter Licensing,</p><p>einfuegen - sie gilt sofort, ein Neustart des Backends ist nicht noetig. Die Details,</p><p>einschliesslich der Grenzen, die der Trial ueber die Datensatzzahl hinaus setzt, stehen auf</p><p>der Seite <a href=\"licensing.md\">Licensing</a>.</p>",
28088
+ "html": "<h1>Framework-Uebersicht</h1><p>WUIC Framework ist eine metadata-gesteuerte Plattform zum schnellen und konsistenten Erstellen von Business-Oberflaechen.</p><h2>Was ist enthalten</h2><ul><li>DataSource und DataRepeater zur Orchestrierung von Daten und Rendering.</li><li>Visuelle Archetypes (list, map, scheduler, chart, carousel).</li><li>Runtime-Designer fuer Dashboards und dynamische Templates.</li><li>Workflow-Designer/Runner fuer operative Prozesse.</li><li>RAG Chatbot zum Abfragen der Codebasis in natuerlicher Sprache.</li><li>Multi-DBMS: SQL Server, MySQL, PostgreSQL und Oracle ueber Drop-in-Provider (<code>dbms</code>: <code>mssql</code> | <code>mysql</code> | <code>postgresql</code> | <code>oracle</code>).</li></ul><h2>First Steps — Dev-Umgebung starten</h2><p>Der WUIC-Stack besteht aus mehreren Diensten. Starte sie in der angegebenen Reihenfolge.</p><h3>Voraussetzungen</h3><table><thead><tr><th>Was</th><th>Verifikation</th><th>Hinweise</th></tr></thead><tbody><tr><td>.NET 10 SDK</td><td><code>dotnet --version</code></td><td>Hosting Bundle fuer IIS in Produktion</td></tr><tr><td>Node.js 22 LTS</td><td><code>node --version</code></td><td>Mit 22 wird das Paket getestet; unter npm 10.9.x gelingt die Installation dank der mitgelieferten <code>package-lock.json</code></td></tr><tr><td>PowerShell 5.1+</td><td><code>$PSVersionTable.PSVersion</code></td><td>Die vorinstallierte Windows PowerShell genuegt: <code>rename-project.ps1</code> und die <code>llm-workspace/</code>-Skripte laufen auch unter 5.1. PowerShell 7 wird empfohlen, ist nicht erforderlich</td></tr><tr><td>SQL Server 2019+</td><td><code>sqlcmd -S localhost\\sqlexpress -E -C -Q &quot;SELECT 1&quot;</code></td><td>Fehlt er, <strong>installiert ihn der Einzeiler selbst</strong> (SQL Server Express, oder die mit <code>-Dbms</code> gewaehlte Engine); auf Windows Server nicht, siehe Kasten unten. 2019 reicht fuer die Pakete ohne <code>.bak</code>; das <code>.bak</code>-Tutorial ist ein SQL Server 2022-Backup und benoetigt <strong>2022 oder neuer</strong></td></tr><tr><td>Linux (Installer <code>install.sh</code>)</td><td><code>lsb_release -ds</code></td><td><strong>Ubuntu 22.04 oder 24.04 LTS</strong> (24.04 empfohlen): nur diese Ubuntu-Versionen werden von Microsoft fuer SQL Server unterstuetzt, die <strong>26.04 wird nicht unterstuetzt</strong> (<a href=\"https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup?view=sql-server-ver17\" target=\"_blank\" rel=\"noopener noreferrer\">Supported platforms</a>). Gleiche Empfehlung mit MySQL, PostgreSQL und Oracle, da der Installer auf diesen Versionen getestet ist</td></tr><tr><td>NVIDIA-GPU (CUDA 12.x + cuDNN 9)</td><td><code>nvidia-smi</code></td><td>Optional: beschleunigt den RAG Chatbot (ohne GPU laeuft er auf CPU)</td></tr></tbody></table><p>&gt; <strong>Auf Windows Server muss die Datenbank vorher installiert werden.</strong> Der Einzeiler beschafft</p><p>&gt; fehlende Komponenten ueber <code>winget</code>, das auf Windows Server nicht vorhanden ist: ohne eine</p><p>&gt; bereits erreichbare Instanz bricht die Installation mit `No SQL Server instance reachable and</p><p>&gt; winget is missing` ab. Installiere die Engine also vor dem Ausfuehren der Zeile - SQL Server</p><p>&gt; muss <strong>2019 oder neuer</strong> sein (Express genuegt; <strong>2022 oder neuer</strong> fuer das <code>.bak</code>-Tutorial),</p><p>&gt; eine 2017 wird nicht akzeptiert. Ist es nicht die Standardinstanz, uebergib sie mit</p><p>&gt; <code>-SqlServer &#39;localhost\\INSTANZNAME&#39;</code>. Das gilt fuer alle Engines, nicht nur SQL Server.</p><h3>1. Projekt-Template herunterladen</h3><p>Unter Windows erledigt eine PowerShell-Zeile alles: sie laedt das Paket, prueft .NET SDK 10</p><p>und Node.js 22 (und installiert, was fehlt), fuehrt <code>dotnet restore</code> und <code>npm install</code> aus</p><p>und installiert die Erweiterung WUIC Assistant in VS Code.</p><p>Snippet 1:</p><p>Nuetzliche Optionen auf derselben Zeile: <code>-Dbms mysql</code> (oder <code>postgres</code>, <code>oracle</code>) fuer eine</p><p>andere Engine als SQL Server — der Installer installiert und konfiguriert sie selbst — und</p><p><code>-WithTutorial</code>, um auch die Beispieldatenbank herunterzuladen.</p><p>Wenn er fertig ist, ist der letzte Bildschirm bereits die Aufgabenliste: öffnen Sie die</p><p>Workspace-Datei <code>WuicTest.code-workspace</code> in VS Code und drücken Sie <strong>F5</strong> — das startet</p><p>Backend und Frontend gemeinsam über den Launcher <code>Fullstack: WuicTest + Chrome</code> (die Launcher</p><p>stehen in <code>.vscode/launch.json</code>, daneben der nur für das Backend). Wer lieber im Terminal</p><p>arbeitet, findet die entsprechenden Befehle weiter unten.</p><p>Snippet 2:</p><p>Die Verbindungszeichenfolge ist fertig: beim ersten Start fragt der Wizard nach nichts</p><p>weiterem. Mit <code>-Dbms mysql</code> oder <code>-Dbms postgres</code> aendert sich diese Zeile entsprechend und</p><p>traegt Benutzer und Passwort in sich, denn das Superuser-Passwort <strong>erzeugt der Installer</strong>;</p><p>dasselbe Passwort wird in <code>wuic-secrets.json</code> neben der Installation gespeichert, und das ist</p><p>der einzige Ort, an dem es geschrieben bleibt.</p><p>Der manuelle Weg bleibt gueltig, auf jedem System:</p><p>Starten Sie mit dem Paket <code>WuicTest-src-*.zip</code> (Link im <strong>Download</strong>-Bereich der Website).</p><p>Entpacken Sie es in einen Arbeitsordner, z.B. <code>C:\\dev\\WuicTest</code> (die Dateien kommen direkt</p><p>in den Root, NICHT in einen <code>src/</code>-Unterordner). Das Paket enthaelt:</p><ul><li><code>WuicTest.csproj</code> → Host-Projekt mit <code>&lt;PackageReference Include=&quot;WuicCore&quot; Version=&quot;...&quot; /&gt;</code></li><li><code>wwwroot/</code> → Angular-App, deren <code>package.json</code> <code>wuic-framework-lib</code> aus der npm-Registry referenziert</li><li><code>appsettings.json</code> vorkonfiguriert im <code>firstRun = &quot;true&quot;</code>-Modus mit <code>__SET_*__</code>-Platzhaltern</li><li><code>appsettings-samples/</code> → 6 fertige Templates (<code>mssql</code>/<code>mysql</code>, firstRun/post-firstRun, <code>Development</code>)</li></ul><p>&gt; <strong>Kein Klonen des Framework-Repositories noetig</strong>: <code>WuicCore</code> kommt als NuGet-Paket von</p><p>&gt; <code>nuget.org</code> und <code>wuic-framework-lib</code> als npm-Paket aus der npm-Registry. Der Framework-</p><p>&gt; Quellcode wird nur benoetigt, wenn Sie ihn modifizieren wollen — typischerweise nicht Ihr Fall.</p><p>&gt; <strong>npm 10.9.x und Pakete aelter als 1.7.1</strong>: diese Pakete enthalten keine <code>package-lock.json</code>, und</p><p>&gt; ohne Lock bricht npm 10.9.x (die Version, die Node 22 LTS mitbringt) mit</p><p>&gt; <code>Cannot read properties of null (reading &#39;edgesOut&#39;)</code> bei der Peer-Aufloesung ab.</p><p>&gt; Laden Sie den neben der Release veroeffentlichten Lock herunter —</p><p>&gt; <code>https://wuic-framework.com/downloads/locks/&lt;zip-name&gt;.package-lock.json</code> — legen Sie ihn in</p><p>&gt; <code>wwwroot/</code> und starten Sie <code>npm install</code> erneut. Der <code>-Src</code>-Einzeiler erledigt das selbst.</p><h3>2. Datenbank</h3><p>Es braucht eine erreichbare Instanz der Engine. <strong>Mit dem Einzeiler ist nichts zu tun</strong>: er</p><p>installiert und konfiguriert die Instanz, und die Verbindungszeichenfolge zum Einfuegen in den</p><p>Wizard ist das Letzte, was er ausgibt. Auf dem manuellen Weg muss die Instanz bereits</p><p>vorhanden sein.</p><p>Verbindungen in <code>appsettings.json</code>:</p><ul><li><code>MetaDataSQLConnection</code> — Metadata-DB (Menue, Tabellen, Spalten, Board, Benutzer)</li><li><code>DataSQLConnection</code> — Anwendungsdaten-DB</li></ul><p>Bei Erstinstallation (<code>AppSettings.firstRun = &quot;true&quot;</code> im Template) erstellt der Scaffolding-</p><p>Wizard beim ersten Start das Metadata-Schema und befuellt die Daten-DB.</p><p>Siehe Seite <em>Initial Scaffolding</em> fuer den gefuehrten Flow.</p><p>#### Was der First-Run-Wizard abfragt</p><p>Beim ersten Start zeigt die Anwendung nicht das Login, sondern die Seite **Initiale</p><p>Projektkonfiguration**. Sie wird genau einmal ausgefuellt: danach steht <code>firstRun</code> auf <code>false</code></p><p>und die Seite erscheint nicht wieder.</p><ul><li><strong>Setup-Modus</strong> — zwei Eintraege: <strong>Bestehende Datenbank</strong>, die eine eigene Datenbank</li></ul><p> registriert, und <strong>Tutorial WideWorldImporters</strong>, das die Beispieldatenbank installiert. Der</p><p> zweite erscheint nur, wenn das Paket das Tutorial mitbringt (<code>tutorialAvailable</code>): in Paketen</p><p> ohne Tutorial gibt es die Auswahl gar nicht, und der Modus ist &quot;Bestehende Datenbank&quot;.</p><ul><li><strong>DBMS</strong> — die Engine. Beim Wechsel wird die Verbindungszeichenfolge darunter in der Form des</li></ul><p> gewaehlten Providers neu geschrieben.</p><ul><li><strong>DataSQLConnection</strong> — die Zeichenfolge zur Daten-DB und das Feld, von dem alles Weitere</li></ul><p> abhaengt: bis <strong>Verbindung testen und Datenbanken laden</strong> gedrueckt wurde, bleibt die</p><p> Datenbankliste deaktiviert. Der Test prueft die Zugangsdaten, listet die Datenbanken des</p><p> Servers und korrigiert auf SQL Server selbsttaetig die Zertifikatsparameter</p><p> (<code>Encrypt=False;TrustServerCertificate=True</code>) und sagt es Ihnen. <code>Integrated Security</code> gilt</p><p> nur auf SQL Server: die anderen Engines brauchen Benutzer und Passwort.</p><ul><li><strong>Daten-Datenbank</strong> und <strong>Name der Metadaten-DB</strong> — die erste wird aus der vom Test geladenen</li></ul><p> Liste gewaehlt (im Tutorial-Modus ist es ein freies Textfeld, Standard <code>WideWorldImporters</code>);</p><p> die zweite ist der Name der anzulegenden Metadaten-DB (Standard <code>metadataDB</code>, im</p><p> Tutorial-Modus <code>MetadataCRM</code>). Existiert diese Datenbank bereits, fragt der Wizard vor dem</p><p> Neuanlegen nach.</p><ul><li><strong>Tabellen automatisch scaffolden</strong> (nur bei &quot;Bestehende Datenbank&quot;) — befuellt</li></ul><p> <code>_metadati__tabelle</code> / <code>_metadati__colonne</code> aus allen Tabellen der gewaehlten Datenbank. Das</p><p> verlaengert das Provisioning bei Datenbanken mit vielen Tabellen am staerksten.</p><ul><li><strong>Initialer Admin-Benutzer</strong> — Benutzername (Standard <code>admin</code>), Passwort (mindestens 4</li></ul><p> Zeichen) und Sprache. Es gibt kein Standardpasswort: das hier eingegebene ist das einzige, mit</p><p> dem die erste Anmeldung gelingt. Die gewaehlte Sprache gilt fuer den Admin-Benutzer und fuer</p><p> diese Seite selbst.</p><ul><li><strong>RAG Chatbot</strong> — nur der Anthropic-Schluessel, optional. Nichts zu installieren: siehe</li></ul><p> Schritt 5.</p><p>Nach <strong>Bestaetigen und Metadaten-DB erzeugen</strong> laeuft das Provisioning selbstaendig weiter, mit</p><p>Fortschrittsbalken und der Zahl verarbeiteter Batches. Dauer, gemessen auf den</p><p>Test-Installationen: <strong>30 s - 2 min</strong> im Tutorial-Modus, <strong>1-4 min</strong> auf einer bestehenden</p><p>Datenbank mit aktiviertem automatischem Scaffolding. Am Ende weicht die Seite dem Login-Formular.</p><h3>3. Backend (.NET)</h3><p>Aus dem Root des entpackten Ordners (z.B. <code>C:\\dev\\WuicTest</code>):</p><p>Snippet 3:</p><p>Das Backend stellt die APIs auf <code>http://localhost:5000</code> bereit.</p><h3>4. Frontend (Angular)</h3><p>In einer zweiten Shell, aus dem Unterordner <code>wwwroot/</code>:</p><p>Snippet 4:</p><p>Das Frontend ist auf <code>http://localhost:4200</code> verfuegbar.</p><p>Die Zugangsdaten sind die des im First-Run-Wizard gewaehlten Admin-Benutzers: der</p><p>Benutzername ist <code>admin</code>, sofern nicht geaendert, und das Passwort ist das selbst gesetzte (ein</p><p>Standardpasswort gibt es nicht).</p><h3>5. RAG Chatbot (optional)</h3><p>Der RAG Chatbot (<code>Amministrazione &gt; RAG Chat</code>) laeuft <strong>im .NET-Backend</strong>: die</p><p>Retrieval-Engine (<code>rag-engine/WuicRagEngine.dll</code>, ONNX Runtime in-process) wird</p><p>vom Backend geladen, wenn <code>AppSettings.rag-use-dotnet-engine</code> auf <code>&quot;true&quot;</code></p><p>steht (Default in den Release-Paketen). **Kein Python, keine venv, kein</p><p>separater Server, der gestartet werden muss.**</p><p>Beim <strong>ersten</strong> Oeffnen des Chatbots (oder am Ende des First-Run) laedt das</p><p>Backend selbststaendig die ONNX-Modelle (<code>bge-m3</code> + Reranker), den Tokenizer</p><p>und den Index (~4,5 GB) von <code>rag-engine-models-url</code> (Default</p><p><code>https://wuic-framework.com/rag-models</code>) nach <code>rag-engine/artifacts/</code>. Eine</p><p>Internetverbindung wird nur einmal benoetigt; der Download laeuft im</p><p>Hintergrund (1-5 Min.) und eine In-App-Benachrichtigung erscheint zu Beginn</p><p>und am Ende. Solange zeigt die Komponente den Status <strong>RAG offline</strong>, der Rest</p><p>der Anwendung funktioniert normal.</p><p>Relevante <code>AppSettings</code>-Schluessel (alle mit fertigen Defaults):</p><ul><li><code>rag-use-dotnet-engine</code> = <code>true</code> — aktiviert die .NET-Engine (<code>false</code> = Python-Fallback, siehe unten)</li><li><code>rag-engine-device</code> = <code>auto</code> — <code>auto</code> | <code>cuda</code> | <code>cpu</code>: mit NVIDIA-GPU (CUDA 12.x + cuDNN 9) ~1 s/Abfrage, auf CPU ~15-25 s/Abfrage</li><li><code>rag-engine-profile</code> = <code>release</code> — <code>release</code> = Framework-Quellen nur als Signatur; <code>internal</code> = vollstaendiger Index</li><li><code>rag-engine-models-url</code> — Download-Quelle beim ersten Start</li><li><code>rag-engine-cuda-path</code> = (leer) — Ordner der CUDA/cuDNN-DLLs, falls nicht systemweit installiert</li></ul><p>Fuer den Chat mit LLM <code>rag-llm-provider</code> + <code>rag-llm-api-key</code> setzen (Claude</p><p>via Anthropic oder ein lokales Modell via Ollama mit <code>rag-llm-base-url</code>). Ohne</p><p>Provider funktioniert nur der Retrieval-Modus (Snippet-Suche). Details zu den</p><p>Schluesseln auf der Seite <em>AppSettings</em>.</p><p>&gt; Der historische Python-Stack (<code>scripts/rag-setup.ps1</code> + <code>rag_server.py</code> auf</p><p>&gt; <code>127.0.0.1:8765</code>) ist nur als Fallback mit <code>rag-use-dotnet-engine = &quot;false&quot;</code></p><p>&gt; nutzbar und ist in den Release-Paketen nicht enthalten.</p><h2>Dienste-Zusammenfassung</h2><table><thead><tr><th>Dienst</th><th>Port</th><th>Erforderlich</th><th>Schnellbefehl</th></tr></thead><tbody><tr><td>SQL Server</td><td>1433</td><td>ja</td><td>(Windows-Dienst)</td></tr><tr><td>Backend .NET</td><td>5000</td><td>ja</td><td><code>dotnet watch ... run</code></td></tr><tr><td>Frontend Angular</td><td>4200</td><td>ja</td><td><code>npm run serve:dev</code></td></tr><tr><td>RAG-Engine .NET (ONNX)</td><td>in-process</td><td>nein</td><td>wird vom Backend beim ersten Chat geladen</td></tr></tbody></table><h2>Quick Start — erste Komponente</h2><p>1. AsmxProxy-Endpoint konfigurieren.</p><p>2. Route-Metadata und Spalten definieren.</p><p>3. <code>wuic-data-source</code> + <code>wuic-data-repeater</code> montieren.</p><p>Snippet 5:</p><h2>Die ersten Listen zeigen 20 Datensaetze: das ist der Trial-Modus</h2><p>Eine frische Installation laeuft <strong>ohne Lizenz</strong>, also im Trial-Modus, und in diesem Modus</p><p>ist <strong>jede Abfrage auf 20 Datensaetze begrenzt</strong>. Das ist weder ein Fehler noch eine Grenze</p><p>der Datenbank: Eine Liste, die bei einer Tabelle mit Tausenden von Zeilen &quot;20 von 20&quot;</p><p>anzeigt, laeuft schlicht ohne Lizenz.</p><p>So laesst es sich in einer Sekunde bestaetigen:</p><p>Snippet 6:</p><p><code>licenseValid: false</code> bestaetigt den Trial-Modus, und <code>licenseReason</code> nennt den Grund</p><p>(<code>invalid_signature</code>, <code>license_expired</code>, <code>machine_fingerprint_not_authorized</code>, ...).</p><p>So kommt man heraus: Die Lizenz unter <strong>Verwaltung &gt; AppSettings-Editor</strong>, Reiter Licensing,</p><p>einfuegen - sie gilt sofort, ein Neustart des Backends ist nicht noetig. Die Details,</p><p>einschliesslich der Grenzen, die der Trial ueber die Datensatzzahl hinaus setzt, stehen auf</p><p>der Seite <a href=\"licensing.md\">Licensing</a>.</p>",
28065
28089
  "codeSamples": [
28066
28090
  {
28067
28091
  "id": "code_1",
@@ -28424,7 +28448,7 @@ const frameworkDocsContent = {
28424
28448
  {
28425
28449
  "id": "overview",
28426
28450
  "title": "Overview",
28427
- "html": "<h1>Pattern: Framework-Komponente + Custom-Daten</h1><p>Die High-Level-UI-Komponenten des Frameworks (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) akzeptieren ein <strong>manuell konstruiertes Datasource</strong> (<code>hardcodedDatasource</code>), das nicht durch den Data-Layer des Frameworks laeuft. Die Daten kommen aus deinem Custom-Backend (dein .NET-Controller, externe REST, statische Dateien, WebSocket, ...).</p><h2>Wann verwenden</h2><ul><li>Du willst die vollstaendige UX einer List-Grid/Chart-List (Filter, Sortierung, Client-Paging, Export, Edit-Dialog), aber die Daten werden erzeugt von:</li></ul><p> - Einem externen REST-Endpoint (3rd-Party-API, Microservice).</p><p> - Einem deiner eigenen .NET-Controller, nicht integriert mit dem Data-Layer des Frameworks.</p><p> - Statischen Dateien, aggregierten Berechnungen, Live-Daten (WebSocket, Polling).</p><ul><li>Du hast eine Legacy-Domain, die du nicht im Framework modellieren willst.</li><li>Du prototypisierst, ohne die Datenstruktur bereits definiert zu haben.</li></ul><h2>Architektur</h2><ul><li><strong>Entwickler</strong>: schreibt eine kleine Angular-Komponente, die die Daten vom eigenen Backend abruft und sie in ein lokales Datasource verpackt.</li><li><strong>Framework</strong>: die List-Grid verhaelt sich genauso, als kaemen die Daten aus dem Standard-Data-Layer (Filter, Sortierung, Paging, Export funktionieren).</li><li><strong>Backend</strong>: totale Freiheit. Klassische REST-Endpoints, keine Metadata-Konventionen.</li></ul><h2>Was du tust (Frontend)</h2><p>Du erstellst eine Angular-Standalone-Komponente, die:</p><p>1. Deinen Custom-Endpoint mit <code>HttpClient</code> aufruft.</p><p>2. Die Spalten (Name, Label, Typ) fuer das lokale Datasource definiert.</p><p>3. Zeilen + Spalten im Datasource publiziert und es an die List-Grid uebergibt.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Was du tust (Backend, optional)</h2><p>Wenn die Daten aus deinem internen Backend kommen, genuegt ein klassischer REST-Controller. Keine Framework-Konventionen, keine Metadata zu schreiben.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contra</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> mit Filter/Sortierung/Paging/Export kostenlos (Client-Side)</td><td>Du haeltst selbst die Konsistenz zwischen den Zeilen und der Spaltendefinition</td></tr><tr><td>Backend vollstaendig frei</td><td>CRUD funktioniert nicht out-of-the-box: Edit/Insert muessen manuell an dein Backend verdrahtet werden</td></tr><tr><td>Gut fuer 3rd-Party-Integrationen</td><td>Paging/Sortierung/Filter serverseitig erfordert Custom-Wiring (siehe unten)</td></tr><tr><td>Kein Scaffolding-Aufwand</td><td>Type Safety nur via Cast</td></tr></tbody></table><h2>Filter / Sortierung / Paging: Client-Side vs. Server-Side</h2><p>Der Satz &quot;Filter/Sortierung/Paging/Export kostenlos&quot; der Trade-off-Tabelle gilt <strong>nur im Client-Side-Modus</strong> und haengt von einem Metadata-Flag ab, das explizit im Hardcoded-Datasource gesetzt werden muss.</p><h3>Das Schluessel-Flag: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (Eigenschaft von <code>MetadatiTabella</code>) steuert <strong>wo</strong> Paging/Sortierung/Filter ausgefuehrt werden:</p><table><thead><tr><th>Wert</th><th>Bedeutung</th><th>Wann verwenden</th></tr></thead><tbody><tr><td><code>true</code> (Default)</td><td>Die List-Grid sendet Paging-/Sortier-/Filter-Events an das Backend ueber den Standard-CRUD-Endpoint des Frameworks. Das Backend gibt nur die angeforderte Seite zurueck und wendet Sortierung/Filter SQL-seitig an.</td><td>Pattern 1 und 2 (mit echter Route-Metadata und WUIC-Backend dahinter).</td></tr><tr><td><code>false</code></td><td>Die List-Grid fuehrt Paging/Sortierung/Filter in-memory auf dem bereits geladenen Array aus. Kein Server-Roundtrip.</td><td><strong>Immer in den Hardcoded-Datasources des Pattern 3</strong> (und jedes Mal, wenn du alle Zeilen auf einmal via <code>fetchInfo$.next</code> publizierst).</td></tr></tbody></table><p><strong>Typische Falle bei Pattern 3</strong>: wenn du vergisst, <code>md_server_side_operations: false</code> zu erzwingen, zeigt die List-Grid die 50/100 empfangenen Zeilen an, aber <strong>Klicken auf Seite 2, Sortieren einer Spalte oder Tippen im Filter macht nichts</strong> — das Grid sendet das Event an das &quot;Framework-Backend&quot;, das nicht existiert, und die UX erscheint blockiert ohne Konsolenfehler.</p><p>&gt; <strong>Framework-Hinweis</strong>: <code>DataSourceComponent.fetchData()</code> erkennt automatisch den Fall &quot;Hardcoded-Datasource&quot; (keine <code>[hardcodedRoute]</code> gesetzt) und <strong>ueberspringt den Backend-Aufruf</strong> bei jedem Paging-/Sortier-/Filter-Wechsel und re-publiziert das bereits im Speicher vorhandene Payload auf <code>fetchInfo$</code>. Das bedeutet, dass, sobald <code>fetchInfo$.next(...)</code> das erste Mal in deinem <code>ngAfterViewInit</code> befuellt ist, Paging/Sortierung/Filter clientseitig ohne Server-Roundtrip funktionieren, auch wenn das WUIC-Backend die Route nicht registriert hat. Siehe <code>data-source.component.ts</code> (Short-Circuit in <code>fetchData()</code>).</p><h3>Client-Side-Modus (empfohlener Default fuer Pattern 3)</h3><p>Snippet 4:</p><ul><li>Du laedst <strong>alle</strong> Zeilen mit einem einzigen <code>fetchInfo$.next</code>.</li><li>Die List-Grid wendet Filter/Sortierung/Paging/Export auf dem bereits vorhandenen Array an.</li><li>Null Zusatzcode.</li><li>Geeignet fuer kleine/mittlere Datasets (Groessenordnung einige Tausend Zeilen).</li></ul><h3>Server-Side-Modus (manuelles Wiring)</h3><p>Fuer grosse Datasets (Zehn-/Hunderttausende Zeilen) <strong>willst du</strong> nicht alles im Speicher laden. Du laesst <code>md_server_side_operations: true</code> (Default), abonnierst die <strong>@Output von `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) und rufst deinen REST-Endpoint bei jeder Zustandsaenderung erneut auf. Die List-Grid aktualisiert <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>vor</strong> dem Emit des Events, also genuegt es im Handler, den aktuellen Zustand zu lesen.</p><p>Snippet 5:</p><p>Komplementaerer Server-Endpoint (C#-Beispiel, siehe <code>SamplesController.GetInventory</code>):</p><p>Snippet 6:</p><p><strong>Schluesselpunkte:</strong></p><ul><li>Die <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> von <code>&lt;wuic-list-grid&gt;</code> exponieren die UI-Events <strong>nachdem</strong> der List-Grid-Handler bereits den Datasource-Zustand aktualisiert hat. Nichts neu zu implementieren: lies <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>Das Backend muss <code>{ rows, total }</code> zurueckgeben, wobei <code>total</code> der Count <strong>POST-filter / PRE-page</strong> ist. Ohne dies weiss der UI-Pager nicht, wie viele Seiten existieren, und funktioniert nicht korrekt.</li><li>Der Filter-Operator kommt im Feld <code>operatore</code> des Filter-Eintrags an (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code> usw., siehe matchMode-Tabelle in <a href=\"./list-grid.md\">List Grid</a>). Mappe ihn konsistent serverseitig.</li></ul><h2>Variante: Konsum des OData-Endpoints des Frameworks</h2><p>Wenn die Entity, die du anzeigen willst, bereits vom Framework als OData-Entity-Set (<code>/odata/&lt;EntitySet&gt;</code>) exponiert wird, brauchst du KEINEN Controller zu schreiben: es genuegt, den UI-Zustand der List-Grid in eine Standard-OData-v4-Query-String zu uebersetzen.</p><p>&gt; <strong>Alternative 100% Framework-driven (Pattern 1 mit OData-Backend)</strong>: wenn du eine Standard-Route-Metadata fuer die Entity registrierst, kannst du <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> konfigurieren und das Datasource macht <strong>alles allein</strong> (Filter/Sortierung/Paging/Export) via dem internen OData-Provider. Kein Custom-Angular-Code. Siehe <a href=\"./odata.md\">OData</a> fuer das vollstaendige Setup. Pattern 3 (diese Seite) gilt hingegen, wenn du explizite Kontrolle auf der Frontend-Seite willst oder keine Metadata fuer die Entity registriert hast.</p><p>Das Framework stellt <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> bereit, das das gesamte Operator-Mapping WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) mit automatischem Quoting fuer string/numeric, Unterstuetzung fuer verschachtelte Filter-Gruppen (AND/OR rekursiv) und isnull/isnotnull durchfuehrt. Der Return-Wert ist eine relative URL wie <code>/odata/Cities?$filter=&lt;encoded expression&gt;</code>. Es genuegt, die Base-URL zu prefixieren und <code>$top / $skip / $orderby</code> hinzuzufuegen.</p><p>Snippet 7:</p><p>&gt; <strong>Hinweis zu total/count</strong>: wenn der verwendete OData-Endpoint konfiguriert ist, den Standard-OData-Wrapper <code>{ value: [...], &quot;@odata.count&quot;: N }</code> zurueckzugeben (via <code>$count=true</code>), kannst du den Total direkt aus der Response lesen, ohne die zweite Query. Der WUIC-Framework-Endpoint gibt aktuell das Plain Array zurueck und erfordert die parallele Query.</p><h2>Lebende Beispiele im WuicTest</h2><p>Die drei Beispiele decken die <strong>drei Hauptstrategien</strong> des Pattern 3 ab:</p><ul><li><strong>External REST Grid (Client-Side)</strong> &rarr; laedt ALLE 50 Posts von einem externen Endpoint (<code>jsonplaceholder.typicode.com/posts</code>) auf einmal, Paging/Sortierung/Filter in-memory von der List-Grid angewendet (<code>md_server_side_operations: false</code>). Source-Ordner: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Demo oeffnen</a>.</li><li><strong>Custom .NET Grid (Server-Side, REST Custom)</strong> &rarr; ruft den Controller <code>SamplesController.GetInventory</code> mit offset/limit/sort/filter als Ad-hoc-Query-Params auf, laedt nur die aktuelle Seite bei jeder Aenderung neu (<code>md_server_side_operations: true</code> + explizites Wiring auf <code>(onPaging)/(onSorting)/(onFiltering)</code>). Source-Ordner: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Demo oeffnen</a>.</li><li><strong>OData Cities Grid (Server-Side, Standard OData v4)</strong> &rarr; konsumiert den generischen OData-Endpoint des Frameworks (<code>GET /odata/Cities</code>) mit Standard-Query-String <code>$top / $skip / $filter / $orderby</code>, kein Custom-Controller zu schreiben. Uebersetzt die UI-Events der List-Grid in OData-Queries (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code> usw.). Source-Ordner: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Demo oeffnen</a>.</li></ul><h3>Wann welche Variante waehlen</h3><table><thead><tr><th>Beispiel</th><th>Strategie</th><th>Backend</th><th>Wann verwenden</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-Side</td><td>Klassischer REST-Endpoint, der ein Array zurueckgibt</td><td>Kleines-mittleres Dataset (&lt; einige Tausend Zeilen), maximale Einfachheit, 3rd-Party-API ohne serverseitige Kontrolle</td></tr><tr><td><strong>3b</strong></td><td>Server-Side REST Custom</td><td>Dein REST-Controller mit Paging-/Sortier-/Filter-Query-Params</td><td>Grosses Dataset, du willst totale Kontrolle ueber die Query; der End-Dev hat bereits einen existierenden Endpoint mit offset/limit usw.</td></tr><tr><td><strong>3c</strong></td><td>Server-Side OData</td><td>OData-Endpoint des Frameworks (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Grosses Dataset vom Framework automatisch als OData-Set exponiert; null Backend-Code; Standard-Syntax kompatibel mit anderen Clients</td></tr></tbody></table><h2>Siehe auch</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full Autogeneration</a>: wenn die Standard-UX genuegt und die Daten im gescaffoldeten Modell existieren.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework-Daten + Custom-Komponente</a>: umgekehrt (Custom-UI, Framework-Daten).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full Custom</a>: wenn du nicht einmal die List-Grid brauchst.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework-Komponente + Framework-Daten (manuelle Montage)</a>: &quot;Framework&quot;-Variante dieses Patterns: gleiche manuelle Widget-Komposition, aber metadata-gesteuerter Data-Layer statt Custom-Backend.</li></ul>",
28451
+ "html": "<h1>Pattern: Framework-Komponente + Custom-Daten</h1><p>Die High-Level-UI-Komponenten des Frameworks (<code>&lt;wuic-list-grid&gt;</code>, <code>&lt;wuic-chart-list&gt;</code>, ...) akzeptieren ein <strong>manuell konstruiertes Datasource</strong> (<code>hardcodedDatasource</code>), das nicht durch den Data-Layer des Frameworks laeuft. Die Daten kommen aus deinem Custom-Backend (dein .NET-Controller, externe REST, statische Dateien, WebSocket, ...).</p><h2>Wann verwenden</h2><ul><li>Du willst die vollstaendige UX einer List-Grid/Chart-List (Filter, Sortierung, Client-Paging, Export, Edit-Dialog), aber die Daten werden erzeugt von:</li></ul><p> - Einem externen REST-Endpoint (3rd-Party-API, Microservice).</p><p> - Einem deiner eigenen .NET-Controller, nicht integriert mit dem Data-Layer des Frameworks.</p><p> - Statischen Dateien, aggregierten Berechnungen, Live-Daten (WebSocket, Polling).</p><ul><li>Du hast eine Legacy-Domain, die du nicht im Framework modellieren willst.</li><li>Du prototypisierst, ohne die Datenstruktur bereits definiert zu haben.</li></ul><h2>Architektur</h2><ul><li><strong>Entwickler</strong>: schreibt eine kleine Angular-Komponente, die die Daten vom eigenen Backend abruft und sie in ein lokales Datasource verpackt.</li><li><strong>Framework</strong>: die List-Grid verhaelt sich genauso, als kaemen die Daten aus dem Standard-Data-Layer (Filter, Sortierung, Paging, Export funktionieren).</li><li><strong>Backend</strong>: totale Freiheit. Klassische REST-Endpoints, keine Metadata-Konventionen.</li></ul><h2>Was du tust (Frontend)</h2><p>Du erstellst eine Angular-Standalone-Komponente, die:</p><p>1. Deinen Custom-Endpoint mit <code>HttpClient</code> aufruft.</p><p>2. Die Spalten (Name, Label, Typ) fuer das lokale Datasource definiert.</p><p>3. Zeilen + Spalten im Datasource publiziert und es an die List-Grid uebergibt.</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html --&gt;</p><p>Snippet 1:</p><p>&lt;!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts --&gt;</p><p>Snippet 2:</p><h2>Was du tust (Backend, optional)</h2><p>Wenn die Daten aus deinem internen Backend kommen, genuegt ein klassischer REST-Controller. Keine Framework-Konventionen, keine Metadata zu schreiben.</p><p>&lt;!-- source: WuicTest/Controllers/SamplesController.cs --&gt;</p><p>Snippet 3:</p><h3>Wenn Sie die Verbindung selbst oeffnen: unter Oracle die Session auf Ihr Schema bringen</h3><p>Wenn Ihr Controller mit der Verbindungszeichenfolge <code>DataSQLConnection</code> eine eigene Verbindung</p><p>oeffnet (ADO.NET, Dapper, EF Core mit <code>UseOracle(...)</code>), verlangt <strong>Oracle</strong> einen Schritt mehr</p><p>als die anderen Engines: in dieser Zeichenfolge steht kein <code>Database</code> / <code>Initial Catalog</code>,</p><p>denn unter Oracle gehoert das Schema nicht zur Verbindung. <code>DataSQLConnection</code> authentifiziert</p><p>nur; auf das Datenschema bringt das Framework die Session selbst, mit</p><p><code>ALTER SESSION SET CURRENT_SCHEMA = &lt;Schema&gt;</code> beim Oeffnen.</p><p>Eine von Hand geoeffnete Verbindung hat diesen Schritt nicht: sie bleibt im Schema des</p><p>authentifizierten Benutzers, und die erste Abfrage auf Ihre Tabellen antwortet</p><p><code>ORA-00942: table or view does not exist</code>, obwohl die Tabelle existiert und die Rechte da sind.</p><p>Zwei saubere Wege:</p><p>Snippet 4:</p><p>Die anderen drei Engines brauchen das nicht: die Datenbank steht in der Verbindungszeichenfolge,</p><p>und die Session startet darin.</p><h2>Trade-off</h2><table><thead><tr><th>Pro</th><th>Contra</th></tr></thead><tbody><tr><td>UX <code>wuic-list-grid</code> mit Filter/Sortierung/Paging/Export kostenlos (Client-Side)</td><td>Du haeltst selbst die Konsistenz zwischen den Zeilen und der Spaltendefinition</td></tr><tr><td>Backend vollstaendig frei</td><td>CRUD funktioniert nicht out-of-the-box: Edit/Insert muessen manuell an dein Backend verdrahtet werden</td></tr><tr><td>Gut fuer 3rd-Party-Integrationen</td><td>Paging/Sortierung/Filter serverseitig erfordert Custom-Wiring (siehe unten)</td></tr><tr><td>Kein Scaffolding-Aufwand</td><td>Type Safety nur via Cast</td></tr></tbody></table><h2>Filter / Sortierung / Paging: Client-Side vs. Server-Side</h2><p>Der Satz &quot;Filter/Sortierung/Paging/Export kostenlos&quot; der Trade-off-Tabelle gilt <strong>nur im Client-Side-Modus</strong> und haengt von einem Metadata-Flag ab, das explizit im Hardcoded-Datasource gesetzt werden muss.</p><h3>Das Schluessel-Flag: <code>md_server_side_operations</code></h3><p><code>md_server_side_operations</code> (Eigenschaft von <code>MetadatiTabella</code>) steuert <strong>wo</strong> Paging/Sortierung/Filter ausgefuehrt werden:</p><table><thead><tr><th>Wert</th><th>Bedeutung</th><th>Wann verwenden</th></tr></thead><tbody><tr><td><code>true</code> (Default)</td><td>Die List-Grid sendet Paging-/Sortier-/Filter-Events an das Backend ueber den Standard-CRUD-Endpoint des Frameworks. Das Backend gibt nur die angeforderte Seite zurueck und wendet Sortierung/Filter SQL-seitig an.</td><td>Pattern 1 und 2 (mit echter Route-Metadata und WUIC-Backend dahinter).</td></tr><tr><td><code>false</code></td><td>Die List-Grid fuehrt Paging/Sortierung/Filter in-memory auf dem bereits geladenen Array aus. Kein Server-Roundtrip.</td><td><strong>Immer in den Hardcoded-Datasources des Pattern 3</strong> (und jedes Mal, wenn du alle Zeilen auf einmal via <code>fetchInfo$.next</code> publizierst).</td></tr></tbody></table><p><strong>Typische Falle bei Pattern 3</strong>: wenn du vergisst, <code>md_server_side_operations: false</code> zu erzwingen, zeigt die List-Grid die 50/100 empfangenen Zeilen an, aber <strong>Klicken auf Seite 2, Sortieren einer Spalte oder Tippen im Filter macht nichts</strong> — das Grid sendet das Event an das &quot;Framework-Backend&quot;, das nicht existiert, und die UX erscheint blockiert ohne Konsolenfehler.</p><p>&gt; <strong>Framework-Hinweis</strong>: <code>DataSourceComponent.fetchData()</code> erkennt automatisch den Fall &quot;Hardcoded-Datasource&quot; (keine <code>[hardcodedRoute]</code> gesetzt) und <strong>ueberspringt den Backend-Aufruf</strong> bei jedem Paging-/Sortier-/Filter-Wechsel und re-publiziert das bereits im Speicher vorhandene Payload auf <code>fetchInfo$</code>. Das bedeutet, dass, sobald <code>fetchInfo$.next(...)</code> das erste Mal in deinem <code>ngAfterViewInit</code> befuellt ist, Paging/Sortierung/Filter clientseitig ohne Server-Roundtrip funktionieren, auch wenn das WUIC-Backend die Route nicht registriert hat. Siehe <code>data-source.component.ts</code> (Short-Circuit in <code>fetchData()</code>).</p><h3>Client-Side-Modus (empfohlener Default fuer Pattern 3)</h3><p>Snippet 5:</p><ul><li>Du laedst <strong>alle</strong> Zeilen mit einem einzigen <code>fetchInfo$.next</code>.</li><li>Die List-Grid wendet Filter/Sortierung/Paging/Export auf dem bereits vorhandenen Array an.</li><li>Null Zusatzcode.</li><li>Geeignet fuer kleine/mittlere Datasets (Groessenordnung einige Tausend Zeilen).</li></ul><h3>Server-Side-Modus (manuelles Wiring)</h3><p>Fuer grosse Datasets (Zehn-/Hunderttausende Zeilen) <strong>willst du</strong> nicht alles im Speicher laden. Du laesst <code>md_server_side_operations: true</code> (Default), abonnierst die <strong>@Output von `&lt;wuic-list-grid&gt;`</strong> (<code>onPaging</code>, <code>onSorting</code>, <code>onFiltering</code>) und rufst deinen REST-Endpoint bei jeder Zustandsaenderung erneut auf. Die List-Grid aktualisiert <code>ds.currentPage / pageSize / sortInfo / filterInfo</code> <strong>vor</strong> dem Emit des Events, also genuegt es im Handler, den aktuellen Zustand zu lesen.</p><p>Snippet 6:</p><p>Komplementaerer Server-Endpoint (C#-Beispiel, siehe <code>SamplesController.GetInventory</code>):</p><p>Snippet 7:</p><p><strong>Schluesselpunkte:</strong></p><ul><li>Die <code>@Output</code> <code>(onPaging) / (onSorting) / (onFiltering)</code> von <code>&lt;wuic-list-grid&gt;</code> exponieren die UI-Events <strong>nachdem</strong> der List-Grid-Handler bereits den Datasource-Zustand aktualisiert hat. Nichts neu zu implementieren: lies <code>ds.currentPage</code>, <code>ds.pageSize</code>, <code>ds.sortInfo[0]</code>, <code>ds.filterInfo.filters[0]</code>.</li><li>Das Backend muss <code>{ rows, total }</code> zurueckgeben, wobei <code>total</code> der Count <strong>POST-filter / PRE-page</strong> ist. Ohne dies weiss der UI-Pager nicht, wie viele Seiten existieren, und funktioniert nicht korrekt.</li><li>Der Filter-Operator kommt im Feld <code>operatore</code> des Filter-Eintrags an (<code>&#39;eq&#39;</code>, <code>&#39;contains&#39;</code>, <code>&#39;startswith&#39;</code> usw., siehe matchMode-Tabelle in <a href=\"./list-grid.md\">List Grid</a>). Mappe ihn konsistent serverseitig.</li></ul><h2>Variante: Konsum des OData-Endpoints des Frameworks</h2><p>Wenn die Entity, die du anzeigen willst, bereits vom Framework als OData-Entity-Set (<code>/odata/&lt;EntitySet&gt;</code>) exponiert wird, brauchst du KEINEN Controller zu schreiben: es genuegt, den UI-Zustand der List-Grid in eine Standard-OData-v4-Query-String zu uebersetzen.</p><p>&gt; <strong>Alternative 100% Framework-driven (Pattern 1 mit OData-Backend)</strong>: wenn du eine Standard-Route-Metadata fuer die Entity registrierst, kannst du <code>md_props_bag.endpoint = {&quot;type&quot;:&quot;odata&quot;,&quot;uri&quot;:&quot;/odata/Cities&quot;}</code> konfigurieren und das Datasource macht <strong>alles allein</strong> (Filter/Sortierung/Paging/Export) via dem internen OData-Provider. Kein Custom-Angular-Code. Siehe <a href=\"./odata.md\">OData</a> fuer das vollstaendige Setup. Pattern 3 (diese Seite) gilt hingegen, wenn du explizite Kontrolle auf der Frontend-Seite willst oder keine Metadata fuer die Entity registriert hast.</p><p>Das Framework stellt <code>DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName)</code> bereit, das das gesamte Operator-Mapping WUIC -&gt; <code>$filter</code> OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) mit automatischem Quoting fuer string/numeric, Unterstuetzung fuer verschachtelte Filter-Gruppen (AND/OR rekursiv) und isnull/isnotnull durchfuehrt. Der Return-Wert ist eine relative URL wie <code>/odata/Cities?$filter=&lt;encoded expression&gt;</code>. Es genuegt, die Base-URL zu prefixieren und <code>$top / $skip / $orderby</code> hinzuzufuegen.</p><p>Snippet 8:</p><p>&gt; <strong>Hinweis zu total/count</strong>: wenn der verwendete OData-Endpoint konfiguriert ist, den Standard-OData-Wrapper <code>{ value: [...], &quot;@odata.count&quot;: N }</code> zurueckzugeben (via <code>$count=true</code>), kannst du den Total direkt aus der Response lesen, ohne die zweite Query. Der WUIC-Framework-Endpoint gibt aktuell das Plain Array zurueck und erfordert die parallele Query.</p><h2>Lebende Beispiele im WuicTest</h2><p>Die drei Beispiele decken die <strong>drei Hauptstrategien</strong> des Pattern 3 ab:</p><ul><li><strong>External REST Grid (Client-Side)</strong> &rarr; laedt ALLE 50 Posts von einem externen Endpoint (<code>jsonplaceholder.typicode.com/posts</code>) auf einmal, Paging/Sortierung/Filter in-memory von der List-Grid angewendet (<code>md_server_side_operations: false</code>). Source-Ordner: <code>wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3a-external-rest-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Demo oeffnen</a>.</li><li><strong>Custom .NET Grid (Server-Side, REST Custom)</strong> &rarr; ruft den Controller <code>SamplesController.GetInventory</code> mit offset/limit/sort/filter als Ad-hoc-Query-Params auf, laedt nur die aktuelle Seite bei jeder Aenderung neu (<code>md_server_side_operations: true</code> + explizites Wiring auf <code>(onPaging)/(onSorting)/(onFiltering)</code>). Source-Ordner: <code>wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/</code> + <code>Controllers/SamplesController.cs</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3b-custom-dotnet-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Demo oeffnen</a>.</li><li><strong>OData Cities Grid (Server-Side, Standard OData v4)</strong> &rarr; konsumiert den generischen OData-Endpoint des Frameworks (<code>GET /odata/Cities</code>) mit Standard-Query-String <code>$top / $skip / $filter / $orderby</code>, kein Custom-Controller zu schreiben. Uebersetzt die UI-Events der List-Grid in OData-Queries (<code>contains(name,&#39;v&#39;)</code>, <code>field eq value</code> usw.). Source-Ordner: <code>wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/</code>. <a class=\"wuic-demo-link\" href=\"#/examples/3c-odata-cities-grid\" target=\"_blank\" rel=\"noopener noreferrer\">Demo oeffnen</a>.</li></ul><h3>Wann welche Variante waehlen</h3><table><thead><tr><th>Beispiel</th><th>Strategie</th><th>Backend</th><th>Wann verwenden</th></tr></thead><tbody><tr><td><strong>3a</strong></td><td>Client-Side</td><td>Klassischer REST-Endpoint, der ein Array zurueckgibt</td><td>Kleines-mittleres Dataset (&lt; einige Tausend Zeilen), maximale Einfachheit, 3rd-Party-API ohne serverseitige Kontrolle</td></tr><tr><td><strong>3b</strong></td><td>Server-Side REST Custom</td><td>Dein REST-Controller mit Paging-/Sortier-/Filter-Query-Params</td><td>Grosses Dataset, du willst totale Kontrolle ueber die Query; der End-Dev hat bereits einen existierenden Endpoint mit offset/limit usw.</td></tr><tr><td><strong>3c</strong></td><td>Server-Side OData</td><td>OData-Endpoint des Frameworks (<code>/odata/&lt;EntitySet&gt;</code>)</td><td>Grosses Dataset vom Framework automatisch als OData-Set exponiert; null Backend-Code; Standard-Syntax kompatibel mit anderen Clients</td></tr></tbody></table><h2>Siehe auch</h2><ul><li><a href=\"./pattern-full-autogeneration.md\">Pattern 1 — Full Autogeneration</a>: wenn die Standard-UX genuegt und die Daten im gescaffoldeten Modell existieren.</li><li><a href=\"./pattern-framework-data-custom-comp.md\">Pattern 2 — Framework-Daten + Custom-Komponente</a>: umgekehrt (Custom-UI, Framework-Daten).</li><li><a href=\"./pattern-full-custom.md\">Pattern 4 — Full Custom</a>: wenn du nicht einmal die List-Grid brauchst.</li><li><a href=\"./pattern-framework-manual.md\">Pattern 5 — Framework-Komponente + Framework-Daten (manuelle Montage)</a>: &quot;Framework&quot;-Variante dieses Patterns: gleiche manuelle Widget-Komposition, aber metadata-gesteuerter Data-Layer statt Custom-Backend.</li></ul>",
28428
28452
  "codeSamples": [
28429
28453
  {
28430
28454
  "id": "code_1",
@@ -28447,24 +28471,30 @@ const frameworkDocsContent = {
28447
28471
  {
28448
28472
  "id": "code_4",
28449
28473
  "title": "Snippet 4",
28450
- "language": "ts",
28451
- "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() zum Erben der Defaults (md_sortable usw.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- Schluessel: erzwingt In-Memory\r\ntableMeta.md_pageable = true; // aktiviert UI-Pagination\r\ntableMeta.md_pagesize = 10; // Zeilen pro Seite\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
28474
+ "language": "csharp",
28475
+ "code": "await using var cn = new OracleConnection(cs);\nawait cn.OpenAsync();\n\n// 1) die Session auf das Datenschema bringen (das macht sonst das Framework)\nawait using (var set = new OracleCommand(\"ALTER SESSION SET CURRENT_SCHEMA = MEIN_SCHEMA\", cn))\n await set.ExecuteNonQueryAsync();\n\n// 2) oder die Tabellen qualifizieren: SELECT ... FROM MEIN_SCHEMA.PRODUCT"
28452
28476
  },
28453
28477
  {
28454
28478
  "id": "code_5",
28455
28479
  "title": "Snippet 5",
28456
28480
  "language": "ts",
28457
- "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Verwende die absolute Backend-URL oder `WtoolboxService.appSettings.api_url`,\r\n // wenn dein Frontend-Dev-Server nicht mit dem Backend co-located ist (typischer Fall\r\n // Angular `:4200` + ASP.NET `:5000` ohne proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations bleibt `true` (Default) -> p-table in [lazy]=\"true\",\r\n // kein interner Sort/Filter/Paging: wir laden die richtige Seite vom Server neu.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
28481
+ "code": "const meta = new MetaInfo();\r\n// new MetadatiTabella() zum Erben der Defaults (md_sortable usw.).\r\nconst tableMeta = new MetadatiTabella();\r\ntableMeta.md_server_side_operations = false; // <-- Schluessel: erzwingt In-Memory\r\ntableMeta.md_pageable = true; // aktiviert UI-Pagination\r\ntableMeta.md_pagesize = 10; // Zeilen pro Seite\r\nmeta.tableMetadata = tableMeta;\r\nmeta.columnMetadata = [ /* ... */ ];\r\n\r\nthis.ds.metaInfo = meta;\r\nthis.ds.fetchInfo$.next({\r\n resultInfo: { dato: rows, totalRowCount: rows.length, current: {} } as any,\r\n metaInfo: meta,\r\n filterDescriptor: {}\r\n});"
28458
28482
  },
28459
28483
  {
28460
28484
  "id": "code_6",
28461
28485
  "title": "Snippet 6",
28462
- "language": "csharp",
28463
- "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) Filter vor dem Count: total muss POST-filter / PRE-page sein,\r\n // sonst zeigt der UI-Pager leere Seiten.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) Sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) Page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
28486
+ "language": "ts",
28487
+ "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient, HttpParams } from '@angular/common/http';\r\nimport { BehaviorSubject } from 'rxjs';\r\nimport { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';\r\n\r\ninterface InventoryResponse { rows: any[]; total: number; }\r\n\r\n@Component({\r\n selector: 'app-server-side-grid',\r\n standalone: true,\r\n imports: [DataSourceComponent, ListGridComponent],\r\n template: `\r\n <wuic-data-source #ds></wuic-data-source>\r\n <wuic-list-grid\r\n [hardcodedDatasource]=\"ds\"\r\n (onPaging)=\"reloadFromServer()\"\r\n (onSorting)=\"reloadFromServer()\"\r\n (onFiltering)=\"reloadFromServer()\"></wuic-list-grid>\r\n `\r\n})\r\nexport class ServerSideGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private meta!: MetaInfo;\r\n private fd!: { [k: string]: BehaviorSubject<any> };\r\n\r\n ngAfterViewInit() {\r\n this.initMeta();\r\n this.reloadFromServer();\r\n }\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = this.ds?.filterInfo?.filters?.[0];\r\n\r\n let params = new HttpParams()\r\n .set('offset', String((page - 1) * pageSize))\r\n .set('limit', String(pageSize));\r\n\r\n if (sort?.field) {\r\n params = params.set('sortField', String(sort.field))\r\n .set('sortDir', String(sort.dir || 'asc'));\r\n }\r\n if (filter?.field && filter?.value) {\r\n params = params.set('filterField', String(filter.field))\r\n .set('filterValue', String(filter.value))\r\n .set('filterOp', String(filter.operatore || 'contains'));\r\n }\r\n\r\n // Verwende die absolute Backend-URL oder `WtoolboxService.appSettings.api_url`,\r\n // wenn dein Frontend-Dev-Server nicht mit dem Backend co-located ist (typischer Fall\r\n // Angular `:4200` + ASP.NET `:5000` ohne proxy.conf.json).\r\n this.http.get<InventoryResponse>('http://localhost:5000/api/samples/inventory', { params })\r\n .subscribe(res => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: res.rows, totalRowCount: res.total, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n\r\n private initMeta(): void {\r\n const meta = new MetaInfo();\r\n const tableMeta = new MetadatiTabella('inventory');\r\n // md_server_side_operations bleibt `true` (Default) -> p-table in [lazy]=\"true\",\r\n // kein interner Sort/Filter/Paging: wir laden die richtige Seite vom Server neu.\r\n tableMeta.md_pageable = true;\r\n tableMeta.md_pagesize = 10;\r\n meta.tableMetadata = tableMeta;\r\n\r\n const col = (n: string, p: Partial<MetadatiColonna>) => Object.assign(new MetadatiColonna(n), p);\r\n meta.columnMetadata = [\r\n col('id', { mc_display_string_in_view: 'ID', mc_ui_column_type: 'number', mc_is_primary_key: true }),\r\n col('name', { mc_display_string_in_view: 'Product', mc_ui_column_type: 'text', mc_show_in_filters: true })\r\n ];\r\n\r\n const fd: { [k: string]: BehaviorSubject<any> } = {};\r\n meta.columnMetadata.forEach(c => fd[c.mc_nome_colonna] = new BehaviorSubject<any>(null));\r\n\r\n this.meta = meta;\r\n this.fd = fd;\r\n this.ds.metaInfo = meta;\r\n this.ds.filterDescriptor = fd;\r\n }\r\n}"
28464
28488
  },
28465
28489
  {
28466
28490
  "id": "code_7",
28467
28491
  "title": "Snippet 7",
28492
+ "language": "csharp",
28493
+ "code": "[HttpGet(\"inventory\")]\r\npublic IActionResult GetInventory(\r\n int offset = 0,\r\n int limit = 10,\r\n string? sortField = null,\r\n string? sortDir = \"asc\",\r\n string? filterField = null,\r\n string? filterValue = null,\r\n string? filterOp = \"contains\")\r\n{\r\n IEnumerable<InventoryItem> q = _inventory;\r\n\r\n // 1) Filter vor dem Count: total muss POST-filter / PRE-page sein,\r\n // sonst zeigt der UI-Pager leere Seiten.\r\n if (!string.IsNullOrWhiteSpace(filterField) && !string.IsNullOrWhiteSpace(filterValue))\r\n q = ApplyInventoryFilter(q, filterField, filterValue, filterOp ?? \"contains\");\r\n\r\n var total = q.Count();\r\n\r\n // 2) Sort\r\n if (!string.IsNullOrWhiteSpace(sortField))\r\n q = ApplyInventorySort(q, sortField, (sortDir ?? \"asc\").ToLowerInvariant());\r\n\r\n // 3) Page\r\n var rows = q.Skip(Math.Max(0, offset)).Take(Math.Max(1, limit)).ToList();\r\n\r\n return Ok(new { rows, total });\r\n}"
28494
+ },
28495
+ {
28496
+ "id": "code_8",
28497
+ "title": "Snippet 8",
28468
28498
  "language": "ts",
28469
28499
  "code": "import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';\r\nimport { HttpClient } from '@angular/common/http';\r\nimport { BehaviorSubject, forkJoin } from 'rxjs';\r\nimport {\r\n DataProviderOdataService,\r\n DataSourceComponent, ListGridComponent,\r\n FilterInfo, MetaInfo, MetadatiColonna, MetadatiTabella,\r\n WtoolboxService\r\n} from 'wuic-framework-lib';\r\n\r\n@Component({ /* ... */ })\r\nexport class ODataCitiesGridComponent implements AfterViewInit {\r\n @ViewChild('ds') ds!: DataSourceComponent;\r\n private http = inject(HttpClient);\r\n private odataSrv = inject(DataProviderOdataService);\r\n // ... meta, fd init (wie 3b) ...\r\n\r\n reloadFromServer(): void {\r\n const page = Number(this.ds?.currentPage || 1);\r\n const pageSize = Number(this.ds?.pageSize || 10);\r\n const sort = this.ds?.sortInfo?.[0];\r\n const filter = (this.ds?.filterInfo || new FilterInfo('AND', [])) as FilterInfo;\r\n const base = String(WtoolboxService.appSettings.file_path || '').replace(/\\/$/, '');\r\n\r\n // 1) URL mit $filter bereits vom Framework uebersetzt:\r\n // z.B. '/odata/Cities?$filter=contains(cityName%2C%20%27Mil%27)'\r\n const baseRelUrl = this.odataSrv.filterInfoToOdata(filter, 'Cities');\r\n const sep = baseRelUrl.includes('?') ? '&' : '?';\r\n\r\n // 2) $top/$skip/$orderby als zusaetzliche Query-Params hinzufuegen\r\n const pageExtras = [ `$top=${pageSize}`, `$skip=${(page - 1) * pageSize}` ];\r\n if (sort?.field) {\r\n pageExtras.push(`$orderby=${encodeURIComponent(`${sort.field} ${sort.dir || 'asc'}`)}`);\r\n }\r\n const pageUrl = `${base}${baseRelUrl}${sep}${pageExtras.join('&')}`;\r\n\r\n // 3) Count-Query: selber Filter, $select=cityID fuer minimales Payload\r\n const countUrl = `${base}${baseRelUrl}${sep}$select=cityID`;\r\n\r\n forkJoin({\r\n page: this.http.get<any[]>(pageUrl),\r\n all: this.http.get<any[]>(countUrl)\r\n }).subscribe(({ page, all }) => {\r\n this.ds.fetchInfo$.next({\r\n resultInfo: { dato: page, totalRowCount: all.length, current: {} } as any,\r\n metaInfo: this.meta,\r\n filterDescriptor: this.fd\r\n });\r\n });\r\n }\r\n}"
28470
28500
  }
@@ -35258,4 +35288,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
35258
35288
  }] } });
35259
35289
 
35260
35290
  export { FrameworkDocsComponent };
35261
- //# sourceMappingURL=wuic-framework-lib-framework-docs.component-B2AdE4N0.mjs.map
35291
+ //# sourceMappingURL=wuic-framework-lib-framework-docs.component-DQEh15Vz.mjs.map