@noeva/analytics-core 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +23 -2
  3. package/cli.js +132 -0
  4. package/contenuto/assets/accessibile.js +101 -0
  5. package/contenuto/assets/agenti.js +110 -0
  6. package/contenuto/assets/animazione.js +156 -0
  7. package/contenuto/assets/base.css +369 -0
  8. package/contenuto/assets/chat.js +374 -0
  9. package/contenuto/assets/commento.js +474 -0
  10. package/contenuto/assets/formati.js +100 -0
  11. package/contenuto/assets/guida.js +12 -0
  12. package/contenuto/assets/inferenza.js +291 -0
  13. package/contenuto/assets/lingua.js +82 -0
  14. package/contenuto/assets/ordina.js +281 -0
  15. package/contenuto/assets/presentazione.js +743 -0
  16. package/contenuto/assets/sapere.js +55 -0
  17. package/contenuto/assets/servizio.js +116 -0
  18. package/contenuto/assets/snapshot.js +100 -0
  19. package/contenuto/assets/tema.js +675 -0
  20. package/contenuto/lib/ai/sql_vivo.py +72 -0
  21. package/contenuto/lib/dashboard.sh +195 -0
  22. package/contenuto/lib/mcp-locale/ambiente/.python-version +1 -0
  23. package/contenuto/lib/mcp-locale/ambiente/pyproject.toml +23 -0
  24. package/contenuto/lib/mcp-locale/ambiente/uv.lock +1103 -0
  25. package/contenuto/lib/mcp-locale/cartella/CLAUDE.md +52 -0
  26. package/contenuto/lib/mcp-locale/cartella/LEGGIMI.md +163 -0
  27. package/contenuto/lib/mcp-locale/cartella/imposta-password.ps1 +40 -0
  28. package/contenuto/lib/mcp-locale/cartella/installa.ps1 +286 -0
  29. package/contenuto/lib/mcp-locale/costruisci.py +495 -0
  30. package/contenuto/lib/mcp-locale/prova.py +93 -0
  31. package/contenuto/lib/mcp-locale/server/modello_locale.py +167 -0
  32. package/contenuto/lib/mcp-locale/server/noeva_mcp.py +203 -0
  33. package/contenuto/lib/modello/consegna.py +134 -0
  34. package/contenuto/lib/modello/pubblica.py +131 -0
  35. package/contenuto/lib/snapshot/ai.py +112 -0
  36. package/contenuto/lib/snapshot/ascolta.py +213 -0
  37. package/contenuto/lib/snapshot/esporta.py +466 -0
  38. package/contenuto/lib/snapshot/impacchetta.py +184 -0
  39. package/contenuto/lib/snapshot/kb.py +95 -0
  40. package/contenuto/lib/snapshot/manifesto.py +46 -0
  41. package/contenuto/lib/snapshot/memoria.py +180 -0
  42. package/contenuto/lib/snapshot/noeva_snapshot.py +107 -0
  43. package/contenuto/lib/snapshot/pubblica.py +231 -0
  44. package/contenuto/lib/snapshot/worker/installa.ps1 +340 -0
  45. package/core.js +294 -0
  46. package/kit/blocco-agents.md +12 -0
  47. package/kit/skills/progetto/SKILL.md +59 -0
  48. package/modello/contenitore/Dockerfile +58 -0
  49. package/modello/repository/AGENTS.md +3 -0
  50. package/modello/repository/CLAUDE.md +1 -0
  51. package/modello/repository/README.md +9 -0
  52. package/modello/repository/analytics.json +4 -0
  53. package/modello/repository/docker-compose.yml +32 -0
  54. package/modello/repository/github/workflows/ci.yml +77 -0
  55. package/modello/repository/github/workflows/rilascio-windows.yml +207 -0
  56. package/modello/repository/gitignore +24 -0
  57. package/modello/repository/package.json +15 -0
  58. package/package.json +28 -4
  59. package/pubblica.js +205 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # @noeva/analytics-core — registro delle modifiche
