Salta al contenuto
openplate

L'app

Configurazione

La chiave del database degli alimenti, la Content-Security-Policy, l'analitica, le istanze gestite, gli endpoint di IA personalizzati e forniti dall'istanza

Questa pagina è tradotta automaticamente dalla documentazione in inglese.

openplate si avvia senza alcuna configurazione. Non ci sono URL del database, chiavi di sessione né chiavi di cifratura, perché il server non gestisce account e non memorizza nulla. Ogni variabile è una regolazione facoltativa.

La maggior parte delle variabili viene letta in un solo punto, app/config/index.ts, ed esposta come oggetto tipizzato CONFIG:

typescript
import { CONFIG } from '#config';

const port = CONFIG.server.port;
const appUrl = CONFIG.app.url;

.env.example contiene l'elenco completo con note esplicative nel testo. Copialo in .env per modificarne una.

Variabili d'ambiente

environment-variables.md elenca ogni variabile letta dall'app, con il relativo valore predefinito. Elenca anche le variabili per il server centrale e per il servizio di inferenza. Le sezioni seguenti spiegano le funzionalità principali in modo approfondito.

Una variabile non ha una sezione dedicata. DEFAULT_UI_LANGUAGE imposta la lingua mostrata a chi visita il sito prima di sceglierne una: en, il valore predefinito, de, fr, it, es o tr. La scelta personale dell'utente ha sempre la precedenza. L'impostazione non traduce i nomi dei cibi, le risposte dell'IA né ciò che una persona ha digitato. Qualsiasi altro valore blocca l'avvio.

Le chiavi API dei provider non vengono mai lette dall'ambiente. La chiave di un utente viene inserita nel browser, memorizzata sul dispositivo e inviata direttamente browser → provider; il server non ne conserva alcuna copia. MISTRAL_API_KEY / OPENROUTER_API_KEY in .env.example esistono solo perché uno sviluppatore possa puntare gli script di verifica a un provider attivo. Impostarli su un'istanza distribuita non ha alcun effetto.

La chiave del database degli alimenti

FOOD_DB_API_URL e FOOD_DB_API_KEY sono due decisioni diverse ed è utile tenerle separate.

L'URL decide se questa istanza cerchi o meno gli alimenti. Se lo imposti su una stringa vuota, nessun nome di alimento lascerà mai la tua macchina.

La chiave decide quanto puoi cercare. Ci sono tre livelli:

livellocosa faicosa ottieni
anonimonienteuna piccola quota giornaliera, condivisa per indirizzo di rete
gratuitofornisci un indirizzo email su lowcarbcheck.org/developersuna quota mensile generosa
partnerrichiedinessun limite mensile

LowCarbCheck conteggia in crediti, e una ricerca di alimenti ne consuma uno. Al momento della stesura, il livello anonimo offre 1.000 crediti al giorno e la chiave gratuita 100.000 al mese; lowcarbcheck.org/developers riporta i numeri attuali. LowCarbCheck vede l'indirizzo del tuo server dell'app, non gli indirizzi dei suoi utenti. Con il livello anonimo, chiunque sulla tua istanza condivide un'unica quota giornaliera.

Un'istanza senza chiave continua a funzionare. Corrisponde al livello anonimo e, per una singola persona che sta provando openplate, di solito è sufficiente. Un nucleo familiare, o chiunque scansioni diversi piatti al giorno, vorrà la chiave gratuita.

FOOD_DB_DAILY_CALL_LIMIT limita il numero di chiamate a LowCarbCheck che questo server può effettuare in un giorno UTC. Il valore predefinito, 3.200, mantiene l'uso mensile entro le 100.000 della chiave gratuita. Se un nome è stato cercato negli ultimi cinque minuti, la risposta arriva dalla memoria e non costa nulla. Una volta superato il limite, le ricerche di alimenti si interrompono fino alla mezzanotte UTC, l'app avvisa l'utente e le scansioni continuano a completarsi usando i valori dell'IA. Aumentalo se la tua chiave ne consente di più. Il conteggio risiede in memoria, quindi un riavvio lo azzera.

