@materializecms/server 0.8.0 → 0.9.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.
- package/CHANGELOG.md +34 -0
- package/package.json +4 -4
- package/src/admin.js +3 -2
- package/src/ai.js +17 -16
- package/src/app.d.ts +5 -0
- package/src/app.js +518 -264
- package/src/brief.js +3 -2
- package/src/en/api.d.ts +2 -0
- package/src/en/api.js +199 -0
- package/src/en/index.d.ts +5 -0
- package/src/en/index.js +7 -0
- package/src/en/services.d.ts +2 -0
- package/src/en/services.js +154 -0
- package/src/forms.d.ts +6 -3
- package/src/forms.js +22 -11
- package/src/index.d.ts +13 -0
- package/src/index.js +52 -2
- package/src/lang.d.ts +7 -0
- package/src/lang.js +17 -0
- package/src/mail.d.ts +6 -2
- package/src/mail.js +73 -40
- package/src/media.js +3 -2
- package/src/offsite.js +12 -9
- package/src/remote-storage.js +24 -23
- package/src/remote.d.ts +25 -8
- package/src/remote.js +210 -45
- package/src/status-page.js +9 -4
- package/src/t.d.ts +2 -0
- package/src/t.js +4 -0
- package/src/tools.js +28 -27
- package/src/wordpress.js +12 -11
- package/src/wpimport.js +27 -23
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ Ogni versione, dalla più recente. Le novità in arrivo si scrivono sotto «Pros
|
|
|
4
4
|
|
|
5
5
|
## Prossima versione
|
|
6
6
|
|
|
7
|
+
## 0.9.0 — 2026-11-10
|
|
8
|
+
|
|
9
|
+
- **Da sapere se aggiorni.** `materialize dev` si raggiunge solo dal computer (`--host 0.0.0.0` per il telefono). Online, «Password dimenticata?» manda il link solo se è configurato l'indirizzo del sito (`site.url` o `MATERIALIZE_SITE_URL`). I webhook dei moduli verso indirizzi interni vengono rifiutati (`MATERIALIZE_PRIVATE_WEBHOOKS=1` per un servizio sullo stesso server). `getEntries` non restituisce più le pagine modello (`{ templates: true }` per averle). Il pannello parte nella lingua del browser finché ognuno non ne sceglie una. Cambiando il server dell'AI, dell'email o dei backup esterni, la chiave salvata va scritta di nuovo. Il terminale parla la lingua del sistema (`MATERIALIZE_LANG=it` per l'italiano).
|
|
10
|
+
- **Il pannello, dal controllo generale.** Da telefono si pubblica: il pulsante resta visibile, una freccia riporta all'elenco e le pagine si aprono in modalità Contenuto; annulla, ripristina, tema e cronologia sono in «Altre azioni». «Pubblicato» non sembra più un pulsante spento. Impostazioni → Sito non mostra più comandi che lì non servono.
|
|
11
|
+
- **Coerenza del nuovo aspetto**: menu delle impostazioni a pillole con una larghezza e un titolo uguali in ogni scheda; dettaglio dei media, newsletter, persone e campi su carte arrotondate; pulsanti, selettori e filtri a pillola ovunque; elenchi allineati alla loro intestazione.
|
|
12
|
+
- **Editor**: i selettori del pannello delle proprietà vanno su due righe invece di accavallare le etichette; la scheda Aggiungi mostra i nomi interi; nel pannello Contenuto le voci del menu si chiamano «Voce del menu»; i due «⋯» della tela hanno icone diverse.
|
|
13
|
+
- **Newsletter**: impostazioni del mittente in una scheda **Impostazioni** propria; «Invia ora» dice perché non si può inviare; «Esporta» spento senza iscritti; i nomi delle sezioni di iscrizione seguono la lingua del pannello.
|
|
14
|
+
- **Piccole cose**: badge dei messaggi nuovi anche su **Impostazioni** nella barra laterale; etichetta «Lingua dei contenuti»; la chiave dei webhook è nascosta, con i pulsanti per mostrarla e copiarla; un quarto tono della voce prende il posto del primo; nel registro si vede quale documento è stato «preso»; pochi accessi falliti non sono più un avviso; il menu ⋯ delle righe si vede anche sui touch; «Vai al contenuto» anche nel pannello.
|
|
15
|
+
- **Accessibilità e lingua**: il campo password dell'accesso ha la sua etichetta; i controlli del pannello delle proprietà hanno un nome per i lettori di schermo; la lingua scelta nella pagina di accesso non si perde più entrando; cambiando lingua si ricaricano i testi del server; caricamenti ed export CSV seguono la lingua di chi li fa; le notifiche dei moduli al proprietario partono nella sua lingua.
|
|
16
|
+
- **Il sito, dal controllo generale.** Un'immagine o un video vuoti non mostrano più il riquadro dell'editor sul sito. Le pagine modello non finiscono più nei menu (`getEntries` le lascia fuori; anche le loro traduzioni non hanno più indirizzo né sitemap). I link del menu condiviso a una sezione che non è nella pagina portano alla sezione nella home della stessa lingua.
|
|
17
|
+
- **Accessibilità del sito**: link «Vai al contenuto» quando la pagina comincia con un menu; il menu su telefono si apre da tastiera portando il focus sulle voci e si chiude con Esc; le etichette del pulsante sono nella lingua della pagina; dopo lo scorrimento verso una sezione la tastiera riparte da lì; le scene WebGL decorative non vengono annunciate; i moduli suggeriscono nome, email e telefono.
|
|
18
|
+
- **Prestazioni**: le immagini dicono al browser quanto spazio occupano (niente più versione da 1920 px per una scheda piccola) e la prima della pagina si carica subito; le scene WebGL partono solo quando sono vicine allo schermo e visibili, quindi su telefono un modello 3D nascosto non viene scaricato.
|
|
19
|
+
- **SEO**: i siti nuovi creati con `create-materialize` usano il componente `Seo` (descrizioni, anteprime social, dati strutturati, lingue) e un `<main>` che lascia fuori menu e piè di pagina; una pagina senza descrizione usa il primo paragrafo; le briciole di pane partono dalla home e non puntano più a pagine inesistenti; gli articoli sono di tipo «articolo» senza doverlo scegliere; `og:locale` nella forma `it_IT`.
|
|
20
|
+
- **Sicurezza del sito**: tutte le pagine, gli errori e i reindirizzamenti hanno le intestazioni di sicurezza (nosniff, referrer, nessun inserimento in frame di altri siti, HSTS su HTTPS); le immagini della libreria si possono usare nelle anteprime social e nei lettori di feed; un indirizzo malformato riceve un 400 invece di un errore del server. In sviluppo il ricaricamento automatico funziona anche da `127.0.0.1` e dalla rete locale, e i file di configurazione del progetto non sono leggibili dal browser.
|
|
21
|
+
- **Sicurezza, dal controllo generale.** Le pagine salvate vengono ripulite prima di diventare HTML: un tipo di elemento, un valore CSS, un'icona SVG o una copia di un componente non possono più portare codice nel sito (prima un editor poteva farlo, e lo script sarebbe girato anche per gli amministratori che aprivano la pagina). Le icone SVG personalizzate vengono ricostruite da zero con i soli elementi da disegno.
|
|
22
|
+
- Le copie dei componenti cambiano solo i propri contenuti; ripristinare una versione segue le stesse regole del salvataggio; la risposta automatica dei moduli la cambia solo un amministratore, come destinatari e webhook.
|
|
23
|
+
- Gli indirizzi interni sono bloccati anche nelle forme IPv6 e dopo un cambio di DNS, sia nelle importazioni da URL sia nei webhook dei moduli (`MATERIALIZE_PRIVATE_WEBHOOKS=1` per un servizio sullo stesso server).
|
|
24
|
+
- Limiti di dimensione sulle richieste al server e sulle password; più limiti ai messaggi dei moduli per indirizzo; gli allegati dei messaggi di spam non vengono salvati.
|
|
25
|
+
- «Password dimenticata?» online usa solo l'indirizzo configurato del sito (senza, il link non parte e Impostazioni → Sicurezza lo segnala) e risponde sempre negli stessi tempi; il reset con il link chiude le sessioni e revoca i token personali.
|
|
26
|
+
- L'API pubblica dei contenuti non mostra più destinatari, webhook e risposte automatiche dei moduli. Il cookie di sessione è `Secure` anche dietro un proxy HTTPS. Le prove di invio della newsletter sono limitate. Cambiando il server dell'AI, dell'email o dei backup esterni, la chiave salvata va scritta di nuovo.
|
|
27
|
+
- **Avviso di aggiornamento**: quando esce una nuova versione di Materialize, gli amministratori vedono in alto a destra una pillola con il numero della versione (al passaggio del mouse, le prime novità) e una scheda **Aggiornamento** nella Panoramica, che portano a Impostazioni → Aggiornamenti. La pillola si chiude e torna con la versione successiva. Si vede anche in sviluppo, dove si aggiorna dal terminale.
|
|
28
|
+
- **Scorrimento fluido verso le sezioni**: i link a una sezione della stessa pagina, come le voci del menu, non saltano più di colpo: la pagina scorre in modo morbido (al massimo un secondo) e la sezione si ferma sotto il menu fisso, anche se intanto finiscono di caricarsi immagini più in alto. La rotella o un tocco fermano lo scorrimento; chi ha chiesto meno movimento vede il salto di sempre.
|
|
29
|
+
- **Materialize in inglese, completo.** Oltre al pannello, sono in inglese i messaggi del server, le email a chi ha un account, i controlli SEO, di accessibilità e di prestazioni, i nomi delle sezioni della libreria e degli stili, il terminale (`materialize` e `create-materialize`), la guida (`docs/guide`) e il README (`README.en.md`).
|
|
30
|
+
- Ognuno sceglie la lingua per sé, nel profilo o nella pagina di accesso; prima di scegliere, il pannello segue la lingua del browser. Due persone possono usare lo stesso sito una in italiano e una in inglese: il server risponde a ognuna nella sua lingua, e le email partono nella lingua del profilo di chi le riceve.
|
|
31
|
+
- Il terminale parla la lingua del sistema; `MATERIALIZE_LANG=it` o `en` la sceglie a mano.
|
|
32
|
+
- Restano nella lingua del sito i testi che vedono i visitatori: pagine, moduli, banner dei cookie, newsletter.
|
|
33
|
+
- Anche i testi che il server salva per dopo si leggono nella lingua di chi guarda: motivi dello spam, errori delle pubblicazioni programmate, dei rilasci e delle newsletter, e gli stessi messaggi nel registro attività. Quelli salvati prima di questa versione restano come sono.
|
|
34
|
+
- **Pannello in inglese, terza parte: tutto il resto del pannello.** Documenti, liste e rilasci, ricerca ⌘K, campi e libreria media, newsletter, persone e verifica in due passaggi, tutte le impostazioni, l'installazione iniziale (con il selettore della lingua) e i tour.
|
|
35
|
+
- **Pannello in inglese, seconda parte: l'editor.** Tela, livelli, libreria, stili, controlli, pannello delle proprietà, animazioni, interazioni, assistente, commenti, tour e scorciatoie, esportazione e modalità Design delle raccolte a blocchi. I nuovi elementi prendono nome e testo segnaposto nella lingua del pannello.
|
|
36
|
+
- **`materialize dev` resta sul tuo computer**: prima si poteva raggiungere da tutta la rete, e su una Wi-Fi condivisa qualcun altro avrebbe potuto aprire un sito nuovo e crearne l'amministratore. Per provarlo dal telefono: `--host 0.0.0.0`. `materialize start` non cambia.
|
|
37
|
+
- **Nuovo aspetto del pannello**: menu, area di lavoro e pannelli dell'editor sono superfici arrotondate su un fondo più scuro; pulsanti, filtri e selettori sono a pillola.
|
|
38
|
+
- Ricerca, tema e profilo si sono spostati in alto a destra, in un incavo dell'area di lavoro (sul telefono, nella barra in alto). La lingua dei contenuti resta in basso a sinistra.
|
|
39
|
+
- Nell'editor i comandi sopra la tela stanno in tre incavi del suo bordo, e lo zoom è una pillola in basso a destra sulla tela. Sulle tele strette i comandi meno usati passano in un menu ⋯, così la barra non viene tagliata.
|
|
40
|
+
|
|
7
41
|
## 0.8.0 — 2026-11-07
|
|
8
42
|
|
|
9
43
|
- **Un documento alla volta**: se un'altra persona sta modificando la pagina che apri, la vedi in sola lettura con chi ci lavora e da quanto, e puoi prendere il controllo. Nessuno sovrascrive più il lavoro di un altro senza saperlo, nemmeno da due schede.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@materializecms/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Il server di Materialize: API, accessi, media, backup, aggiornamenti dal pannello e server MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@anthropic-ai/sdk": "1.3.0",
|
|
22
22
|
"@hono/node-server": "2.1.5",
|
|
23
|
-
"@materializecms/admin": "0.
|
|
24
|
-
"@materializecms/core": "0.
|
|
25
|
-
"@materializecms/db": "0.
|
|
23
|
+
"@materializecms/admin": "0.9.0",
|
|
24
|
+
"@materializecms/core": "0.9.0",
|
|
25
|
+
"@materializecms/db": "0.9.0",
|
|
26
26
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
27
27
|
"hono": "4.13.14",
|
|
28
28
|
"htmlparser2": "12.0.0",
|
package/src/admin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import { extname, join, normalize, sep } from 'node:path';
|
|
4
|
+
import { t } from './t.js';
|
|
4
5
|
const TYPES = {
|
|
5
6
|
'.html': 'text/html; charset=utf-8',
|
|
6
7
|
'.js': 'text/javascript; charset=utf-8',
|
|
@@ -63,8 +64,8 @@ export async function serveAdmin(c, dir) {
|
|
|
63
64
|
const index = dir ? join(dir, 'index.html') : '';
|
|
64
65
|
if (!dir || !(await isFile(index))) {
|
|
65
66
|
return c.html('<!doctype html><meta charset="utf-8"><title>Materialize</title><body style="font:15px system-ui;padding:40px">' +
|
|
66
|
-
|
|
67
|
-
'
|
|
67
|
+
`<h1 style="font-size:20px">${t('Il pannello non è ancora compilato')}</h1>` +
|
|
68
|
+
`<p>${t('Esegui {command} nella cartella di Materialize, poi ricarica la pagina.', { command: '<code>npm run build</code>' })}</p>`, 503);
|
|
68
69
|
}
|
|
69
70
|
if (c.req.path === FRAME_PATH)
|
|
70
71
|
return c.body(FRAME_HTML, 200, {
|
package/src/ai.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { t } from './t.js';
|
|
3
4
|
import { ToolError } from './tools.js';
|
|
4
5
|
export const EMPTY_AI = { provider: 'anthropic', apiKey: '', baseUrl: '', model: '', fastModel: '' };
|
|
5
6
|
export const DEFAULT_CLAUDE = 'claude-opus-5-5';
|
|
@@ -62,7 +63,7 @@ export async function complete(s, input) {
|
|
|
62
63
|
],
|
|
63
64
|
});
|
|
64
65
|
if (message.stop_reason === 'refusal')
|
|
65
|
-
throw new AiError('La richiesta è stata rifiutata dal modello.');
|
|
66
|
+
throw new AiError(t('La richiesta è stata rifiutata dal modello.'));
|
|
66
67
|
return textOf(message).trim();
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
@@ -72,7 +73,7 @@ export async function complete(s, input) {
|
|
|
72
73
|
export async function runTurn(s, conv, input) {
|
|
73
74
|
const stopped = () => {
|
|
74
75
|
if (input.signal?.aborted)
|
|
75
|
-
throw new AiError('Interrotto.');
|
|
76
|
+
throw new AiError(t('Interrotto.'));
|
|
76
77
|
};
|
|
77
78
|
const images = input.images ?? [];
|
|
78
79
|
const byName = new Map(input.tools.map((t) => [t.name, t]));
|
|
@@ -132,7 +133,7 @@ export async function runTurn(s, conv, input) {
|
|
|
132
133
|
messages.push({ role: 'tool', tool_call_id: call.id, content: result.text });
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
|
-
return 'Mi sono fermato: servivano troppi passaggi. Dimmi se continuare.';
|
|
136
|
+
return t('Mi sono fermato: servivano troppi passaggi. Dimmi se continuare.');
|
|
136
137
|
}
|
|
137
138
|
const tools = input.tools.map((t) => ({
|
|
138
139
|
name: t.name,
|
|
@@ -175,7 +176,7 @@ export async function runTurn(s, conv, input) {
|
|
|
175
176
|
if (block.type === 'thinking' && block.thinking.trim())
|
|
176
177
|
input.onEvent({ type: 'status', text: block.thinking.trim() });
|
|
177
178
|
if (message.stop_reason === 'refusal')
|
|
178
|
-
throw new AiError('La richiesta è stata rifiutata dal modello.');
|
|
179
|
+
throw new AiError(t('La richiesta è stata rifiutata dal modello.'));
|
|
179
180
|
if (message.stop_reason !== 'tool_use')
|
|
180
181
|
return textOf(message).trim();
|
|
181
182
|
const calls = message.content.filter((b) => b.type === 'tool_use');
|
|
@@ -186,7 +187,7 @@ export async function runTurn(s, conv, input) {
|
|
|
186
187
|
// All results in one message, as the API expects for parallel calls.
|
|
187
188
|
messages.push({ role: 'user', content: results });
|
|
188
189
|
}
|
|
189
|
-
return 'Mi sono fermato: servivano troppi passaggi. Dimmi se continuare.';
|
|
190
|
+
return t('Mi sono fermato: servivano troppi passaggi. Dimmi se continuare.');
|
|
190
191
|
}
|
|
191
192
|
// ---------- Claude ----------
|
|
192
193
|
/** Models that take the server-side fallback on a refusal and progress notes between tool calls. */
|
|
@@ -222,20 +223,20 @@ async function claude(s, params) {
|
|
|
222
223
|
}
|
|
223
224
|
catch (error) {
|
|
224
225
|
if (error instanceof Anthropic.AuthenticationError)
|
|
225
|
-
throw new AiError('La chiave API di Anthropic non è valida.');
|
|
226
|
+
throw new AiError(t('La chiave API di Anthropic non è valida.'));
|
|
226
227
|
if (error instanceof Anthropic.PermissionDeniedError)
|
|
227
|
-
throw new AiError('La chiave non ha accesso a questo modello.');
|
|
228
|
+
throw new AiError(t('La chiave non ha accesso a questo modello.'));
|
|
228
229
|
if (error instanceof Anthropic.NotFoundError)
|
|
229
|
-
throw new AiError(
|
|
230
|
+
throw new AiError(t('Il modello “{model}” non esiste.', { model }));
|
|
230
231
|
if (error instanceof Anthropic.RateLimitError)
|
|
231
|
-
throw new AiError('Troppe richieste ad Anthropic: riprova tra poco.');
|
|
232
|
+
throw new AiError(t('Troppe richieste ad Anthropic: riprova tra poco.'));
|
|
232
233
|
// The reasoning of earlier turns no longer fits the conversation (it was changed underneath).
|
|
233
234
|
if (error instanceof Anthropic.BadRequestError && /thinking/i.test(error.message))
|
|
234
|
-
throw new AiError('Questa conversazione non può continuare: iniziane una nuova con ↺ in alto.');
|
|
235
|
+
throw new AiError(t('Questa conversazione non può continuare: iniziane una nuova con ↺ in alto.'));
|
|
235
236
|
if (error instanceof Anthropic.BadRequestError)
|
|
236
|
-
throw new AiError(
|
|
237
|
+
throw new AiError(t('Richiesta non valida: {detail}', { detail: error.message }));
|
|
237
238
|
if (error instanceof Anthropic.APIError)
|
|
238
|
-
throw new AiError(
|
|
239
|
+
throw new AiError(t('Anthropic non risponde ({status}).', { status: error.status ?? t('rete') }));
|
|
239
240
|
throw error;
|
|
240
241
|
}
|
|
241
242
|
}
|
|
@@ -262,11 +263,11 @@ async function openaiChat(s, body) {
|
|
|
262
263
|
});
|
|
263
264
|
}
|
|
264
265
|
catch {
|
|
265
|
-
throw new AiError(
|
|
266
|
+
throw new AiError(t('Il server AI non risponde su {base}.', { base }));
|
|
266
267
|
}
|
|
267
268
|
const json = (await res.json().catch(() => null));
|
|
268
269
|
if (!res.ok)
|
|
269
|
-
throw new AiError(json?.error?.message ??
|
|
270
|
+
throw new AiError(json?.error?.message ?? t('Il server AI ha risposto {status}.', { status: res.status }));
|
|
270
271
|
if (json?.usage) {
|
|
271
272
|
// Cached tokens are part of the prompt tokens here: the rest is the full-price part.
|
|
272
273
|
const cached = json.usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
@@ -280,7 +281,7 @@ async function openaiChat(s, body) {
|
|
|
280
281
|
}
|
|
281
282
|
const message = json?.choices?.[0]?.message;
|
|
282
283
|
if (!message)
|
|
283
|
-
throw new AiError('Il server AI ha dato una risposta vuota.');
|
|
284
|
+
throw new AiError(t('Il server AI ha dato una risposta vuota.'));
|
|
284
285
|
return { role: 'assistant', content: message.content ?? null, tool_calls: message.tool_calls };
|
|
285
286
|
}
|
|
286
287
|
// ---------- translation ----------
|
|
@@ -310,7 +311,7 @@ voice = '') {
|
|
|
310
311
|
result = parseList(reply, batch.length);
|
|
311
312
|
}
|
|
312
313
|
if (!result)
|
|
313
|
-
throw new AiError('Il modello non ha restituito la traduzione completa: riprova.');
|
|
314
|
+
throw new AiError(t('Il modello non ha restituito la traduzione completa: riprova.'));
|
|
314
315
|
out.push(...result);
|
|
315
316
|
onProgress?.(out.length);
|
|
316
317
|
}
|
package/src/app.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export interface ServerContext {
|
|
|
28
28
|
* fresh server first cannot claim it: `materialize start` prints the link that carries it.
|
|
29
29
|
*/
|
|
30
30
|
setupToken?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Lets form webhooks post to this machine or its network (an n8n beside the site). Off by
|
|
33
|
+
* default, since what visitors write is sent there; MATERIALIZE_PRIVATE_WEBHOOKS=1 turns it on.
|
|
34
|
+
*/
|
|
35
|
+
privateWebhooks?: boolean;
|
|
31
36
|
}
|
|
32
37
|
export declare function createApp(base: ServerContext): Hono<Env, import("hono/types").BlankSchema, "/"> & {
|
|
33
38
|
runSchedules: (now?: Date) => Promise<number>;
|