@noeva/analytics-core 0.1.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +4 -0
- package/cli.js +34 -4
- package/contenuto/lib/mcp-locale/ambiente/.python-version +1 -0
- package/contenuto/lib/mcp-locale/ambiente/pyproject.toml +23 -0
- package/contenuto/lib/mcp-locale/ambiente/uv.lock +1103 -0
- package/contenuto/lib/mcp-locale/cartella/CLAUDE.md +52 -0
- package/contenuto/lib/mcp-locale/cartella/LEGGIMI.md +163 -0
- package/contenuto/lib/mcp-locale/cartella/imposta-password.ps1 +40 -0
- package/contenuto/lib/mcp-locale/cartella/installa.ps1 +286 -0
- package/contenuto/lib/mcp-locale/costruisci.py +495 -0
- package/contenuto/lib/mcp-locale/prova.py +93 -0
- package/contenuto/lib/mcp-locale/server/modello_locale.py +167 -0
- package/contenuto/lib/mcp-locale/server/noeva_mcp.py +203 -0
- package/contenuto/lib/modello/consegna.py +134 -0
- package/contenuto/lib/modello/pubblica.py +131 -0
- package/contenuto/lib/snapshot/ascolta.py +213 -0
- package/contenuto/lib/snapshot/impacchetta.py +184 -0
- package/contenuto/lib/snapshot/noeva_snapshot.py +107 -0
- package/contenuto/lib/snapshot/pubblica.py +35 -15
- package/contenuto/lib/snapshot/worker/installa.ps1 +340 -0
- package/core.js +21 -3
- package/modello/repository/github/workflows/ci.yml +53 -3
- package/modello/repository/github/workflows/rilascio-windows.yml +207 -0
- package/modello/repository/gitignore +2 -1
- package/package.json +2 -1
- package/pubblica.js +211 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Installa o aggiorna il worker degli snapshot di {{progetto}} (NOEVA Analytics, ADR 0004),
|
|
2
|
+
# passo per passo, e alla fine dice com'e' andata.
|
|
3
|
+
#
|
|
4
|
+
# powershell -ExecutionPolicy Bypass -File .\installa.ps1 installa o aggiorna
|
|
5
|
+
# powershell -ExecutionPolicy Bypass -File .\installa.ps1 -SoloControllo controlla e basta (dopo un riavvio)
|
|
6
|
+
#
|
|
7
|
+
# Si lancia dalla cartella estratta dallo zip, ENTRANDO IN WINDOWS COME L'UTENTE CHE FARA'
|
|
8
|
+
# GIRARE IL WORKER: le chiavi vanno nelle sue variabili d'ambiente. La copia in
|
|
9
|
+
# C:\NOEVA\snapshot la fa lo script; se c'e' gia' e' un aggiornamento (la cartella vecchia si
|
|
10
|
+
# mette da parte, lavoro\ e le chiavi restano). Alla fine il primo giro parte subito: lo
|
|
11
|
+
# scheduler di NOEVA lo mette in coda e il worker lo esegue qui davanti, poi l'attivita'
|
|
12
|
+
# pianificata prende il posto (ogni {{minuti}} minuti, ascolta --una-volta).
|
|
13
|
+
# Nessun segreto finisce a schermo o nel file dell'esito.
|
|
14
|
+
|
|
15
|
+
param([switch]$SoloControllo)
|
|
16
|
+
|
|
17
|
+
$ErrorActionPreference = 'Stop'
|
|
18
|
+
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { }
|
|
19
|
+
|
|
20
|
+
$qui = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
21
|
+
$progetto = '{{progetto}}'
|
|
22
|
+
$utenteDb = '{{utente}}'
|
|
23
|
+
$varPassword = '{{password_env}}'
|
|
24
|
+
$dbHost = '{{host}}'
|
|
25
|
+
$dbPorta = {{porta}}
|
|
26
|
+
$minuti = {{minuti}}
|
|
27
|
+
$pipeline = "analytics-snapshot-$progetto"
|
|
28
|
+
$attivita = "NOEVA snapshot $progetto"
|
|
29
|
+
$destinazione = 'C:\NOEVA\snapshot'
|
|
30
|
+
$exe = Join-Path $destinazione 'noeva-snapshot\noeva-snapshot.exe'
|
|
31
|
+
$pacchetto = Join-Path $destinazione "pacchetti\$progetto"
|
|
32
|
+
$modo = 'nuova installazione'
|
|
33
|
+
$esiti = New-Object System.Collections.ArrayList
|
|
34
|
+
$conf = Get-Content (Join-Path $qui 'worker.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
35
|
+
$api = $conf.api.TrimEnd('/')
|
|
36
|
+
$contenitore = $conf.contenitore.TrimEnd('/')
|
|
37
|
+
$workspace = $conf.workspace
|
|
38
|
+
|
|
39
|
+
function Passo($titolo) {
|
|
40
|
+
Write-Host ''
|
|
41
|
+
Write-Host "== $titolo" -ForegroundColor Cyan
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Esito($passo, $stato, $nota) {
|
|
45
|
+
[void]$esiti.Add([pscustomobject]@{ Passo = $passo; Stato = $stato; Nota = $nota })
|
|
46
|
+
$colore = @{ 'ok' = 'Green'; 'avviso' = 'Yellow'; 'NO' = 'Red'; 'saltato' = 'DarkGray' }[$stato]
|
|
47
|
+
Write-Host (' {0,-7} {1}' -f $stato, $nota) -ForegroundColor $colore
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Chiedi($domanda, [bool]$predefinito = $true) {
|
|
51
|
+
$suggerimento = if ($predefinito) { '(S/n)' } else { '(s/N)' }
|
|
52
|
+
$r = Read-Host " $domanda $suggerimento"
|
|
53
|
+
if (-not $r) { return $predefinito }
|
|
54
|
+
return $r.Trim().ToLower().StartsWith('s')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ChiediSegreto($domanda) {
|
|
58
|
+
$sicura = Read-Host " $domanda" -AsSecureString
|
|
59
|
+
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sicura)
|
|
60
|
+
try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) }
|
|
61
|
+
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function PortaAperta($h, $p) {
|
|
65
|
+
$c = New-Object Net.Sockets.TcpClient
|
|
66
|
+
try { return $c.BeginConnect($h, $p, $null, $null).AsyncWaitHandle.WaitOne(5000) -and $c.Connected }
|
|
67
|
+
catch { return $false }
|
|
68
|
+
finally { $c.Close() }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Esegui($comando, [string[]]$argomenti) {
|
|
72
|
+
$prima = $ErrorActionPreference
|
|
73
|
+
$ErrorActionPreference = 'Continue'
|
|
74
|
+
try {
|
|
75
|
+
$testo = (& $comando @argomenti 2>&1 | ForEach-Object { "$_" }) -join "`n"
|
|
76
|
+
return [pscustomobject]@{ Codice = $LASTEXITCODE; Testo = $testo }
|
|
77
|
+
} finally { $ErrorActionPreference = $prima }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Come Esegui, ma le righe si vedono mentre arrivano (la verifica e il giro durano minuti).
|
|
81
|
+
function EseguiDalVivo($comando, [string[]]$argomenti) {
|
|
82
|
+
$prima = $ErrorActionPreference
|
|
83
|
+
$ErrorActionPreference = 'Continue'
|
|
84
|
+
try {
|
|
85
|
+
$righe = New-Object System.Collections.ArrayList
|
|
86
|
+
& $comando @argomenti 2>&1 | ForEach-Object { $r = "$_"; [void]$righe.Add($r); Write-Host " $r" }
|
|
87
|
+
return [pscustomobject]@{ Codice = $LASTEXITCODE; Testo = ($righe -join "`n") }
|
|
88
|
+
} finally { $ErrorActionPreference = $prima }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Una chiamata all'API NOEVA con la chiave del worker. Cloudflare blocca lo user agent di
|
|
92
|
+
# PowerShell come quello di Python: ci si presenta come noeva-snapshot.
|
|
93
|
+
function ApiNoeva($metodo, $percorso, $corpo) {
|
|
94
|
+
$par = @{
|
|
95
|
+
Method = $metodo; Uri = "$api$percorso"; UseBasicParsing = $true; TimeoutSec = 60
|
|
96
|
+
UserAgent = 'noeva-snapshot (NOEVA Analytics)'
|
|
97
|
+
Headers = @{ Authorization = "Bearer $env:NOEVA_API_KEY"; 'x-noeva-workspace' = $workspace }
|
|
98
|
+
}
|
|
99
|
+
if ($corpo) { $par.Body = ($corpo | ConvertTo-Json -Compress); $par.ContentType = 'application/json' }
|
|
100
|
+
try {
|
|
101
|
+
$r = Invoke-WebRequest @par
|
|
102
|
+
return [pscustomobject]@{ Codice = [int]$r.StatusCode; Corpo = $(if ($r.Content) { $r.Content | ConvertFrom-Json } else { $null }); Errore = '' }
|
|
103
|
+
} catch {
|
|
104
|
+
$codice = 0
|
|
105
|
+
if ($_.Exception.Response) { $codice = [int]$_.Exception.Response.StatusCode }
|
|
106
|
+
$messaggio = if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $_.ErrorDetails.Message } else { $_.Exception.Message }
|
|
107
|
+
return [pscustomobject]@{ Codice = $codice; Corpo = $null; Errore = $messaggio.Substring(0, [Math]::Min(200, $messaggio.Length)) }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function Fine {
|
|
112
|
+
$no = @($esiti | Where-Object { $_.Stato -eq 'NO' }).Count
|
|
113
|
+
$avvisi = @($esiti | Where-Object { $_.Stato -eq 'avviso' }).Count
|
|
114
|
+
$esitoGenerale = if ($no) { "NON COMPLETATA: $no passi da sistemare" } elseif ($avvisi) { "COMPLETATA con $avvisi avvisi" } else { 'COMPLETATA' }
|
|
115
|
+
$quando = Get-Date
|
|
116
|
+
$runner = ''
|
|
117
|
+
$quale = if (Test-Path $exe) { $exe } else { Join-Path $qui 'noeva-snapshot\noeva-snapshot.exe' }
|
|
118
|
+
if (Test-Path $quale) { try { $runner = ((Esegui $quale @('versione')).Testo | ConvertFrom-Json).runner } catch { } }
|
|
119
|
+
$righe = @(
|
|
120
|
+
"worker degli snapshot di $progetto (runner $runner) · $modo · $esitoGenerale",
|
|
121
|
+
"macchina $env:COMPUTERNAME · utente Windows $env:USERDOMAIN\$env:USERNAME · $($quando.ToString('dd/MM/yyyy HH:mm'))",
|
|
122
|
+
''
|
|
123
|
+
) + ($esiti | ForEach-Object { '{0,-7} {1,-20} {2}' -f $_.Stato, $_.Passo, $_.Nota })
|
|
124
|
+
Write-Host ''
|
|
125
|
+
Write-Host '==================== RIEPILOGO ====================' -ForegroundColor Cyan
|
|
126
|
+
$colore = if ($no) { 'Red' } elseif ($avvisi) { 'Yellow' } else { 'Green' }
|
|
127
|
+
Write-Host $righe[0] -ForegroundColor $colore
|
|
128
|
+
$righe | Select-Object -Skip 1 | ForEach-Object { Write-Host $_ }
|
|
129
|
+
$cartellaEsito = if (Test-Path $destinazione) { $destinazione } else { $qui }
|
|
130
|
+
$file = Join-Path $cartellaEsito ("esito-installa-{0}.txt" -f $quando.ToString('yyyyMMdd-HHmm'))
|
|
131
|
+
try {
|
|
132
|
+
$righe | Set-Content -Path $file -Encoding UTF8
|
|
133
|
+
Write-Host ''
|
|
134
|
+
Write-Host "Esito salvato in $file (senza chiavi ne' password): da mandare a chi segue l'installazione."
|
|
135
|
+
} catch { Write-Host "Non riesco a salvare l'esito in $file" -ForegroundColor Yellow }
|
|
136
|
+
if ($no) { exit 1 } else { exit 0 }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Ferma($passo, $nota) {
|
|
140
|
+
Esito $passo 'NO' $nota
|
|
141
|
+
Write-Host ' Mi fermo qui: sistemato questo, rilancia lo script.' -ForegroundColor Red
|
|
142
|
+
Fine
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Write-Host "NOEVA Analytics · worker degli snapshot di $progetto" -ForegroundColor Cyan
|
|
146
|
+
Write-Host "macchina $env:COMPUTERNAME · utente Windows $env:USERDOMAIN\$env:USERNAME"
|
|
147
|
+
if ($SoloControllo) { $modo = 'solo controllo'; Write-Host 'Solo controllo: non cambio niente.' }
|
|
148
|
+
|
|
149
|
+
# 0. L'utente ----------------------------------------------------------------------
|
|
150
|
+
if (-not $SoloControllo) {
|
|
151
|
+
Passo "0. L'utente Windows del worker"
|
|
152
|
+
Write-Host " Il worker girera' come $env:USERDOMAIN\$env:USERNAME, con le sue variabili d'ambiente."
|
|
153
|
+
Write-Host ' Su un server: un account di servizio dedicato, non amministratore, non una persona.'
|
|
154
|
+
if (Chiedi "E' l'utente giusto?") { Esito 'utente' 'ok' "$env:USERDOMAIN\$env:USERNAME" }
|
|
155
|
+
else { Ferma 'utente' 'entra in Windows come l''utente del worker e rilancia' }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# 1. La rete -------------------------------------------------------------------------
|
|
159
|
+
Passo '1. La rete: il database e NOEVA, solo in uscita'
|
|
160
|
+
if (PortaAperta $dbHost $dbPorta) { Esito 'rete database' 'ok' "$dbHost`:$dbPorta" }
|
|
161
|
+
else { Ferma 'rete database' "$dbHost`:$dbPorta non risponde: rete o VPN, per l'IT" }
|
|
162
|
+
foreach ($u in @($api, $contenitore)) {
|
|
163
|
+
$h = ([Uri]$u).Host
|
|
164
|
+
if (PortaAperta $h 443) { Esito 'rete NOEVA' 'ok' "$h`:443" }
|
|
165
|
+
else { Ferma 'rete NOEVA' "$h`:443 non risponde: serve l'uscita HTTPS (proxy?), per l'IT" }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# 2. Il driver ODBC --------------------------------------------------------------------
|
|
169
|
+
Passo '2. Il driver ODBC per SQL Server'
|
|
170
|
+
$driver = @(Get-OdbcDriver -Platform 64-bit -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^ODBC Driver (18|17) for SQL Server$' } | ForEach-Object { $_.Name })
|
|
171
|
+
if ($driver -contains 'ODBC Driver 18 for SQL Server') { Esito 'driver' 'ok' 'ODBC Driver 18 for SQL Server' }
|
|
172
|
+
elseif ($driver -contains 'ODBC Driver 17 for SQL Server') {
|
|
173
|
+
if (-not $SoloControllo) { [Environment]::SetEnvironmentVariable('NOEVA_MSSQL_DRIVER', 'ODBC Driver 17 for SQL Server', 'User') }
|
|
174
|
+
$env:NOEVA_MSSQL_DRIVER = 'ODBC Driver 17 for SQL Server'
|
|
175
|
+
Esito 'driver' 'avviso' 'c''e'' il 17, non il 18: va bene, impostato NOEVA_MSSQL_DRIVER'
|
|
176
|
+
} else {
|
|
177
|
+
Ferma 'driver' 'manca: ODBC Driver 18 for SQL Server x64 da https://aka.ms/downloadmsodbcsql (serve un amministratore)'
|
|
178
|
+
}
|
|
179
|
+
$driverUtente = [Environment]::GetEnvironmentVariable('NOEVA_MSSQL_DRIVER', 'User')
|
|
180
|
+
if ($driverUtente) { $env:NOEVA_MSSQL_DRIVER = $driverUtente }
|
|
181
|
+
|
|
182
|
+
# 3. La cartella -------------------------------------------------------------------------
|
|
183
|
+
Passo "3. La cartella $destinazione"
|
|
184
|
+
$giaInstallata = [bool](Get-ScheduledTask -TaskName $attivita -ErrorAction SilentlyContinue)
|
|
185
|
+
if ($SoloControllo) {
|
|
186
|
+
if (Test-Path $exe) { Esito 'cartella' 'ok' $destinazione } else { Ferma 'cartella' "$exe non c'e'" }
|
|
187
|
+
} elseif ($qui.TrimEnd('\') -ieq $destinazione) {
|
|
188
|
+
$modo = 'sul posto'
|
|
189
|
+
Esito 'cartella' 'ok' "lo script gira gia' da $destinazione"
|
|
190
|
+
} else {
|
|
191
|
+
if (Test-Path $destinazione) {
|
|
192
|
+
$modo = 'aggiornamento'
|
|
193
|
+
# Durante la copia l'attivita' non deve partire: si riaccende al passo 7.
|
|
194
|
+
if ($giaInstallata) { Disable-ScheduledTask -TaskName $attivita | Out-Null }
|
|
195
|
+
$inCorso = @(Get-Process -Name 'noeva-snapshot' -ErrorAction SilentlyContinue)
|
|
196
|
+
if ($inCorso.Count) {
|
|
197
|
+
Write-Host ' Un giro e'' in corso: aspetto che finisca (al massimo 10 minuti).'
|
|
198
|
+
$inCorso | Wait-Process -Timeout 600 -ErrorAction SilentlyContinue
|
|
199
|
+
}
|
|
200
|
+
$vecchia = "$destinazione-prima-$((Get-Date).ToString('yyyyMMdd-HHmm'))"
|
|
201
|
+
try { Rename-Item $destinazione (Split-Path -Leaf $vecchia) }
|
|
202
|
+
catch { Ferma 'cartella' "non riesco a rinominare $destinazione (in uso?): $($_.Exception.Message)" }
|
|
203
|
+
Copy-Item $qui $destinazione -Recurse
|
|
204
|
+
# lavoro\ (gli ultimi giri e ascolta.log) resta: serve a capire i giri passati.
|
|
205
|
+
if (Test-Path (Join-Path $vecchia 'lavoro')) { Copy-Item (Join-Path $vecchia 'lavoro') (Join-Path $destinazione 'lavoro') -Recurse -Force }
|
|
206
|
+
Esito 'cartella' 'ok' "vecchia messa da parte in $vecchia, lavoro\ riportato"
|
|
207
|
+
} else {
|
|
208
|
+
New-Item -ItemType Directory -Force (Split-Path $destinazione) | Out-Null
|
|
209
|
+
Copy-Item $qui $destinazione -Recurse
|
|
210
|
+
Esito 'cartella' 'ok' "copiata in $destinazione"
|
|
211
|
+
}
|
|
212
|
+
Get-ChildItem $destinazione -Recurse | Unblock-File
|
|
213
|
+
if (-not (Test-Path $exe)) { Ferma 'cartella' "$exe non c'e' dopo la copia" }
|
|
214
|
+
}
|
|
215
|
+
New-Item -ItemType Directory -Force (Join-Path $destinazione 'lavoro') | Out-Null
|
|
216
|
+
$v = Esegui $exe @('versione')
|
|
217
|
+
if ($v.Codice -eq 0) { Esito 'eseguibile' 'ok' $v.Testo.Trim() } else { Ferma 'eseguibile' "non parte: $($v.Testo.Trim())" }
|
|
218
|
+
|
|
219
|
+
# 4. La chiave del worker -----------------------------------------------------------------
|
|
220
|
+
Passo '4. La chiave NOEVA del worker'
|
|
221
|
+
$chiave = [Environment]::GetEnvironmentVariable('NOEVA_API_KEY', 'User')
|
|
222
|
+
if ($chiave -and -not $SoloControllo -and -not (Chiedi "La chiave e' gia' salvata per $env:USERNAME. La tengo?")) { $chiave = '' }
|
|
223
|
+
if (-not $chiave) {
|
|
224
|
+
if ($SoloControllo) { Ferma 'chiave' 'manca NOEVA_API_KEY: rilancia senza -SoloControllo' }
|
|
225
|
+
for ($i = 0; $i -lt 3; $i++) {
|
|
226
|
+
$chiave = ChiediSegreto "Chiave NOEVA dell'utente di servizio (noeva_sk_..., non si vede mentre scrivi)"
|
|
227
|
+
if ($chiave.StartsWith('noeva_sk_')) { break }
|
|
228
|
+
Write-Host ' Non sembra una chiave NOEVA (comincia con noeva_sk_).' -ForegroundColor Yellow
|
|
229
|
+
$chiave = ''
|
|
230
|
+
}
|
|
231
|
+
if (-not $chiave) { Ferma 'chiave' 'nessuna chiave valida inserita' }
|
|
232
|
+
[Environment]::SetEnvironmentVariable('NOEVA_API_KEY', $chiave, 'User')
|
|
233
|
+
}
|
|
234
|
+
$env:NOEVA_API_KEY = $chiave
|
|
235
|
+
$chiave = $null
|
|
236
|
+
# La prova della chiave: un biglietto per una versione di prova. Dice in una volta se la chiave
|
|
237
|
+
# vale, se l'utente e' membro del workspace e se il workspace ha il modulo. Non scrive niente.
|
|
238
|
+
$b = ApiNoeva 'POST' '/api/analytics/biglietto-dati' @{ progetto = $progetto; versione = 'prova-installazione'; da = $env:COMPUTERNAME }
|
|
239
|
+
switch ($b.Codice) {
|
|
240
|
+
200 { Esito 'chiave' 'ok' "valida, l'utente e' membro del workspace e il modulo e' attivo" }
|
|
241
|
+
401 { Ferma 'chiave' 'non valida o revocata: rilancia e reinseriscila' }
|
|
242
|
+
403 { Ferma 'chiave' "rifiutata: $($b.Errore) (l'utente e' membro del workspace? il modulo e' abilitato?)" }
|
|
243
|
+
503 { Ferma 'chiave' "l'API non rilascia biglietti (manca ANALYTICS_SEGRETO_DATI): e' da NOEVA, non da qui" }
|
|
244
|
+
default { Ferma 'chiave' "risposta inattesa dall'API ($($b.Codice)): $($b.Errore)" }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# 5. La password e la verifica ----------------------------------------------------------------
|
|
248
|
+
Passo "5. La password di $utenteDb e la verifica dal vivo"
|
|
249
|
+
$password = [Environment]::GetEnvironmentVariable($varPassword, 'User')
|
|
250
|
+
if ($password -and -not $SoloControllo -and -not (Chiedi "La password e' gia' salvata per $env:USERNAME. La tengo?")) { $password = '' }
|
|
251
|
+
if (-not $password) {
|
|
252
|
+
if ($SoloControllo) { Ferma 'password' "manca $varPassword`: rilancia senza -SoloControllo" }
|
|
253
|
+
for ($i = 0; $i -lt 3 -and -not $password; $i++) { $password = ChiediSegreto "Password di $utenteDb (non si vede mentre scrivi)" }
|
|
254
|
+
if (-not $password) { Ferma 'password' 'nessuna password inserita' }
|
|
255
|
+
[Environment]::SetEnvironmentVariable($varPassword, $password, 'User')
|
|
256
|
+
}
|
|
257
|
+
Set-Item "env:$varPassword" $password
|
|
258
|
+
$password = $null
|
|
259
|
+
$v = EseguiDalVivo $exe @('verifica', '--pacchetto', $pacchetto, '--connessione', (Join-Path $pacchetto 'connessione.json'))
|
|
260
|
+
$avvisiVerifica = @($v.Testo -split "`n" | Where-Object { $_ -match '^\s*avviso\s' }).Count
|
|
261
|
+
if ($v.Codice -eq 0 -and $avvisiVerifica) { Esito 'verifica' 'avviso' "i controlli tornano, $avvisiVerifica avvisi entro la tolleranza (dati che l'ERP riscrive)" }
|
|
262
|
+
elseif ($v.Codice -eq 0) { Esito 'verifica' 'ok' 'connessione e cifre di controllo tornano' }
|
|
263
|
+
else {
|
|
264
|
+
$primoNo = ($v.Testo -split "`n" | Where-Object { $_ -match '^\s*NO\s' } | Select-Object -First 1)
|
|
265
|
+
Ferma 'verifica' $(if ($primoNo) { $primoNo.Trim() } else { "uscita $($v.Codice)" })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# 6. Il primo giro ------------------------------------------------------------------------------
|
|
269
|
+
if ($SoloControllo) {
|
|
270
|
+
$log = Join-Path $destinazione 'lavoro\ascolta.log'
|
|
271
|
+
if (Test-Path $log) {
|
|
272
|
+
$ultima = Get-Content $log -Tail 40 | Where-Object { $_ -match 'pubblicata|FALLITO' } | Select-Object -Last 1
|
|
273
|
+
Esito 'ultimo giro' $(if ($ultima -match 'FALLITO') { 'NO' } elseif ($ultima) { 'ok' } else { 'avviso' }) $(if ($ultima) { $ultima.Trim() } else { 'nessun giro ancora in ascolta.log' })
|
|
274
|
+
} else { Esito 'ultimo giro' 'avviso' 'ascolta.log non c''e'' ancora' }
|
|
275
|
+
} else {
|
|
276
|
+
Passo '6. Il primo giro, adesso'
|
|
277
|
+
if (Chiedi 'Faccio partire adesso il primo giro (circa 4 minuti)?') {
|
|
278
|
+
$jobs = ApiNoeva 'GET' '/api/scheduled-jobs' $null
|
|
279
|
+
$job = @($jobs.Corpo.data | Where-Object { $_.process_function -eq $pipeline }) | Select-Object -First 1
|
|
280
|
+
if (-not $job) {
|
|
281
|
+
Esito 'schedulazione' 'NO' "nessuna schedulazione '$pipeline' nel workspace: va creata nel pannello NOEVA (tipo ETL)"
|
|
282
|
+
} else {
|
|
283
|
+
Esito 'schedulazione' $(if ($job.enabled) { 'ok' } else { 'avviso' }) ("'{0}', {1} {2}{3}" -f $job.name, $job.cron_expression, $job.timezone, $(if ($job.enabled) { '' } else { ', DISATTIVATA: riattivala dal pannello' }))
|
|
284
|
+
$t = ApiNoeva 'POST' "/api/scheduled-jobs/$($job.id)/trigger" $null
|
|
285
|
+
if ($t.Codice -ne 200) { Esito 'primo giro' 'NO' "non riesco a farlo partire ($($t.Codice)): $($t.Errore)" }
|
|
286
|
+
else {
|
|
287
|
+
Write-Host ' In coda. Lo eseguo qui (i giri arretrati in coda, se ce ne sono, passano tutti):'
|
|
288
|
+
$g = EseguiDalVivo $exe @('ascolta', '--config', (Join-Path $destinazione 'worker.json'), '--una-volta')
|
|
289
|
+
Add-Content -Path (Join-Path $destinazione 'lavoro\ascolta.log') -Value $g.Testo -Encoding UTF8
|
|
290
|
+
$pubblicate = @([regex]::Matches($g.Testo, ': pubblicata (\S+) in') | ForEach-Object { $_.Groups[1].Value })
|
|
291
|
+
$falliti = @($g.Testo -split "`n" | Where-Object { $_ -match 'FALLITO' })
|
|
292
|
+
if ($falliti.Count) { Esito 'primo giro' 'NO' $falliti[0].Trim() }
|
|
293
|
+
elseif ($pubblicate.Count) { Esito 'primo giro' 'ok' "pubblicata la versione $($pubblicate[-1]) ($($pubblicate.Count) giri)" }
|
|
294
|
+
else { Esito 'primo giro' 'avviso' 'coda vuota: il giro lo prendera'' l''attivita'' pianificata, guarda ascolta.log' }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else { Esito 'primo giro' 'saltato' 'lo fara'' l''attivita'' pianificata alla prossima schedulazione' }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# 7. L'attivita' pianificata --------------------------------------------------------------------
|
|
301
|
+
Passo "7. L'attivita' pianificata '$attivita'"
|
|
302
|
+
if ($SoloControllo) {
|
|
303
|
+
$t = Get-ScheduledTask -TaskName $attivita -ErrorAction SilentlyContinue
|
|
304
|
+
if (-not $t) { Esito 'attivita''' 'NO' 'non installata: rilancia senza -SoloControllo' }
|
|
305
|
+
else {
|
|
306
|
+
$info = Get-ScheduledTaskInfo -TaskName $attivita
|
|
307
|
+
$stato = if ($t.State -eq 'Disabled') { 'NO' } elseif ($info.LastTaskResult -in 0, 267009, 267011) { 'ok' } else { 'avviso' }
|
|
308
|
+
Esito 'attivita''' $stato ("{0}, ultimo avvio {1}, risultato {2}" -f $t.State, $info.LastRunTime, $info.LastTaskResult)
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
Write-Host " Ogni $minuti minuti lancia 'ascolta --una-volta' come $env:USERNAME, anche senza sessione aperta:"
|
|
312
|
+
Write-Host ' per questo Windows chiede la password di questo utente (quella di Windows, non del database).'
|
|
313
|
+
$credenziali = Get-Credential -UserName "$env:USERDOMAIN\$env:USERNAME" -Message "Password Windows di $env:USERNAME"
|
|
314
|
+
if (-not $credenziali) { Ferma 'attivita''' 'nessuna password Windows: senza, l''attivita'' non gira a sessione chiusa' }
|
|
315
|
+
$log = Join-Path $destinazione 'lavoro\ascolta.log'
|
|
316
|
+
$azione = New-ScheduledTaskAction -Execute 'cmd.exe' -WorkingDirectory $destinazione `
|
|
317
|
+
-Argument "/c `"`"$exe`" ascolta --config `"$(Join-Path $destinazione 'worker.json')`" --una-volta >> `"$log`" 2>&1`""
|
|
318
|
+
$quando = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval (New-TimeSpan -Minutes $minuti)
|
|
319
|
+
$regole = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
|
|
320
|
+
-StartWhenAvailable -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries
|
|
321
|
+
try {
|
|
322
|
+
Register-ScheduledTask -TaskName $attivita -Action $azione -Trigger $quando -Settings $regole `
|
|
323
|
+
-User $credenziali.UserName -Password $credenziali.GetNetworkCredential().Password -RunLevel Limited -Force | Out-Null
|
|
324
|
+
Enable-ScheduledTask -TaskName $attivita | Out-Null
|
|
325
|
+
Esito 'attivita''' 'ok' "ogni $minuti minuti come $($credenziali.UserName); registro in $log"
|
|
326
|
+
} catch {
|
|
327
|
+
Esito 'attivita''' 'NO' "Windows l'ha rifiutata (password giusta?): $($_.Exception.Message)"
|
|
328
|
+
}
|
|
329
|
+
$credenziali = $null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# 8. La dashboard ------------------------------------------------------------------------------
|
|
333
|
+
if (-not $SoloControllo) {
|
|
334
|
+
Passo '8. La dashboard'
|
|
335
|
+
Write-Host " Apri $contenitore/$progetto/ (login NOEVA) e guarda la data dei dati."
|
|
336
|
+
if (Chiedi 'La data e'' quella di oggi?') { Esito 'dashboard' 'ok' 'dati di oggi' }
|
|
337
|
+
else { Esito 'dashboard' 'avviso' 'da guardare con chi segue l''installazione (la pagina si ricarica?)' }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Fine
|
package/core.js
CHANGED
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync,
|
|
13
13
|
} from 'node:fs';
|
|
14
14
|
import { dirname, join, relative, sep } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
// fileURLToPath e non .pathname: su Windows .pathname e' /D:/... e il percorso diventa D:\D:\...
|
|
18
|
+
export const PACCHETTO = fileURLToPath(new URL('.', import.meta.url));
|
|
17
19
|
export const VERSIONE = JSON.parse(readFileSync(join(PACCHETTO, 'package.json'), 'utf8')).version;
|
|
18
20
|
const MODELLO = join(PACCHETTO, 'modello', 'repository');
|
|
19
21
|
const KIT = join(PACCHETTO, 'kit');
|
|
@@ -50,7 +52,12 @@ export function leggiConfig(radice) {
|
|
|
50
52
|
if (!Array.isArray(nonGestiti) || nonGestiti.some((p) => typeof p !== 'string')) {
|
|
51
53
|
throw new ErroreConfig('analytics.json: `non_gestiti` deve essere un elenco di percorsi');
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
// Il workspace NOEVA che possiede il progetto: lo nomina il biglietto con cui
|
|
56
|
+
// `analytics pubblica` scrive le pagine. Non e' un segreto, e' un id.
|
|
57
|
+
if (c.workspace !== undefined && typeof c.workspace !== 'string') {
|
|
58
|
+
throw new ErroreConfig('analytics.json: `workspace` deve essere l\'id del workspace NOEVA');
|
|
59
|
+
}
|
|
60
|
+
return { progetto: c.progetto, nonGestiti, ...(c.workspace ? { workspace: c.workspace } : {}) };
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// --- file gestiti ---------------------------------------------------------
|
|
@@ -78,7 +85,9 @@ export function fileGestiti(config) {
|
|
|
78
85
|
for (const f of readdirSync(join(CONTENUTO, 'assets')).sort()) {
|
|
79
86
|
voci.push({ percorso: `apps/assets/${f}`, sorgente: join(CONTENUTO, 'assets', f) });
|
|
80
87
|
}
|
|
81
|
-
|
|
88
|
+
for (const w of ['ci.yml', 'rilascio-windows.yml']) {
|
|
89
|
+
voci.push({ percorso: `.github/workflows/${w}`, sorgente: join(MODELLO, 'github', 'workflows', w) });
|
|
90
|
+
}
|
|
82
91
|
for (const nome of readdirSync(join(KIT, 'skills')).sort()) {
|
|
83
92
|
const base = join(KIT, 'skills', nome);
|
|
84
93
|
for (const f of fileSotto(base).sort()) {
|
|
@@ -269,6 +278,15 @@ export function verifica(radice, config) {
|
|
|
269
278
|
|
|
270
279
|
// L'ultima versione pubblicata, o null se il registry non risponde o il pacchetto
|
|
271
280
|
// non c'e': il controllo resta in sola lettura e non fallisce per la rete.
|
|
281
|
+
/** a e' una versione piu' nuova di b? (x.y.z; un prerilascio o un formato strano non lo e') */
|
|
282
|
+
export function piuNuova(a, b) {
|
|
283
|
+
const n = (v) => (/^\d+\.\d+\.\d+$/.test(v || '') ? v.split('.').map(Number) : null);
|
|
284
|
+
const [x, y] = [n(a), n(b)];
|
|
285
|
+
if (!x || !y) return false;
|
|
286
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
export async function ultimaVersione(fetchImpl = fetch) {
|
|
273
291
|
try {
|
|
274
292
|
const r = await fetchImpl('https://registry.npmjs.org/@noeva%2fanalytics-core', { signal: AbortSignal.timeout(5000) });
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
# La verifica del progetto
|
|
2
|
-
# Quali controlli girano lo decide la versione del pacchetto,
|
|
3
|
-
# qui ci sono solo l'installazione e
|
|
1
|
+
# La verifica del progetto e, su main, la pubblicazione (ADR 0005 di NOEVA Analytics).
|
|
2
|
+
# Quali controlli girano lo decide la versione del pacchetto @noeva/analytics-core,
|
|
3
|
+
# non questo file: qui ci sono solo l'installazione e i comandi.
|
|
4
4
|
# File gestito: lo riscrive `analytics upgrade`, non si modifica a mano.
|
|
5
|
+
#
|
|
6
|
+
# `pubblica` porta online, a ogni push su main che passa la verifica, le pagine
|
|
7
|
+
# (apps/) e il modello compilato del commit: su R2, attraverso il contenitore, con
|
|
8
|
+
# un biglietto che l'API NOEVA rilascia alla chiave di un membro del workspace del
|
|
9
|
+
# progetto. Si accende con due impostazioni del repository:
|
|
10
|
+
#
|
|
11
|
+
# secret NOEVA_API_KEY la chiave API NOEVA di un utente del workspace
|
|
12
|
+
# variable ANALYTICS_PUBBLICA "si" quando il contenitore serve questo progetto da R2
|
|
13
|
+
# variable ANALYTICS_URL facoltativa, default https://analytics.noeva.ai
|
|
14
|
+
#
|
|
15
|
+
# I dati no: lo snapshot si genera dove c'e' il database (aggiorna.sh --pubblica
|
|
16
|
+
# sul Mac, o il worker dal cliente) e si pubblica da li'.
|
|
5
17
|
|
|
6
18
|
name: ci
|
|
7
19
|
|
|
@@ -25,3 +37,41 @@ jobs:
|
|
|
25
37
|
node-version: '24'
|
|
26
38
|
- run: npm ci
|
|
27
39
|
- run: npx analytics ci
|
|
40
|
+
|
|
41
|
+
pubblica:
|
|
42
|
+
needs: verifica
|
|
43
|
+
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && vars.ANALYTICS_PUBBLICA == 'si'
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
# Una pubblicazione alla volta, e quella in corso non si interrompe: la
|
|
46
|
+
# successiva aspetta e pubblica il main piu' recente.
|
|
47
|
+
concurrency:
|
|
48
|
+
group: pubblica
|
|
49
|
+
cancel-in-progress: false
|
|
50
|
+
env:
|
|
51
|
+
NOEVA_API_KEY: ${{ secrets.NOEVA_API_KEY }}
|
|
52
|
+
URL: ${{ vars.ANALYTICS_URL || 'https://analytics.noeva.ai' }}
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: '24'
|
|
58
|
+
- run: npm ci
|
|
59
|
+
- name: la chiave API NOEVA c'e'
|
|
60
|
+
run: |
|
|
61
|
+
if [ -z "$NOEVA_API_KEY" ]; then
|
|
62
|
+
echo "::error::manca il secret NOEVA_API_KEY: la chiave di un utente del workspace del progetto"
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
- name: le pagine
|
|
66
|
+
run: npx analytics pubblica --url "$URL"
|
|
67
|
+
# Il modello si compila con wren, che e' Python: le versioni sono quelle del
|
|
68
|
+
# servizio locale (lib/mcp-locale/ambiente del pacchetto).
|
|
69
|
+
- uses: actions/setup-python@v5
|
|
70
|
+
if: hashFiles('wren_project.yml') != ''
|
|
71
|
+
with:
|
|
72
|
+
python-version: '3.11'
|
|
73
|
+
- name: il modello
|
|
74
|
+
if: hashFiles('wren_project.yml') != ''
|
|
75
|
+
run: |
|
|
76
|
+
pip install --quiet "wrenai==0.15.0" "wren-core-py==0.8.0" "pyyaml>=6"
|
|
77
|
+
PYTHON=python npx analytics modello --url "$URL"
|