Su un'istanza gestita, anche la ricerca degli alimenti richiede un account autenticato. L'app invia la sessione dell'account a ogni ricerca, e il server dell'app chiede al server centrale su CORE_URL se la sessione è attiva prima che qualsiasi dato raggiunga LowCarbCheck. Il server dell'app deve quindi raggiungere anche quell'indirizzo. Se non ci riesce, le ricerche vengono rifiutate finché non torna raggiungibile, mentre le scansioni si completano comunque con i numeri generati dall'IA. Un'istanza aperta risponde invece a ogni ricerca, come prima.

La ricerca è comunque di tipo fail-open: se il database degli alimenti non è raggiungibile, rifiuta la richiesta o esaurisce la quota, la scansione viene comunque completata e mostra comunque i numeri. Quei numeri saranno quindi una stima elaborata dall'IA anziché un dato del database, e l'app lo segnala chiaramente a schermo invece di lasciare che la differenza passi inosservata.

Proposte per il database degli alimenti

Con FOOD_DB_BACKFILL=true e una chiave, openplate inoltra a LowCarbCheck, tramite questo server, ogni alimento salvato da una foto o da un pasto digitato:

  • Un alimento associato a una riga di LowCarbCheck invia i nomi dell'alimento in tutte le lingue dell'app, consentendo alla riga di acquisire i titoli mancanti.
  • Un alimento senza corrispondenza invia i propri nomi in tutte le lingue dell'app e i macronutrienti per 100 g, così che LowCarbCheck possa aggiungerlo. Richiede un nome in inglese e tutti e quattro i valori di carboidrati, grassi, proteine ed energia. Un alimento privo di questi dati non viene inviato.
  • Il nome digitato o modificato direttamente dalla persona non viene mai inviato.

Una proposta include i nomi, i macronutrienti e l'indicazione se l'alimento provenga da una foto o da un pasto digitato. Non include alcun account, alcuna voce del diario, alcuna foto né l'indirizzo della persona. LowCarbCheck vede il tuo server e la tua chiave. LowCarbCheck valuta ogni proposta con un modello al momento della ricezione e pubblica ciò che viene approvato. Un alimento pubblicato in questo modo viene restituito dal database degli alimenti contrassegnato come stima, e openplate lo mostra e lo memorizza come tale, mai come fonte verificata.

Ogni persona può disattivarlo per il proprio dispositivo in Impostazioni → IA. Rimane disattivato su ogni istanza finché chi la gestisce non imposta FOOD_DB_BACKFILL=true.

Iscrizione alla newsletter

openplate non include una mailing list predefinita. Imposta sia NEWSLETTER_SUBSCRIBE_URL sia NEWSLETTER_TURNSTILE_SITE_KEY e la pagina iniziale aggiungerà un modulo di iscrizione. Il browser lo invia al server openplate, che inoltra {email, locale, consent, source, turnstileToken} al tuo URL, per un massimo di cinque richieste al minuto da un singolo indirizzo. Il browser non apprende mai quell'URL, che può quindi trovarsi su una rete privata. Impostane solo uno e l'avvio si arresta. Lasciali entrambi non impostati, l'opzione predefinita, e non ci saranno moduli, script aggiuntivi né modifiche alla CSP.

Il controllo delle versioni

Il server scarica https://openplate.de/latest.json ogni sei ore e una volta circa 90 secondi dopo l'avvio. Questo piccolo file indica la versione più recente. Il server confronta tale versione con quella in esecuzione e ne segnala il risultato su Impostazioni > Informazioni. Anche il pulsante "Controlla ora" avvia una verifica, con una frequenza limitata a una richiesta reale al minuto per l'intera istanza.

La richiesta viene eseguita da server, non dal browser. Aggiungere openplate.de al file connect-src di produzione amplierebbe la allowlist che impedisce a uno script iniettato di sottrarre una chiave BYOK. La richiesta è una semplice GET senza corpo, parametri di query, token o identificatore dell'istanza. Come ogni richiesta HTTPS, invia l'indirizzo IP del server. Invia anche un User-Agent nel formato openplate/<version> (<platform>; <arch>), per esempio openplate/1.2.3 (linux; arm64). Non trasmette nient'altro. Il progetto conta gli indirizzi richiedenti univoci al giorno e memorizza solo i totali giornalieri. I log del proxy conservano gli indirizzi IP fino a 15 giorni, esattamente come per le normali visite a un sito web. Vedi ADR-0021.