2
+
3
+ Le versioni del pacchetto che i repository dei progetti installano (ADR 0005). Prima di
4
+ `analytics upgrade` si legge cosa cambia fra la versione installata e la nuova.
5
+
6
+ ## 0.3.0 — 6 novembre 2026
7
+
8
+ Il repository del progetto basta a se stesso (ADR 0005): pubblica pagine e modello e
9
+ rilascia i due eseguibili Windows senza il monorepo.
10
+
11
+ - `analytics pubblica` e `analytics modello` chiedono il biglietto all'API NOEVA con la
12
+ chiave di un membro del workspace (`NOEVA_API_KEY` o `~/.noeva-api-key`) quando il
13
+ segreto del contenitore non c'e'. Serve l'API con il campo `tipo` del biglietto
14
+ (noeva-server-api, `POST /api/analytics/biglietto-dati`).
15
+ - `.github/workflows/ci.yml` (gestito): il job `pubblica` porta online pagine e modello a
16
+ ogni push su `main` che passa la verifica. Si accende con il secret `NOEVA_API_KEY` e
17
+ la variabile `ANALYTICS_PUBBLICA=si`.
18
+ - `.github/workflows/rilascio-windows.yml` (gestito): la cartella MCP per i PC e
19
+ l'eseguibile degli snapshot, costruiti e provati su Windows, e le release nel repository
20
+ del progetto. Le ricette sono nel pacchetto (`contenuto/lib/mcp-locale`,
21
+ `contenuto/lib/snapshot`) e leggono `consegna.yml`, che ora e' la configurazione di
22
+ consegna intera: cliente, connessione senza password, verifiche, trasformazioni.
23
+ - La pubblicazione dei dati (`aggiorna.sh --pubblica`) trova il workspace in
24
+ `analytics.json`: fuori dal monorepo il registro non c'e'.
25
+ - `.gitignore` del modello: `dist/`, dove escono le cartelle Windows.
26
+
27
+ ## 0.2.0 — 6 novembre 2026
28
+
29
+ - `analytics pubblica --url <url>`: la `apps/` del commit (HEAD, non il disco) sul
30
+ contenitore, in R2, meno quello che `apps/.assetsignore` esclude (ADR 0005, punto 3).
31
+ `--prova` elenca soltanto. In prova il biglietto si firma col segreto del contenitore
32
+ (`SEGRETO_DATI` o `~/.segreto-dati`); quello rilasciato dall'API NOEVA alla CI non c'e'
33
+ ancora.
34
+ - `analytics modello --url <url>`: il modello del commit (wren_project.yml, relationships.yml,
35
+ models/, knowledge/), con le trasformazioni di `consegna.yml`, compilato come `wren context
36
+ build` e pubblicato su R2 per il servizio locale (ADR 0005, punto 6). Vuole python3 con
37
+ pyyaml e wrenai (il contenitore di sviluppo li ha); `--prova` compila soltanto.
38
+ - `analytics.json` accetta `workspace`: il workspace NOEVA che possiede il progetto, che il
39
+ biglietto nomina.
40
+
41
+ ## 0.1.0 — 6 novembre 2026
42
+
43
+ Prima versione, per la prima prova dell'ADR 0005.
44
+
45
+ - `analytics init`, `ci`, `upgrade [--check]`, `agent sync`, `percorso`.
46
+ - File gestiti: i moduli di pagina in `apps/assets/` (da `lib/pagina` e `snapshot.js`),
47
+ `.github/workflows/ci.yml`, la skill `progetto` in `.claude/skills/noeva-analytics-progetto/`
48
+ e il blocco NOEVA Analytics di `AGENTS.md`.
49
+ - `analytics ci`: struttura dello standard (§2), `.assetsignore`, `controlli` di
50
+ `snapshot.yml`, file gestiti, nessun dato o credenziale in git, nessun riferimento
51
+ fuori dal prefisso del progetto.
52
+ - Gli strumenti dello snapshot (`contenuto/lib/`) per `aggiorna.sh`, e il Dockerfile del
53
+ contenitore di sviluppo (`modello/contenitore/`).
package/README.md CHANGED
@@ -1,3 +1,24 @@
1
- # Temporary Holding Version
1
+ # @noeva/analytics-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Il pacchetto che il repository di ogni progetto installa (ADR 0005): la CLI `analytics`,
4
+ i moduli di pagina, gli strumenti dello snapshot, il kit dell'agente e il modello del
5
+ repository. La forma e' quella di `@noeva/etl-core` (noeva-etl, ADR-0093 e 0094).
6
+
7
+ La fonte resta `lib/`: `prepara.js` ne copia in `contenuto/` (gitignorata) i file che il
8
+ pacchetto porta, a ogni `npm pack` e a ogni prova. Questo README non va nel pacchetto.
9
+
10
+ ```bash
11
+ node lib/core/prepara.js && node --test lib/core/test/*.test.js # le prove, anche in CI
12
+ cd lib/core && npm pack --dry-run # cosa pubblicherebbe
13
+ python3 lib/rilascio/estrai-progetto.py <id> <cartella vuota> # un progetto nel suo repository
14
+ ```
15
+
16
+ - **Il pacchetto non nomina clienti**: `test/nomi.test.js` lo prova contro il registro.
17
+ Un commento in `lib/pagina` o `lib/snapshot` che cita un cliente arriva a tutti.
18
+ - **La CI del repository nuovo puo' non partire**: con OIKI (6/11/2026) GitHub ha registrato
19
+ `ci.yml`, arrivato col push che crea il repository, ma non partiva su push e PR, solo a mano.
20
+ Si e' sbloccato al primo push di un commit che cambiava `ci.yml`, anche su un branch di
21
+ prova poi cancellato. Dopo il primo push si controlla che la PR abbia il check `verifica`.
22
+ - **Una versione nuova** si scrive in `package.json` e in `CHANGELOG.md`, che i progetti
23
+ leggono prima di aggiornare. Pubblicare su npm lo fa Stefano Denti, come i rilasci
24
+ (`AGENTS.md`, «Chi rilascia»).
package/cli.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // La CLI del pacchetto @noeva/analytics-core, nel repository di un progetto:
3
+ //
4
+ // analytics init <id> [cartella] il repository di un progetto nuovo, da una cartella vuota
5
+ // analytics ci la verifica dello standard (la stessa della CI)
6
+ // analytics upgrade --check la versione installata, l'ultima, i file gestiti cambiati
7
+ // analytics upgrade aggiorna il pacchetto e riscrive i file gestiti
8
+ // analytics agent sync riscrive solo il kit dell'agente
9
+ // analytics pubblica --url <url> la apps/ di HEAD sul contenitore, in R2 (--prova: elenca soltanto)
10
+ // analytics modello --url <url> il modello di HEAD, compilato, sul contenitore in R2 (python3, wren)
11
+ // analytics percorso dove sta il contenuto del pacchetto (per aggiorna.sh)
12
+ // analytics versione
13
+ //
14
+ // Le funzioni stanno in core.js; qui solo argomenti, stampa e codice d'uscita.
15
+
16
+ import { execFileSync } from 'node:child_process';
17
+ import { join, resolve } from 'node:path';
18
+ import {
19
+ ErroreConfig, PACCHETTO, VERSIONE, derive, init, leggiConfig, scriviGestiti, trovaRadice,
20
+ piuNuova, ultimaVersione, verifica,
21
+ } from './core.js';
22
+ import { pubblica } from './pubblica.js';
23
+
24
+ const verde = (s) => `\x1b[0;32m${s}\x1b[0m`;
25
+ const rosso = (s) => `\x1b[0;31m${s}\x1b[0m`;
26
+ const giallo = (s) => `\x1b[0;33m${s}\x1b[0m`;
27
+
28
+ function elenca(titolo, voci) {
29
+ if (!voci.length) return;
30
+ console.log(titolo);
31
+ for (const v of voci) console.log(` ${v}`);
32
+ }
33
+
34
+ async function principale(argv) {
35
+ const [comando, ...resto] = argv;
36
+ switch (comando) {
37
+ case 'init': {
38
+ const [id, cartella = '.'] = resto;
39
+ const scritti = init(resolve(cartella), id);
40
+ console.log(verde(`noeva-analytics-${id} creato in ${resolve(cartella)} (@noeva/analytics-core ${VERSIONE}, ${scritti.length} file gestiti)`));
41
+ console.log('Poi: npm install, la cartella del progetto, npx analytics ci.');
42
+ return 0;
43
+ }
44
+ case 'ci': {
45
+ const radice = trovaRadice();
46
+ const config = leggiConfig(radice);
47
+ const { errori, avvisi } = verifica(radice, config);
48
+ console.log(`analytics ci — ${config.progetto}, @noeva/analytics-core ${VERSIONE}`);
49
+ elenca(giallo('avvisi'), avvisi);
50
+ elenca(rosso('errori'), errori);
51
+ const ultima = await ultimaVersione();
52
+ if (piuNuova(ultima, VERSIONE)) console.log(giallo(`c'e' la versione ${ultima}: npx analytics upgrade --check`));
53
+ console.log(errori.length ? rosso(`KO: ${errori.length} errori`) : verde('ok'));
54
+ return errori.length ? 1 : 0;
55
+ }
56
+ case 'upgrade': {
57
+ const radice = trovaRadice();
58
+ const config = leggiConfig(radice);
59
+ if (resto.includes('--check')) {
60
+ const ultima = await ultimaVersione();
61
+ console.log(`installata ${VERSIONE}, ultima ${ultima ?? 'non raggiungibile o non pubblicata'}`);
62
+ if (piuNuova(ultima, VERSIONE)) console.log(`il registro delle modifiche: npm view @noeva/analytics-core@${ultima} — leggerlo prima di aggiornare`);
63
+ const d = derive(radice, config);
64
+ elenca(giallo('file gestiti diversi dal pacchetto installato'), d.map((x) => `${x.stato}: ${x.percorso}`));
65
+ if (!d.length) console.log(verde('file gestiti allineati'));
66
+ return 0;
67
+ }
68
+ if (!resto.includes('--solo-file')) {
69
+ // Il pacchetto nuovo, poi la sua CLI scrive i file: questa e' ancora la
70
+ // versione vecchia, e scriverebbe i file di prima.
71
+ execFileSync('npm', ['install', '--save-dev', '@noeva/analytics-core@latest'], { cwd: radice, stdio: 'inherit' });
72
+ execFileSync(process.execPath, [join(radice, 'node_modules', '@noeva', 'analytics-core', 'cli.js'), 'upgrade', '--solo-file'], { cwd: radice, stdio: 'inherit' });
73
+ return 0;
74
+ }
75
+ elenca(`riscritti alla versione ${VERSIONE}`, scriviGestiti(radice, config));
76
+ console.log(verde('file gestiti allineati'));
77
+ return 0;
78
+ }
79
+ case 'agent': {
80
+ if (resto[0] !== 'sync') break;
81
+ const radice = trovaRadice();
82
+ elenca('kit dell\'agente riscritto', scriviGestiti(radice, leggiConfig(radice), { soloKit: true }));
83
+ console.log(verde('kit allineato'));
84
+ return 0;
85
+ }
86
+ case 'pubblica': {
87
+ const radice = trovaRadice();
88
+ const config = leggiConfig(radice);
89
+ const valore = (nome) => { const i = resto.indexOf(nome); return i >= 0 ? resto[i + 1] : undefined; };
90
+ const prova = resto.includes('--prova');
91
+ const esito = await pubblica(radice, config, {
92
+ url: valore('--url') ?? process.env.NOEVA_ANALYTICS_URL,
93
+ workspace: valore('--workspace') ?? config.workspace,
94
+ prova,
95
+ stampa: console.log,
96
+ });
97
+ const quanto = `${esito.file} file, ${Math.round(esito.byte / 1024)} kB`;
98
+ console.log(prova ? `versione ${esito.versione}: ${quanto} (prova, niente caricato)` : verde(`pubblicata ${esito.versione}: ${quanto}`));
99
+ return 0;
100
+ }
101
+ case 'modello': {
102
+ // Compilare il modello e' di wren, che e' Python: lo fa lib/modello/pubblica.py
103
+ // del pacchetto, con il python3 dell'ambiente (il contenitore di sviluppo, la CI).
104
+ const radice = trovaRadice();
105
+ const script = join(PACCHETTO, 'contenuto', 'lib', 'modello', 'pubblica.py');
106
+ try {
107
+ execFileSync(process.env.PYTHON || 'python3', [script, radice, ...resto], { stdio: 'inherit' });
108
+ } catch (e) {
109
+ return e.status ?? 1;
110
+ }
111
+ return 0;
112
+ }
113
+ case 'percorso':
114
+ console.log(join(PACCHETTO, 'contenuto'));
115
+ return 0;
116
+ case 'versione':
117
+ case '--version':
118
+ console.log(VERSIONE);
119
+ return 0;
120
+ default:
121
+ }
122
+ console.error('uso: analytics init <id> [cartella] | ci | upgrade [--check] | agent sync | pubblica --url <url> [--prova] | modello --url <url> [--prova] | percorso | versione');
123
+ return 2;
124
+ }
125
+
126
+ try {
127
+ process.exitCode = await principale(process.argv.slice(2));
128
+ } catch (e) {
129
+ if (!(e instanceof ErroreConfig)) throw e;
130
+ console.error(rosso(e.message));
131
+ process.exitCode = 2;
132
+ }
@@ -0,0 +1,101 @@
1
+ // Modulo condiviso delle dashboard NOEVA Analytics. La sorgente e' lib/pagina/accessibile.js;
2
+ // la copia in apps/assets/ la scrive lib/snapshot/installa.sh --pagina. Non modificare la copia.
3
+ /* Accessibilita' comune a tutte le dashboard (WCAG 2.2 AA).
4
+ *
5
+ * rendiAccessibile(); // una volta, all'avvio della pagina
6
+ *
7
+ * Le pagine si ridisegnano a ogni filtro: qui un osservatore rimette a posto
8
+ * dopo ogni ridisegno quello che il disegno non fa da se'.
9
+ *
10
+ * - "Vai ai contenuti": il primo Tab salta barra e filtri (2.4.1).
11
+ * - La barra in alto e' il banner della pagina, e se la pagina passa da h1 a
12
+ * h3 c'e' un h2 per i lettori di schermo (1.3.1).
13
+ * - Le intestazioni ordinabili si raggiungono con Tab e si attivano con Invio
14
+ * o Spazio, come il clic (2.1.1).
15
+ * - Ogni altra tabella si ordina cliccando l'intestazione (ordina.js), con
16
+ * aria-sort e la tastiera; l'ordine scelto resta dopo un ridisegno.
17
+ * - Ogni grafico ha i suoi numeri anche in tabella: i valori che il grafico
18
+ * mostra solo al passaggio del mouse (data-et, data-v sulle colonne) si
19
+ * leggono in "I numeri del grafico", da tastiera e con il lettore (1.1.1).
20
+ */
21
+
22
+ import { t, LOCALE } from "./lingua.js";
23
+ import { montaOrdinamento } from "./ordina.js";
24
+
25
+ export function rendiAccessibile() {
26
+ const main = document.querySelector("main");
27
+ if (main && !main.id) main.id = "contenuto";
28
+ if (main && !document.querySelector(".salta")) {
29
+ const a = document.createElement("a");
30
+ a.className = "salta"; a.href = `#${main.id}`; a.textContent = t("Vai ai contenuti");
31
+ a.onclick = (e) => {
32
+ e.preventDefault();
33
+ const dove = document.querySelector("#pagina:not([hidden])") || main;
34
+ dove.setAttribute("tabindex", "-1"); dove.focus();
35
+ };
36
+ document.body.prepend(a);
37
+ }
38
+ const barra = document.querySelector(".topbar");
39
+ if (barra && !barra.getAttribute("role")) barra.setAttribute("role", "banner");
40
+
41
+ const pagina = document.getElementById("pagina");
42
+ if (pagina && !pagina.querySelector("h2") && pagina.querySelector("h3")) {
43
+ const h2 = document.createElement("h2");
44
+ h2.className = "sr-only";
45
+ h2.textContent = t("{titolo}: indicatori e dettagli", { titolo: (document.querySelector(".topbar h1")?.textContent || "Dashboard").replace(/^[\s·]+/, "") });
46
+ pagina.prepend(h2);
47
+ }
48
+
49
+ let inCorso = false;
50
+ const ordinaTabelle = montaOrdinamento({ t, locale: LOCALE });
51
+ const sistema = () => {
52
+ if (inCorso) return;
53
+ inCorso = true;
54
+ try { intestazioni(); numeriDeiGrafici(); ordinaTabelle(); } finally { inCorso = false; }
55
+ };
56
+ sistema();
57
+ new MutationObserver(() => queueMicrotask(sistema)).observe(document.body, { childList: true, subtree: true });
58
+ }
59
+
60
+ function intestazioni() {
61
+ for (const th of document.querySelectorAll("th[data-col]")) {
62
+ if (th.dataset.tastiera) continue;
63
+ th.dataset.tastiera = "si";
64
+ th.tabIndex = 0;
65
+ th.setAttribute("title", t("Ordina per questa colonna"));
66
+ th.addEventListener("keydown", (e) => {
67
+ if (e.key !== "Enter" && e.key !== " ") return;
68
+ e.preventDefault();
69
+ const col = th.dataset.col, tab = th.closest("table");
70
+ th.click();
71
+ // la tabella si ridisegna: il fuoco torna sulla stessa colonna
72
+ requestAnimationFrame(() => tab?.querySelector(`th[data-col="${CSS.escape(col)}"]`)?.focus());
73
+ });
74
+ }
75
+ }
76
+
77
+ const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
78
+
79
+ function numeriDeiGrafici() {
80
+ for (const svg of document.querySelectorAll("svg.grafico")) {
81
+ const colonne = [...svg.querySelectorAll(".hit[data-et][data-v]")];
82
+ const riquadro = svg.closest(".card");
83
+ if (!colonne.length || !riquadro) continue;
84
+ let det = riquadro.querySelector("details.numeri-auto");
85
+ // un grafico che ha gia' la sua tabella dei numeri non ne vuole una seconda
86
+ if (!det && riquadro.querySelector("details.numeri, details table")) continue;
87
+ const html = `<thead><tr><th>${t("Voce")}</th><th>${t("Valori")}</th></tr></thead><tbody>${colonne.map((h) =>
88
+ `<tr><td>${esc(h.dataset.et)}</td><td style="text-align:left;white-space:normal">${esc(h.dataset.v)}</td></tr>`).join("")}</tbody>`;
89
+ if (!det) {
90
+ det = document.createElement("details");
91
+ det.className = "numeri numeri-auto";
92
+ det.style.marginTop = "10px";
93
+ det.innerHTML = `<summary style="cursor:pointer;font-size:12px;color:var(--ink-3)">${t("I numeri del grafico")}</summary>
94
+ <div class="tabella-wrap"><table></table></div>`;
95
+ svg.after(det);
96
+ }
97
+ // confronto con l'ultima versione scritta, non con innerHTML: il browser la
98
+ // riscrive a modo suo, e l'osservatore girerebbe all'infinito
99
+ if (det._ultimo !== html) { det._ultimo = html; det.querySelector("table").innerHTML = html; }
100
+ }
101
+ }
@@ -0,0 +1,110 @@
1
+ // Modulo condiviso delle dashboard NOEVA Analytics. La sorgente e' lib/pagina/agenti.js;
2
+ // la copia in apps/assets/ la scrive lib/snapshot/installa.sh --pagina. Non modificare la copia.
3
+ /* La chat su NOEVA Agents (ADR 0006, in prova): agents UI in un iframe dentro il
4
+ * pannello della chat, che conserva le sue quattro viste.
5
+ *
6
+ * const a = await agentiDisponibili(); // null, o { agenti, token, ... }
7
+ * if (a) montaAgenti(contenitore, { pagina, accesso: a });
8
+ *
9
+ * Il Worker custodisce i token NOEVA dell'utente e da' alla pagina solo
10
+ * l'access token, da ai/token, con l'indirizzo di agents UI (AGENTI_URL). La
11
+ * pagina lo passa all'iframe con INIT e, prima che scada, con AUTH_REFRESH: lo
12
+ * stesso protocollo del client NOEVA. Progetto, pagina, lingua e lo stato della
13
+ * pagina (contesto() del commento) viaggiano in customMetadata, aggiornato con
14
+ * UPDATE_METADATA quando i filtri cambiano. I messaggi vanno solo all'origine
15
+ * di agents UI, e se ne ascoltano solo da li'.
16
+ */
17
+ import { contesto } from "./commento.js";
18
+ import { rotta } from "./servizio.js";
19
+ import { t, LINGUA } from "./lingua.js";
20
+ import { PROGETTO } from "./progetto.js";
21
+
22
+ const AGENTE = "analyticsChatAgent";
23
+ // Il token si chiede di nuovo un minuto prima del margine del Worker (5 minuti):
24
+ // arriva gia' rinnovato.
25
+ const ANTICIPO_MS = 6 * 60 * 1000;
26
+
27
+ let _accesso = null;
28
+ /** L'accesso ad agents UI, o null se il contenitore non lo offre (niente SESSIONI o AGENTI_URL). */
29
+ export function agentiDisponibili() {
30
+ _accesso ||= chiediToken().catch(() => null);
31
+ return _accesso;
32
+ }
33
+
34
+ async function chiediToken() {
35
+ const r = await fetch(rotta("ai/token"), { cache: "no-store" });
36
+ if (!r.ok) return null;
37
+ const j = await r.json();
38
+ return j?.token && j?.agenti ? j : null;
39
+ }
40
+
41
+ const tema = () => {
42
+ try { return { light: "light", dark: "dark" }[localStorage.getItem("noeva-tema")] || "system"; } catch { return "system"; }
43
+ };
44
+
45
+ export function montaAgenti(contenitore, { pagina, accesso, suggerimenti = [] }) {
46
+ const origine = new URL(accesso.agenti).origin;
47
+ const f = document.createElement("iframe");
48
+ f.src = accesso.agenti;
49
+ f.title = t("Chat sulla pagina");
50
+ f.className = "ch-agenti";
51
+ f.setAttribute("allow", "microphone; clipboard-write");
52
+ f.style.cssText = "flex: 1; width: 100%; border: 0; background: transparent;";
53
+ contenitore.append(f);
54
+
55
+ const manda = (messaggio) => f.contentWindow?.postMessage(messaggio, origine);
56
+ const metadata = () => ({ progetto: PROGETTO.id, pagina, lingua: LINGUA, contesto: contesto() });
57
+ let pronto = false, rinnovo = null;
58
+
59
+ function programmaRinnovo(scade) {
60
+ clearTimeout(rinnovo);
61
+ rinnovo = setTimeout(async () => {
62
+ const nuovo = await chiediToken().catch(() => null);
63
+ if (!nuovo) { location.reload(); return; } // sessione finita: il cancello porta al login
64
+ manda({ type: "AUTH_REFRESH", payload: { authToken: nuovo.token } });
65
+ programmaRinnovo(nuovo.scade);
66
+ }, Math.max(30_000, scade - Date.now() - ANTICIPO_MS));
67
+ }
68
+
69
+ addEventListener("message", (e) => {
70
+ if (e.origin !== origine || e.source !== f.contentWindow) return;
71
+ if (e.data?.type !== "READY" || pronto) return;
72
+ pronto = true;
73
+ manda({
74
+ type: "INIT",
75
+ payload: {
76
+ userId: accesso.utente,
77
+ workspaceId: accesso.workspace,
78
+ authToken: accesso.token,
79
+ agentId: AGENTE,
80
+ initialState: { agentId: AGENTE, viewChat: true },
81
+ // lo storico e' per progetto: chi segue due progetti ne ha due (ADR 0006)
82
+ threadsResource: `analytics:${PROGETTO.id}`,
83
+ theme: tema(),
84
+ language: LINGUA === "en" ? "en" : "it",
85
+ customChatTitle: PROGETTO.agenti?.titolo || t("Domande sulla pagina"),
86
+ ...(PROGETTO.agenti?.avatar ? { titleAvatar: PROGETTO.agenti.avatar } : {}),
87
+ welcomeMessage: t("Chiedi quello che la pagina non dice: rispondo dai numeri che vedi e, se serve, interrogo il modello dei dati con i filtri scelti."),
88
+ welcomeMessageQuickActions: suggerimenti.map((s) => ({ label: s, message: s })),
89
+ hideChatSettings: true,
90
+ showTokenUsage: true,
91
+ showSpinner: true,
92
+ disableAttachments: true,
93
+ customMetadata: metadata(),
94
+ },
95
+ });
96
+ programmaRinnovo(accesso.scade);
97
+ });
98
+
99
+ // I filtri cambiano: lo stato della pagina nuovo va al prossimo turno.
100
+ let attesa = null;
101
+ const aggiorna = () => {
102
+ if (!pronto) return;
103
+ clearTimeout(attesa);
104
+ attesa = setTimeout(() => manda({ type: "UPDATE_METADATA", payload: metadata() }), 400);
105
+ };
106
+ document.addEventListener("change", aggiorna, true);
107
+ document.addEventListener("click", (e) => { if (!contenitore.contains(e.target)) aggiorna(); }, true);
108
+ addEventListener("popstate", aggiorna);
109
+ return { aggiorna };
110
+ }
@@ -0,0 +1,156 @@
1
+ // Modulo condiviso delle dashboard NOEVA Analytics. La sorgente e' lib/pagina/animazione.js;
2
+ // la copia in apps/assets/ la scrive lib/snapshot/installa.sh --pagina. Non modificare la copia.
3
+ /* La regia delle slide quando si presentano dall'app (Presenta in presentazione.js).
4
+ *
5
+ * const a = animaSlide(box, tipo); // parte subito
6
+ * a.finisci(); // un tasto durante la regia: tutto al posto finale
7
+ * a.finita // true quando la regia e' conclusa
8
+ *
9
+ * Web Animations API, niente librerie: funziona anche senza rete. La regia e'
10
+ * fatta per far leggere, non per stupire: ogni cosa entra nell'ordine in cui si
11
+ * legge, i numeri grandi contano fino al valore, le barre crescono dal basso, le
12
+ * linee si disegnano, le righe di una tabella arrivano una alla volta e quella di
13
+ * cui si parla si accende per ultima. Tutto in meno di tre secondi.
14
+ *
15
+ * Con `prefers-reduced-motion` non si muove niente: la slide appare pronta.
16
+ */
17
+
18
+ import { LOCALE, SEPARATORI } from "./lingua.js";
19
+
20
+ const FUORI = "cubic-bezier(.22,.61,.36,1)"; // ease-out: parte deciso, arriva piano
21
+ const fermo = () => matchMedia("(prefers-reduced-motion: reduce)").matches;
22
+
23
+ /* Un numero scritto come lo scrive la pagina ("57,7 mln€", "−4,1%", "2.468", "52 mila";
24
+ * in inglese "57.7m€", "2,468") conta da zero al suo valore, tenendo segno,
25
+ * decimali, separatori e quello che segue. */
26
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ const RE_NUMERO = new RegExp(`^(\\s*)(−|-|\\+)?(\\d[\\d${escRe(SEPARATORI.migliaia)}]*)(${escRe(SEPARATORI.decimali)}\\d+)?([\\s\\S]*)$`);
28
+ function contaFinoA(el, durata, ritardo, registro) {
29
+ const testo = el.textContent;
30
+ const m = testo.match(RE_NUMERO);
31
+ if (!m) return;
32
+ const [, pre, segno = "", intero, dec = "", resto] = m;
33
+ const decimali = dec ? dec.length - 1 : 0;
34
+ const valore = parseFloat(intero.split(SEPARATORI.migliaia).join("") + (dec ? "." + dec.slice(1) : ""));
35
+ if (!isFinite(valore) || valore === 0) return;
36
+ const scrivi = (v) => { el.textContent = pre + segno + v.toLocaleString(LOCALE, { minimumFractionDigits: decimali, maximumFractionDigits: decimali }) + resto; };
37
+ const t0 = performance.now() + ritardo;
38
+ let vivo = true;
39
+ scrivi(0);
40
+ const passo = (ora) => {
41
+ if (!vivo) return;
42
+ const x = Math.min(Math.max((ora - t0) / durata, 0), 1);
43
+ const e = 1 - Math.pow(1 - x, 3);
44
+ scrivi(Math.round(valore * e * 10 ** decimali) / 10 ** decimali);
45
+ if (x < 1) requestAnimationFrame(passo); else vivo = false;
46
+ };
47
+ requestAnimationFrame(passo);
48
+ registro.push({ finish: () => { vivo = false; scrivi(valore); }, get playState() { return vivo ? "running" : "finished"; } });
49
+ }
50
+
51
+ const entra = (el, { ritardo = 0, durata = 520, da = "translateY(18px)" } = {}, registro) => {
52
+ const a = el.animate([{ opacity: 0, transform: da }, { opacity: 1, transform: "none" }],
53
+ { duration: durata, delay: ritardo, easing: FUORI, fill: "backwards" });
54
+ registro.push(a);
55
+ return a;
56
+ };
57
+
58
+ const appare = (el, ritardo, registro, durata = 400) => {
59
+ registro.push(el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: durata, delay: ritardo, easing: "ease-out", fill: "backwards" }));
60
+ };
61
+
62
+ /* Il grafico della dashboard, clonato nella slide: barre dal basso, linee disegnate, punti che spuntano. */
63
+ function animaGrafico(svg, t0, registro) {
64
+ const barre = [...svg.querySelectorAll("rect")].filter((r) => !r.matches(".hit, .sel, .griglia rect"));
65
+ barre.forEach((r, i) => {
66
+ r.style.transformBox = "fill-box"; r.style.transformOrigin = "50% 100%";
67
+ registro.push(r.animate([{ transform: "scaleY(0.001)" }, { transform: "scaleY(1)" }],
68
+ { duration: 650, delay: t0 + Math.min(i, 40) * 18, easing: FUORI, fill: "backwards" }));
69
+ });
70
+ const linee = [...svg.querySelectorAll("path, polyline")].filter((p) => {
71
+ const cs = getComputedStyle(p); return cs.stroke !== "none" && cs.fill === "none";
72
+ });
73
+ const tLinee = t0 + 350;
74
+ linee.forEach((p, i) => {
75
+ const cs = getComputedStyle(p);
76
+ if (cs.strokeDasharray !== "none" || typeof p.getTotalLength !== "function") { appare(p, tLinee + i * 120, registro, 600); return; }
77
+ const L = p.getTotalLength();
78
+ p.style.strokeDasharray = `${L}`;
79
+ const a = p.animate([{ strokeDashoffset: L }, { strokeDashoffset: 0 }], { duration: 1100, delay: tLinee + i * 120, easing: FUORI, fill: "backwards" });
80
+ a.onfinish = () => { p.style.strokeDasharray = ""; };
81
+ registro.push(a);
82
+ });
83
+ // aree piene (es. sotto una linea) e punti
84
+ [...svg.querySelectorAll("path, polygon")].filter((p) => getComputedStyle(p).fill !== "none" && !linee.includes(p))
85
+ .forEach((p) => appare(p, t0 + 200, registro, 700));
86
+ [...svg.querySelectorAll("circle")].forEach((c, i) => {
87
+ c.style.transformBox = "fill-box"; c.style.transformOrigin = "center";
88
+ registro.push(c.animate([{ transform: "scale(0)", opacity: 0 }, { transform: "scale(1)", opacity: 1 }],
89
+ { duration: 300, delay: tLinee + 500 + Math.min(i, 30) * 40, easing: FUORI, fill: "backwards" }));
90
+ });
91
+ // le etichette degli assi arrivano con la griglia, subito e piano
92
+ [...svg.querySelectorAll("text")].forEach((t) => appare(t, t0, registro, 500));
93
+ const evid = svg.querySelector(".hit.evid");
94
+ if (evid) registro.push(evid.animate([{ opacity: 0 }, { opacity: 0.14 }, { opacity: 0.4 }, { opacity: 0.14 }],
95
+ { duration: 900, delay: t0 + 1300, easing: "ease-in-out", fill: "backwards" }));
96
+ return t0 + 1300;
97
+ }
98
+
99
+ /* La tabella: una riga alla volta, quella indicata si accende per ultima. */
100
+ function animaTabella(tab, t0, registro) {
101
+ const righe = [...tab.querySelectorAll("tr")];
102
+ const passo = righe.length > 8 ? 35 : 50;
103
+ righe.forEach((tr, i) => entra(tr, { ritardo: t0 + i * passo, durata: 380, da: "translateY(6px)" }, registro));
104
+ const fine = t0 + righe.length * passo + 200;
105
+ const evid = tab.querySelector("tr.evid");
106
+ if (evid) registro.push(evid.animate([{ outline: "0 solid transparent" }, { outline: "3px solid var(--primary)", offset: .4 }, { outline: "3px solid transparent" }],
107
+ { duration: 900, delay: fine, easing: "ease-in-out", fill: "backwards" }));
108
+ // i punti partono mentre la riga si accende: la regia resta sotto i tre secondi
109
+ return fine + 150;
110
+ }
111
+
112
+ export function animaSlide(box, tipo) {
113
+ const registro = [];
114
+ const esito = {
115
+ registro,
116
+ get finita() { return registro.every((a) => a.playState === "finished"); },
117
+ finisci() { for (const a of registro) try { a.finish(); } catch { /* gia' finita */ } },
118
+ };
119
+ if (fermo()) return esito;
120
+ const $ = (s) => box.querySelector(s), $$ = (s) => [...box.querySelectorAll(s)];
121
+ const piede = $(".s-piede");
122
+ if (piede) appare(piede, 1200, registro, 500);
123
+
124
+ if (box.classList.contains("s-cover")) {
125
+ const logo = $(".s-logo"); if (logo) appare(logo, 0, registro);
126
+ const barra = $(".s-barra");
127
+ if (barra) { barra.style.transformOrigin = "0 50%"; registro.push(barra.animate([{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }], { duration: 600, delay: 250, easing: FUORI, fill: "backwards" })); }
128
+ const h1 = $(".s-h1"); if (h1) entra(h1, { ritardo: 450, durata: 700, da: "translateY(28px)" }, registro);
129
+ const sotto = $(".s-sotto"); if (sotto) entra(sotto, { ritardo: 800 }, registro);
130
+ const meta = $(".s-cover-meta"); if (meta) entra(meta, { ritardo: 1050 }, registro);
131
+ return esito;
132
+ }
133
+
134
+ const titolo = $(".s-titolo"); if (titolo) entra(titolo, { ritardo: 0, durata: 600, da: "translateY(16px)" }, registro);
135
+ let t = 350;
136
+ if (tipo === "numeri") {
137
+ $$(".s-card").forEach((c, i) => { entra(c, { ritardo: t + i * 110, durata: 560, da: "translateY(22px)" }, registro);
138
+ const n = c.querySelector(".s-n"); if (n) contaFinoA(n, 1100, t + i * 110 + 150, registro); });
139
+ t += $$(".s-card").length * 110 + 900;
140
+ } else if (tipo === "riquadro") {
141
+ const vista = $(".s-vista"), eyebrow = $(".s-sx .s-eyebrow");
142
+ if (eyebrow) appare(eyebrow, t, registro);
143
+ if (vista) {
144
+ entra(vista, { ritardo: t, durata: 500, da: "translateY(12px)" }, registro);
145
+ const svg = vista.querySelector("svg"), tab = vista.querySelector("table");
146
+ t = svg ? animaGrafico(svg, t + 250, registro) : tab ? animaTabella(tab, t + 200, registro) : t + 500;
147
+ }
148
+ const barra = $(".s-barra-v"); if (barra) entra(barra, { ritardo: t - 300, durata: 400, da: "scaleY(0)" }, registro);
149
+ t -= 250;
150
+ } else {
151
+ const barra = $(".s-barra-v"); if (barra) entra(barra, { ritardo: t, durata: 400, da: "scaleY(0)" }, registro);
152
+ t += 150;
153
+ }
154
+ $$(".s-punti li").forEach((li, i) => entra(li, { ritardo: t + i * 140, durata: 480, da: "translateX(-14px)" }, registro));
155
+ return esito;
156
+ }