bash
UPDATE_CHECK=off

Questa impostazione disabilita completamente i controlli. Il server non avvia alcun timer, non invia richieste, esclude l'istanza dai conteggi del progetto e segnala nella pagina Informazioni che i controlli sono disabilitati.

Il controllo si limita sempre a notificare. openplate è un singolo container stateless e non può sostituire la propria immagine, quindi l'aggiornamento rimane quello di sempre:

bash
docker compose -f compose.yml pull && docker compose -f compose.yml up -d

L'unico pulsante nell'app che modifica qualcosa è "Ricarica per aggiornare", che compare quando il server sta già distribuendo una build più recente di quella in esecuzione nella pagina aperta. Ricarica il browser sulle risorse presenti nel server e non tocca nulla sull'host.

Spostare gli utenti su un'altra istanza

Quando chiudi un'istanza e i suoi utenti passano a un'altra, mantieni in esecuzione il vecchio container con un'unica impostazione:

bash
MOVED_TO_URL=https://app.openplate.example

Ogni route sulla vecchia istanza mostra quindi una singola pagina di avviso. La pagina indica dove si trova ora openplate, fornisce un pulsante per accedere alla pagina di accesso della nuova sede e spiega a chi ha aggiunto l'app alla schermata iniziale di rimuovere quell'icona e aggiungere il nuovo indirizzo. La pagina sceglie la lingua in questo ordine: la scelta salvata dell'utente, le lingue richieste dal browser, quindi DEFAULT_UI_LANGUAGE.

La pagina informa gli utenti che il loro account e il loro diario si sono spostati con loro. Attiva questa modalità solo se ciò corrisponde al vero: la nuova istanza usa lo stesso server centrale, oppure hai migrato gli account lì. Un diario archiviato solo in un browser rimane in quel browser sotto il vecchio indirizzo; il nuovo indirizzo non può leggerlo.

Un reindirizzamento HTTP non può gestire questa migrazione. Un telefono con l'app installata esegue un service worker che memorizza nella cache le pagine dell'applicazione. I browser non seguono i reindirizzamenti quando verificano la presenza di aggiornamenti del worker, quindi un'app installata continuerebbe ad aprire la copia salvata. In questa modalità, /sw.js fornisce un piccolo worker che elimina ogni cache sotto il vecchio indirizzo, annulla la propria registrazione e ricarica la pagina. La richiesta successiva carica quindi l'avviso direttamente dal server. Il worker lascia intatti i dati del diario memorizzati nel browser.

L'API restituisce 410 Gone con il nuovo indirizzo nel corpo della risposta. /healthcheck risponde come prima, e il manifest dell'app web rimane invariato, quindi un'icona sulla schermata iniziale apre ancora il vecchio indirizzo e carica la pagina. Mantieni questa modalità attiva fino a quando il traffico verso il vecchio indirizzo non cessa del tutto. Il valore deve essere un indirizzo https:// su un host diverso da APP_URL, privo di nome utente o password; qualsiasi altra configurazione blocca l'avvio.

La Content-Security-Policy

La chiamata di visione BYOK e la chiave risiedono entrambe interamente nel browser, quindi la build di produzione include una Content-Security-Policy restrittiva. La sua direttiva connect-src consente:

  • 'self'
  • le origini dei provider integrati (OpenRouter, Mistral, Anthropic), ricavate automaticamente dal registro dei provider: vedi ADR-0007
  • localhost e 127.0.0.1 su qualsiasi porta. [::1] non è nell'elenco, perché un'origine CSP non può specificare un indirizzo IPv6; indirizza invece un client verso localhost.
  • le tue variabili CORE_URL e DEFAULT_INFERENCE_BASE_URL, se impostate
  • qualsiasi elemento in CSP_CONNECT_EXTRA

Questa lista di elementi consentiti è ciò che impedisce a uno script iniettato di sottrarre una chiave presente nella pagina. Allargala solo con piena cognizione di causa.

Su un'istanza gestita il proxy IA è il server centrale con cui il client comunica già, quindi la sua origine è CORE_URL, già presente nell'elenco qui sopra. Non c'è un secondo endpoint remoto da autorizzare e non serve aggiungere nulla a CSP_CONNECT_EXTRA a tale scopo.

Analisi

openplate può conteggiare il proprio utilizzo. Non registra nulla finché non lo configuri, e non registra mai cosa mangi.

Imposta MATOMO_URL e MATOMO_SITE_ID per puntare un'istanza a un'installazione di Matomo gestita da te. Se le lasci entrambe vuote, che è il valore predefinito, l'istanza non carica alcuno script di analisi dati, non invia alcuna richiesta e presenta la stessa intestazione Content-Security-Policy utilizzata prima dell'introduzione delle statistiche. Se ne imposti una senza l'altra, l'avvio fallisce intenzionalmente: un operatore convinto di raccogliere dati analitici mentre non è così si trova in una condizione peggiore rispetto a chi visualizza un errore.

Il tracciatore funziona con i cookie disabilitati. Non memorizza nulla sul dispositivo, quindi non occorre alcun banner di consenso.

Il controllo della versione è separato. Non fa uso di tracker né di Matomo. UPDATE_CHECK=off interrompe il controllo e il conteggio giornaliero delle richieste del progetto. Vedi Il controllo delle versioni.

Cosa determina un livello

MATOMO_EVENT_LEVEL determina la quantità di dati che l'istanza può trasmettere. Si applica solo quando l'analisi dati è già attiva.

LivelloCosa conta
pageviewsSolo le visualizzazioni di pagina. Nessun evento legato alle funzionalità viene mai generato.
productVisualizzazioni di pagina e utilizzo del software. L'impostazione predefinita.
researchTutto, inclusi digiuno, peso, condivisione con il medico e partecipazione agli studi.

Se non impostato, equivale a product. Un valore non riconosciuto blocca l'avvio. Anche un livello impostato su un'istanza senza Matomo configurato blocca l'avvio, per lo stesso motivo per cui lo fa una coppia configurata a metà.

Cosa conta product

36 eventi, tutti relativi al software anziché alla persona.

AreaEventi
Onboardingcompletato, passaggio completato (focus, peso, corpo), passaggio saltato
Scansioneriuscita, non riuscita (con una categoria di errore fissa), nessun risultato, modalità scelta, avviata da una foto condivisa
Diarioregistrato (con il metodo di immissione: ricerca, manuale, scansione del piatto, scansione dell'etichetta, tag, copia giorno, registra di nuovo, pasto salvato), voce modificata, voce eliminata, voce ripristinata, pasto salvato
Alimenti personalizzatimodificato, eliminato
Provider IAcollegato (manuale, OAuth, preimpostazione dell'istanza), verifica della chiave non riuscita, scollegato
Preferenzemodificate (tema o lingua)
Backupesportato, importato, esportato in CSV, cache delle foto svuotata
Accountcreato, eliminato, password modificata, reimpostazione della password richiesta, reimpostazione della password completata, configurazione completata
Invitilink incollato, partecipazione completata
Installazione dell'apprichiesta di installazione mostrata, installata, visualizzazione di pagina non in linea
Pagina di destinazioneiscrizione alla newsletter effettuata, clic sulla call to action

Cosa aggiunge research

Altri 12 eventi. Ciascuno rivela dettagli sulla salute di una persona o sulla sua partecipazione a uno studio, motivo per cui rimangono disattivati a meno che tu non li richieda espressamente.

AreaEventi
Digiunodigiuno iniziato (subito o programmato), digiuno terminato
Obiettivi e pesoobiettivi salvati (traguardi o parametri fisici), peso registrato
Condivisione con il medicocondivisione concessa, revocata, chiave ruotata, identità creata, diario condiviso aperto
Studi di ricercaiscrizione effettuata, revoca effettuata, contributo inviato

Questi eventi non contengono valori. Un evento di digiuno non ne riporta la durata, e un evento relativo al peso non riporta il peso misurato. Tuttavia gli eventi hanno una marcatura temporale, come tutti gli eventi di analisi, quindi un inizio e una fine indicano per sottrazione una durata, e un evento di condivisione rivela che la persona è seguita da un medico. La partecipazione a uno studio rientra nelle categorie particolari di dati personali secondo l'Art. 9 del GDPR.

È l'unico motivo per cui questo livello esiste. Un ricercatore che conduce uno studio sulla propria istanza ha bisogno di questi parametri e può raccoglierli lecitamente dai partecipanti che hanno dato il consenso. Un'istanza generica non dovrebbe raccoglierli e, per impostazione predefinita, non lo fa.

Se attivi research, indicalo nella tua informativa sulla privacy. Quella di openplate descrive le istanze gestite da openplate, non la tua.

Cosa non viene mai conteggiato, a nessun livello

  • Tutto ciò che proviene da un diario. Nessun nome di alimento, peso, obiettivo, foto, orario del pasto o id di studio.
  • Qualsiasi valore numerico misurato su una persona. Gli eventi contengono un'etichetta fissa oppure niente.
  • Qualsiasi identificatore. Nessun id account, indirizzo email o id dispositivo.
  • Le stringhe di query e i frammenti di URL, che vengono scartati per intero prima di registrare la visualizzazione di una pagina. openplate li usa per inserire token monouso.
  • Identificatori all'interno di un percorso. /diary/entry/<id> e /shared/<account id> vengono sostituiti da un segnaposto prima che la visualizzazione della pagina venga registrata.

Il rispetto delle regole è garantito dai tipi anziché da una revisione manuale. Ciascuna funzione di evento in app/lib/matomo-events.ts non accetta argomenti oppure ne accetta solo uno da un elenco prefissato, impedendo di passare il nome di un alimento senza generare un errore di compilazione. tests/unit/no-telemetry-wiring.test.ts blocca la compilazione se qualsiasi altro file tenta di accedere direttamente al tracker, o se nell'origine sono presenti un host Matomo o un site id, circostanza che farebbe inviare i dati di un'istanza self-hosted all'account di qualcun altro.

Vedi ADR-0010 per la decisione e le motivazioni.

Istanze gestite

Un'istanza che imposta INSTANCE_MODE=managed è un istanza gestita: un amministratore invita le persone via email, e ciascun account include una quota giornaliera per l'IA, quindi l'accesso fornisce a una persona sia il diario sia l'IA in un unico passaggio. Le istanze ospitate su beta.openplate.de e app.openplate.de usano questa modalità. È disattivata per impostazione predefinita: un self-hoster che non imposta nulla ottiene l'app aperta.

INSTANCE_MODE=managed richiede CORE_URL. L'account è ciò che tiene insieme il diario e la quota; dichiarare managed senza un server centrale blocca l'avvio anziché abilitare le cose a metà.

Un amministratore invita le persone dall'app stessa, su /admin, oppure con l'API di amministrazione di openplate-core e ADMIN_TOKEN. Il primissimo account, prima che esista qualsiasi amministratore, proviene da quell'API. self-hosting.md contiene il comando. Con la posta configurata sul server centrale, l'invito viene spedito via email. Senza posta configurata, la risposta contiene il link che dovrai trasmettere tu. Una password dimenticata si reimposta tramite un link. Quel link viene inviato via email oppure, senza posta, generato da un amministratore (vedi self-hosting.md). Il server conserva un codice di recupero depositato a garanzia che decifra la chiave dei dati dopo la reimpostazione (vedi sync.md).

Un'istanza gestita con IA richiede anche tre valori sul server centrale: UPSTREAM_BASE_URL e UPSTREAM_API_KEY (il provider e la sua chiave) e un modello. Il modello è AI_ADVERTISED_MODEL, quello utilizzato da ogni scansione. Oppure imposta AI_TIERS_FILE=bundled, e il modello verrà ricavato dal file dei livelli del core, ai-tiers.json, che raccoglie in un unico punto verificato il modello, il suo instradamento e il suo prezzo (funziona anche il percorso di un file che monti; vedi il proxy IA). Per le scansioni è richiesto un modello. Senza un modello, il server centrale segnala che non ce n'è alcuno e l'app rifiuta di eseguire la scansione invece di scegliere un modello a tue spese. Indica il nome del modello esattamente come fa il tuo provider, ad esempio vendor/model-name con UPSTREAM_BASE_URL=https://openrouter.ai/api/v1, oppure openplate-plate-1 davanti a openplate-inference. Con un file dei livelli, AI_ADVERTISED_MODEL serve solo come override di emergenza per il modello del livello predefinito, e il core registra un avviso a ogni avvio. Ciascun account richiede poi una quota giornaliera, che parte da 0. Assegnala con "dailyAiLimit" quando generi l'invito, oppure impostala in seguito in /admin.

Cosa cambia quando è impostato:

  • /welcome offre esattamente due azioni: Accedi e Ho un link di invito (che accetta un link incollato e lo passa a /join). Non è presente "Inizia".
  • /onboarding reindirizza a /welcome nel caso di un dispositivo senza un diario locale né un account. Il percorso locale anonimo non è semplicemente nascosto, è chiuso del tutto: su un'istanza gestita non porta a nulla, dato che senza account non c'è AI e senza account nessun diario sopravvive al dispositivo. Un dispositivo che contiene già un diario non viene mai espulso.
  • /join esegue un'unica procedura: l'invito viene riscattato dalla richiesta di registrazione stessa, in una singola transazione, e da quel momento l'account include sia il diario sia la quota. L'azione "Salta, ho già un account" non è più presente, perché su un'istanza di questo tipo chi la riceve non ha né l'uno né l'altro.
  • Impostazioni → Account, quando non hai effettuato l'accesso, propone di accedere e spiega che qui gli account provengono da un link di invito. Non c'è alcun pulsante per "creare un account".

Tutto quanto descritto sopra resta invariato su un'istanza aperta (INSTANCE_MODE non impostata o open), e un test verifica entrambe le varianti a confronto.

Inviti dei membri

Su un'istanza gestita, un amministratore non è l'unica persona che può invitare. Tre variabili di openplate-core stabiliscono se un membro ordinario può invitare qualcuno, e a quali condizioni. Si impostano sul server centrale, non sull'app. compose.core.yml e compose.full.yml passano tutte e tre da .env al server centrale.

VariabilePredefinitoDescrizione
MEMBER_INVITE_DAILY_AI_LIMITnon impostata (inviti disattivati)Quante richieste AI al giorno UTC riceve l'account invitato. Imposta sia questa sia MEMBER_INVITE_ALLOWANCE_DAYS, oppure nessuna delle due.
MEMBER_INVITE_ALLOWANCE_DAYSnon impostata (inviti disattivati)Quanti giorni dopo la registrazione dura questa quota. Imposta sia questa sia MEMBER_INVITE_DAILY_AI_LIMIT, oppure nessuna delle due.
MEMBER_INVITE_LIFETIME_CAP5Quanti inviti può inviare in totale un membro, per sempre. Un numero intero pari a 0 o superiore. Richiede che le due variabili sopra siano impostate.

Le prime due si impostano entrambe o nessuna delle due. Se ne imposti solo una, l'avvio si blocca indicando quella mancante. Se non le imposti entrambe, comportamento predefinito, i membri non possono invitare nessuno e POST /v1/auth/invites restituisce 404 a chiunque. In tal caso generi tu direttamente ogni invito.

Ciò che l'invito concede è il periodo di prova. La persona invitata riceve il proprio account e il proprio diario, più MEMBER_INVITE_DAILY_AI_LIMIT richieste AI al giorno per MEMBER_INVITE_ALLOWANCE_DAYS giorni a partire dalla registrazione. Chiusa la finestra, il proxy AI risponde con 403. Il diario continua a funzionare. La sincronizzazione non è mai vincolata a una quota. Chi invita non decide nulla di tutto questo. Invia un indirizzo e niente altro.

Il limite conta le lettere inviate, non i successi. Ritirare un invito non lo restituisce. Il conteggio è per account, non per istanza. MEMBER_INVITE_LIFETIME_CAP=0 lascia la route attiva e non dà nulla da consumare a nessun membro. È diverso dal rimuovere la coppia di variabili e togliere la route. Considera questo limite assieme a AI_INSTANCE_DAILY_LIMIT. Prima di arrivare alla fattura del tuo provider, la quota giornaliera indicata sopra viene moltiplicata per ogni membro dell'istanza e per il limite.

Gli amministratori sono esenti. Il limite non si applica a loro, e non vale neanche la regola per cui un indirizzo che ha già consumato un invito da membro non può riceverne un secondo. Possono invitare da /admin tutte le volte che vogliono.

Un membro vede nell'app quanti inviti gli restano. La voce si trova in Impostazioni, Account, sotto Invita qualcuno. Quella sezione compare solo su un'istanza in cui la funzionalità è attiva.

Endpoint AI personalizzati

openplate-inference è un endpoint per foto del piatto compatibile con OpenAI, self-hosted, che esegui sul tuo hardware con modelli a pesi aperti, così nessuno ha bisogno di una chiave AI sul cloud. Questo strumento, Ollama, vLLM, LM Studio o qualunque altro software che supporti il protocollo OpenAI chat-completions si collegano tutti allo stesso modo: in Impostazioni → IA, aggiungi un provider openai-compatible e indica il tuo URL di base.

  • Un endpoint locale sulla stessa macchina (http://localhost:11434/v1 e simili) non richiede alcuna configurazione: l'eccezione per il loopback lo copre già.
  • Un endpoint remoto (un'altra macchina sulla tua LAN, o un server di inferenza ospitato da te) è bloccato dalla CSP per impostazione predefinita. Aggiungi la sua origine e riavvia l'app:
    bash
    echo "CSP_CONNECT_EXTRA=https://ai.example.com" >> .env
    docker compose -f compose.yml up -d
  • api.openai.com non è mai raggiungibile da un browser: l'API di OpenAI blocca le richieste cross-origin. Invia invece le richieste per i modelli OpenAI tramite OpenRouter, a prescindere da CSP_CONNECT_EXTRA.

AI fornita dall'istanza

Invece di chiedere a ogni visitatore di inserire una chiave, un'istanza può offrire il proprio endpoint.

Prima di impostare DEFAULT_INFERENCE_API_KEY, tieni a mente che è pubblica: è incorporata nell'HTML della pagina e chiunque possa aprire l'app può leggerla con view-source. La regola completa è il secondo punto qui sotto. Leggila prima.

Imposta DEFAULT_INFERENCE_BASE_URL (più DEFAULT_INFERENCE_MODEL, e DEFAULT_INFERENCE_API_KEY se l'endpoint ne richiede una) e nella pagina delle impostazioni AI e nella schermata di scansione comparirà un pulsante a tocco singolo per collegare "questo openplate fornisce la propria AI". Se la lasci non impostata, l'unica opzione sarà inserire la propria chiave; non verrà mostrato né inviato alcunché di aggiuntivo al browser.

Tre regole:

  • Deve essere un indirizzo che un BROWSER possa raggiungere. La foto va dal dispositivo all'endpoint, non passa mai attraverso il server openplate, quindi un hostname compose come http://openplate-inference:8080/v1 non funziona. Pubblica l'endpoint o mettilo dietro il tuo reverse proxy. La sua origine viene aggiunta alla CSP per te.
  • DEFAULT_INFERENCE_MODEL deve indicare un modello effettivamente servito dal tuo endpoint. Il valore predefinito, openplate-plate-1, è l'id servito da openplate-inference, e viene inviato alla lettera. Se invece punti l'URL di base su Ollama, vLLM o LM Studio, devi impostare questo valore sul nome servito da tale runtime, altrimenti ogni richiesta fallirà.
  • DEFAULT_INFERENCE_API_KEY è pubblica. Non viene conservata sul server: è incorporata nell'HTML della pagina e chiunque possa aprire l'app può leggerla con view-source. Va bene per un endpoint raggiungibile solo dalla tua rete domestica o dalla tua tailnet. non va bene per la chiave a consumo di un provider cloud, e non va bene su un'istanza esposta a tutto internet senza una VPN, una tailnet o un proxy di autenticazione davanti. Se il tuo endpoint non richiede una chiave, lasciala vuota.

Un valore DEFAULT_INFERENCE_BASE_URL non valido blocca intenzionalmente l'avvio, così un errore di battitura non sembrerà un "il pulsante non è mai apparso".

Questa preimpostazione dell'istanza (connectedVia: 'preset') viene configurata dal gestore di questa istanza per ogni visitatore. connectedVia: 'invite' è un valore correlato, ormai deprecato: indicava una riga di impostazioni AI generata dal vecchio flusso di inviti di openplate-gateway, rimosso in M192. Un'istanza gestita non scrive più quella riga: l'account stesso include la quota, e il proxy AI viene raggiunto tramite CORE_URL, non attraverso una voce separata nelle impostazioni.

Connessione con OpenRouter

Impostazioni → AI → Connetti con OpenRouter è un flusso OAuth (PKCE) via browser con un solo clic: nessuna chiave da copiare e incollare. Porta questa scheda alla schermata di consenso di OpenRouter e ritorno; approvala e la chiave emessa finisce direttamente nella memoria locale di questo browser. Il server di openplate non entra mai in questo passaggio: non vede, memorizza o fa mai da proxy per la chiave.

  • Imposta un limite di spesa già che ci sei. La schermata di consenso di OpenRouter offre un limite di spesa autonomo (facoltativo, con un intervallo di ripristino) accanto alla selezione dell'account. Definisce quanto la chiave collegata potrà mai spendere, a prescindere da ciò che fa openplate.
  • Il modello predefinito è google/gemini-3.5-flash-lite, un modello a pagamento, circa 0,001 $ per scansione. È stato scelto rispetto a qualunque modello :free perché gli endpoint :free di OpenRouter diventano raggiungibili solo dopo aver attivato, a livello di account per quel provider, le opzioni "può addestrare sui dati delle richieste" / "può pubblicare i prompt", e alcuni modelli di visione gratuiti conservano i prompt per settimane. Puoi comunque scegliere autonomamente un modello :free dall'elenco dei modelli, con un consenso esplicito e trasparente.
  • L'inserimento manuale della chiave funziona con qualsiasi provider, OpenRouter incluso, tramite il pannello "incolla una chiave API manualmente".
  • La chiave collegata compare nelle tue Impostazioni delle chiavi di OpenRouter con l'etichetta "Un'app". La disconnessione in openplate cancella la chiave solo da quel dispositivo: non la revoca su OpenRouter. Revocala tu stesso da quella pagina.
  • La stessa garanzia di qualunque altra chiave: salvata solo nello storage locale del dispositivo, esclusa dal backup/esportazione JSON, mai inviata al server openplate.

Funziona da qualsiasi origine sicura. L'URL di callback OAuth viene ricavato al momento della richiesta da window.location.origin. Non è mai hardcoded e non viene mai preregistrato con OpenRouter. Il pulsante funziona senza modifiche su http://localhost:3000 o sul tuo dominio https://. Su un semplice indirizzo LAN http:// non funziona, perché calcola l'hash del suo codice monouso con la Web Crypto API, che i browser disabilitano lì. Incolla invece una chiave a mano, oppure vedi self-hosting.md.

Una nota se utilizzi il self-hosting dietro un reverse proxy che registra gli URL delle richieste: l'URL di callback (compreso il suo parametro monouso state) apparirà nei tuoi log di accesso come qualunque altro URL. Non è un segreto (conoscerlo non concede l'accesso alla chiave di nessuno), ma rimuovilo se la conservazione dei tuoi log è motivo di preoccupazione.

Modifica questa pagina su GitHub