Data Backend
IronFlock provvede un database privato per ogni progetto, basato su TimescaleDB. La tua app definisce lo schema dati; IronFlock crea le tabelle e inizia a raccogliere dati nel momento in cui un dispositivo viene aggiunto all’app.
Come Funziona
- Definisci il tuo schema dati in
.ironflock/data-template.yml. - Usa l’IronFlock SDK per pubblicare dati dal tuo codice edge.
- IronFlock configura automaticamente le tabelle del database in ogni progetto dove l’app è installata.
- I dati fluiscono dai dispositivi attraverso il sistema di messaggistica nel database del progetto.
Ogni progetto ha il proprio database fisico — non c’è condivisione di dati tra progetti.
L’utente ha il pieno controllo sui dati raccolti dalla tua app nel suo progetto. Come sviluppatore, non hai accesso a questi dati.
Definire lo Schema Dati
Crea un file data-template.yml nella directory .ironflock/:
data:
tables:
- tablename: sensordata
columns:
- id: tsp
name: Timestamp
description: Timestamp of measurement
path: args[0].timestamp
dataType: timestamp
- id: temperature
name: Temperature
description: Temperature reading in Celsius
path: args[0].temperature
dataType: numeric
- id: humidity
name: Humidity
description: Relative humidity percentage
path: args[0].humidity
dataType: numeric
- id: device_id
name: Device ID
description: Source device identifier
path: args[0].device_id
dataType: stringOpzioni delle Colonne
| Campo | Descrizione |
|---|---|
id | Identificatore interno della colonna (usa tsp per le colonne timestamp) |
name | Nome leggibile della colonna mostrato nelle dashboard |
description | Descrizione opzionale |
path | Percorso al valore nell’oggetto dati pubblicato (es. args[0].temperature) |
dataType | Uno tra: timestamp, numeric, string, boolean |
secret | Cifra questa colonna a riposo e non la restituisce mai in chiaro in una lettura normale — vedi Colonne Segrete più avanti |
Colonne Segrete
Alcuni valori devono essere memorizzati ma non devono mai essere mostrati: un token API, la password di un dispositivo, una chiave di licenza. Contrassegna la colonna con secret: true e il data backend la cifra al momento dell’inserimento:
- tablename: credentials
columns:
- id: tsp
dataType: timestamp
- id: device_id
dataType: string
- id: api_token
dataType: string
secret: trueDa quel momento nessuna lettura ordinaria restituisce il valore in chiaro — nemmeno all’app che lo ha scritto:
| Percorso di lettura | Cosa ottieni in risposta |
|---|---|
| Letture di storico e righe in tempo reale: ciò che ricevono le board, i widget, l’SDK e le altre app che leggono la tabella | il segnaposto __secret__ |
| Accesso SQL (la console per le query SQL, le esportazioni di dati e il login Postgres di FleetDB Access) e backup del database | il testo cifrato memorizzato, ifsec:1:… |
| La funzione di rivelazione dell’SDK | il valore decifrato |
NULL resta NULL ovunque, così “impostato ma nascosto” rimane distinguibile da “mai scritto”.
Rileggere un segreto in chiaro è possibile solo dai container dell’app stessa, attraverso l’SDK — vedi Colonne Segrete nel riferimento dell’SDK. Le board e le altre app non possono ottenerlo in alcun modo, ed è questo che rende “mai visualizzato in chiaro” una garanzia reale e non solo formale.
Aggiornare una riga senza perderne il segreto. Su una tabella entità, un client che modifica una riga non può mai rispedire il segreto reale — le letture gli hanno sempre e solo consegnato il segnaposto. Il data backend tratta perciò il segnaposto __secret__ (e la maschera •••••••• visualizzata dalle board) come mantieni il valore precedente in scrittura, e anche una colonna segreta omessa lo mantiene. Inviare un null esplicito cancella il segreto. Modificare la descrizione di una macchina in un form della board, quindi, non ne cancella mai il codice di accesso.
Ne derivano due conseguenze: le stringhe letterali __secret__ e •••••••• non possono a loro volta essere memorizzate come valori segreti (e il testo cifrato grezzo ifsec:… viene rifiutato come input), e su una tabella senza chiave di entità una scrittura del segnaposto viene rifiutata — non esiste alcuna riga precedente da mantenere.
Vale la pena tenere conto di tre conseguenze, perché sono limiti invalicabili e non semplici raccomandazioni:
- Le colonne segrete sono solo stringhe. La cifratura produce testo, quindi le colonne
numeric,booleanetimestampnon possono essere segrete. Nemmeno la colonna obbligatoriatsppuò essere segreta. - Non puoi filtrare, raggruppare o ordinare per una colonna segreta. Ogni riga è cifrata con un proprio valore casuale, quindi due righe che contengono lo stesso segreto memorizzano testi cifrati diversi. Filtri di uguaglianza,
GROUP BY,ORDER BYeDISTINCTsu una colonna segreta semplicemente non possono funzionare. Per rispondere alla domanda “questo valore corrisponde?”, usa la funzione di verifica dell’SDK, che effettua il confronto all’interno del data backend invece di restituire alcunché. - Una colonna segreta non può essere una chiave di entità. Poiché il testo cifrato di ogni riga è diverso, le letture della riga più recente per entità tratterebbero ciascuna riga come un’entità a sé, quindi indicarne una in
maintainLatestFlagForviene rifiutato senza appello.
Le ultime due restrizioni vengono controllate durante la validazione del data template, perciò una tabella che le viola fallisce al momento del rilascio invece di comportarsi in modo scorretto in seguito.
Opzioni delle Tabelle
Oltre a columns, una tabella accetta alcune chiavi opzionali che controllano come viene descritta e come invecchiano i suoi dati:
data:
tables:
- tablename: sensordata
description: Letture ambientali dal reparto produzione
chunkTimeInterval: 1 hour
dropAfter: 30 days
columns:
# ...| Campo | Descrizione |
|---|---|
tablename | Nome della tabella |
description | Descrizione opzionale, mostrata nell’interfaccia e usata dagli agenti AI per comprendere la tabella |
chunkTimeInterval | Dimensione delle partizioni temporali in cui la tabella viene suddivisa. Predefinito 7 days |
dropAfter | Finestra di conservazione — le partizioni più vecchie vengono eliminate automaticamente |
downsample | Mantiene una copia pre-aggregata per grafici rapidi su finestre lunghe — vedi Downsampling Continuo più avanti |
maintainLatestFlagFor | Colonne che identificano un’entità univoca — vedi Tracciare lo Stato Corrente di un’Entità più avanti |
private | Nasconde questa tabella alle altre app — vedi Condividere Dati con Altre App più avanti |
chunkTimeInterval controlla come i dati time-series vengono partizionati su disco. Scegli un valore per cui una partizione corrisponda all’incirca a ciò che interroghi in una volta: i dati ad alta frequenza raccolti ogni secondo traggono vantaggio da chunk piccoli (da minuti a ore), quelli che cambiano lentamente da chunk grandi (settimane). È solo il valore predefinito dell’app — il proprietario del progetto può modificarlo in seguito sul proprio data backend.
dropAfter trasforma la tabella in una finestra scorrevole. Vengono eliminate intere partizioni più vecchie dell’intervallo indicato, il che è molto più economico che cancellare singole righe. Il job di pulizia viene eseguito con cadenza dropAfter / 4, quindi un record può sopravvivere alla propria scadenza fino a un quarto dell’intervallo prima che la sua partizione venga rimossa. Ometti dropAfter per conservare i dati a tempo indeterminato.
Entrambi accettano stringhe di intervallo PostgreSQL — 30 minutes, 1 hour, 7 days, 6 months.
Downsampling Continuo
Le dashboard possono chiedere al database di aggregare i dati — medie orarie, totali giornalieri, conteggi per macchina. Calcolarli dai record grezzi va benissimo per un giorno ed è costoso per un anno. Aggiungi downsample a una tabella e la piattaforma ne mantiene una copia pre-aggregata, aggiornata di continuo, e risponde da quella copia alle query su finestre lunghe:
data:
tables:
- tablename: sensordata
dropAfter: 30 days
downsample:
bucket: 1 minute
keepFor: 2 years
paths:
- payload.temperature
columns:
# ...| Campo | Descrizione |
|---|---|
bucket | Granularità della copia pre-aggregata. Predefinito 1 minute |
keepFor | Per quanto tempo conservare la cronologia sottocampionata. Ometti per conservarla a tempo indeterminato |
paths | Percorsi dei campi JSON da includere, nella stessa notazione usata dalle dashboard |
bucket è la risoluzione più fine con cui può essere servito un grafico — un grafico che richiede intervalli molto più fini legge invece la tabella grezza. Accetta intervalli a larghezza fissa da 1 second a 1 day che dividono la giornata in modo esatto (1 minute, 5 minutes, 1 hour). Il valore predefinito di 1 minute è adatto praticamente a ogni dashboard; un bucket più grossolano costa meno spazio di archiviazione e meno throughput in scrittura.
keepFor è ciò che rende possibili le cronologie lunghe. I record grezzi spariscono con dropAfter, ma la copia sottocampionata ha una propria conservazione: conserva i dati grezzi per 30 giorni e quelli sottocampionati per 2 anni, e una board potrà comunque tracciare due anni di medie orarie occupando una frazione dello spazio. Impostalo su un valore più lungo di dropAfter — la piattaforma rifiuta il caso opposto come configurazione errata.
paths estende il downsampling ai valori all’interno delle colonne JSON. Le colonne numeriche vengono incluse automaticamente; i campi JSON devono essere indicati esplicitamente, poiché una colonna JSON non ha un insieme fisso di chiavi. I campi non dichiarati funzionano comunque nelle dashboard — vengono semplicemente calcolati dalla tabella grezza.
Tutto il resto è automatico. Per ogni colonna numerica vengono mantenute le statistiche (media, somma, minimo, massimo, primo, ultimo e un conteggio dei record), raggruppate per la chiave di entità della tabella (maintainLatestFlagFor, oppure il dispositivo che pubblica). Le dashboard non richiedono alcuna configurazione né devono esserne a conoscenza: un widget interroga come sempre e la piattaforma decide query per query se la copia pre-aggregata è in grado di rispondere — ricadendo in modo trasparente sulla tabella grezza quando non lo è, ad esempio quando un filtro fa riferimento a una colonna per cui la copia non raggruppa.
Le modifiche di schema ricostruiscono la copia. Aggiungere, rimuovere o cambiare il tipo di una colonna di una tabella sottocampionata — o modificare il blocco
downsamplestesso — comporta la ricostruzione della copia pre-aggregata a partire dalla tabella grezza. Tutto ciò che è più vecchio didropAfternon può essere ricostruito e va perso. Dove possibile, imposta il blocco insieme alla tabella e considera le modifiche di schema successive su tabelle di lunga durata come una decisione consapevole.
Pubblicare Dati dal Codice Edge
Usa l’IronFlock SDK per inviare dati dalla tua app:
Python
from ironflock import IronFlock
flock = IronFlock()
await flock.start()
await flock.publish_to_table("sensordata", {
"timestamp": "2025-01-15T10:30:00Z",
"temperature": 23.5,
"humidity": 62.1,
"device_id": "sensor-001"
})Ogni riga ha bisogno di un valore per la colonna obbligatoria tsp: il data backend rifiuta una riga che ne è priva. La scrittura in tabella dell’SDK fallisce con un errore; una riga pubblicata da una board viene scartata, lasciando solo una voce nella tabella error-logs dell’app. La definizione di sensordata qui sopra mappa questa colonna con path: args[0].timestamp, quindi tsp proviene dal campo timestamp della riga. Una colonna senza path legge il campo omonimo della riga (args[0].<id>): senza questa mappatura, ogni riga avrebbe bisogno di un proprio campo tsp.
Per dati ad alta frequenza, invia molte righe in un singolo messaggio invece di un round-trip per riga usando publish_rows_to_table / publishRowsToTable / PublishRowsToTable o append_rows_to_table / appendRowsToTable / AppendRowsToTable — i due metodi sono identici e restituiscono l’esito dell’inserimento. Ogni batch viene inserito in modo atomico — tutto-o-niente. Consulta il riferimento dell’SDK per i dettagli.
Tabelle di Trasformazione
Puoi definire trasformazioni SQL che aggregano o elaborano automaticamente i tuoi dati grezzi:
data:
tables:
- tablename: sensordata
columns:
# ... raw data columns ...
transforms:
- tablename: hourly_averages
materialize: true
schedule: "0 * * * *"
sql: >
SELECT
time_bucket('1 hour', tsp) AS hour,
avg(temperature) AS avg_temp,
avg(humidity) AS avg_humidity
FROM sensordata
GROUP BY hour
columns:
- id: hour
name: Hour
dataType: timestamp
- id: avg_temp
name: Average Temperature
dataType: numeric
- id: avg_humidity
name: Average Humidity
dataType: numeric| Campo | Descrizione |
|---|---|
tablename | Nome della tabella derivata |
materialize | Se true, il risultato viene memorizzato e aggiornato secondo la pianificazione. Se false, la trasformazione è una vista, calcolata a ogni lettura |
schedule | Espressione cron per le esecuzioni della trasformazione: cinque campi (minuto, ora, giorno del mese, mese, giorno della settimana), o sei con un campo iniziale per i secondi. */5 * * * * viene eseguita ogni 5 minuti, */10 * * * * * ogni 10 secondi. Ogni esecuzione aggiorna un risultato materializzato e invia il risultato corrente alle board collegate alla trasformazione. Il codice dell’app può anche avviare un’esecuzione di questo tipo su richiesta, vedi Aggiornare su Richiesta |
sql | Query SQL che calcola la trasformazione |
columns | Definizioni delle colonne per l’output |
Le tabelle di trasformazione sono accessibili nelle dashboard e tramite l’SDK, esattamente come le tabelle normali.
Una trasformazione viene letta esattamente come il suo SQL la restituisce: le finestre temporali dei widget, i filtri di calendario e la modalità latest si applicano alle tabelle e per una trasformazione vengono ignorati. Da qui derivano tre regole. Limita l’intervallo temporale all’interno della query (WHERE tsp > now() - interval '7 days'), ordina una serie temporale dalla più recente (ORDER BY <time column> DESC) così che un limite di righe mantenga le righe più recenti, e seleziona ogni colonna che una dashboard deve rappresentare o usare come filtro — una trasformazione non ha colonne timestamp o dispositivo implicite. Una singola lettura restituisce al massimo 3000 righe, quindi aggrega nella query.
Una trasformazione serve per ciò che una lettura di tabella non può esprimere: un join, un rapporto, un’aggregazione sulle righe correnti di più tabelle. Non è il modo per mostrare lo stato corrente delle entità o per nascondere i record eliminati. Quelle sono modalità di lettura della tabella stessa, l’interruttore latest e un filtro su una colonna deleted (vedi sotto), e funzionano live: un widget collegato alla tabella segue ogni nuova riga entro un secondo. Una trasformazione raggiunge una dashboard solo secondo la sua pianificazione (o quando il codice dell’app la aggiorna dopo le proprie scritture, cosa che un salvataggio da una dashboard non provoca mai), quindi una vista che seleziona la riga più recente per entità o scarta le righe eliminate mostra lo stesso risultato in ritardo. Quando una trasformazione ha davvero bisogno dello stato corrente di una tabella entità, ad esempio per unirla a un’altra tabella, selezionalo nel suo SQL con DISTINCT ON e applica lì il controllo su deleted, nell’ordine mostrato in Eliminazione Logica dei Record.
Distribuire una trasformazione insieme all’app non è l’unico modo per averne una: un membro del progetto con il privilegio Data Access può salvare lo stesso tipo di trasformazione dalla vista dati del progetto, senza alcuna app, e lo stesso può fare l’assistente AI. Vedi Trasformazioni personalizzate.
Quando Viene Aggiornata una Trasformazione Materializzata
Un’esecuzione pianificata aggiorna una trasformazione materializzata solo quando in una tabella che legge è stato scritto qualcosa dall’ultimo aggiornamento, e comunque almeno ogni 10 minuti. Un’esecuzione senza nulla di nuovo da leggere lascia invariato il risultato memorizzato: una trasformazione pianificata ogni minuto su una tabella che riceve una riga all’ora si aggiorna dopo quella riga, e altrimenti ogni 10 minuti.
Una trasformazione materializzata si aggiorna a ogni esecuzione quando il suo risultato può cambiare senza una scrittura:
- il suo SQL usa l’ora corrente o valori casuali, come
now(),CURRENT_DATEorandom(); - chiama una funzione che può restituire un risultato diverso per lo stesso input: una funzione volatile, oppure una funzione stabile che non è integrata in PostgreSQL;
- legge qualcosa di diverso da tabelle di app e trasformazioni materializzate, per esempio una trasformazione non materializzata, una copia sottocampionata o una delle tabelle della piattaforma stessa.
Quando molte trasformazioni arrivano a scadenza nello stesso momento, la piattaforma ne aggiorna poche alla volta e le altre attendono il proprio turno, quindi un aggiornamento può terminare più tardi dell’orario pianificato. Un’esecuzione che arriva a scadenza mentre l’aggiornamento precedente della stessa trasformazione è ancora in corso o in attesa viene saltata.
Aggiornare su Richiesta
Il codice dell’app stessa può avviare l’esecuzione di una trasformazione senza attendere la sua pianificazione: refresh_transform / refreshTransform / RefreshTransform dell’SDK aggiorna il risultato materializzato (una semplice vista viene solo letta) e invia il risultato corrente alle board collegate alla trasformazione, così una board mostra subito un batch che l’app ha appena scritto. Valgono le stesse regole di un’esecuzione pianificata: l’aggiornamento viene saltato — e segnalato come saltato — quando in nessuna tabella che la trasformazione legge è stato scritto qualcosa dall’ultimo aggiornamento, una chiamata che arriva mentre un aggiornamento della stessa trasformazione è in corso attende quello in coda dietro di esso, e per trasformazione parte al massimo un aggiornamento al secondo. Le scritture nelle tabelle dell’SDK sono confermate quando ritornano, quindi un aggiornamento che segue una scrittura non la perde mai. Solo i container dell’app stessa possono aggiornare le sue trasformazioni; una board riceve le righe, e un’app che consuma i dati di un’altra app legge le sue trasformazioni ma non può aggiornarle. Le trasformazioni salvate dalla vista dati del progetto non appartengono a nessuna app e vengono eseguite solo secondo la loro pianificazione. Vedi refreshTransform / refresh_transform.
Tracciare lo Stato Corrente di un’Entità
Per le tabelle che rappresentano lo stato corrente di entità del mondo reale — macchine, asset, ordini di produzione — IronFlock supporta un pattern chiamato tracciamento dello stato corrente.
Invece di sovrascrivere una riga quando qualcosa cambia, si aggiunge sempre una nuova riga. Dichiari quali colonne identificano un’entità univoca e IronFlock deriva la riga più recente per ogni entità ogni volta che la tabella viene letta. Questo ti dà un’intera cronologia di ogni modifica rendendo allo stesso tempo facile interrogare solo lo stato corrente.
Abilitala su una tabella con maintainLatestFlagFor:
- tablename: machineform
maintainLatestFlagFor: ['machinename']
columns:
- id: tsp
dataType: timestamp
- id: machinename
dataType: string
- id: machinetype
dataType: string
- id: active
dataType: boolean
- id: description
dataType: stringmaintainLatestFlagFor accetta una lista di colonne che insieme identificano un’entità univoca. Nulla viene scritto nella riga stessa: IronFlock indicizza la tabella in base a questa chiave di entità più il timestamp e seleziona la riga più recente per ogni entità al momento della query. Una riga che arriva in ritardo o fuori ordine non può quindi mai lasciare dietro di sé un marcatore obsoleto.
Una tabella entità contiene una riga per entità e timestamp. Una riga che ripete sia la chiave di entità sia il tsp di una riga memorizzata viene rifiutata, e lo stesso vale per un batch in cui due righe lo fanno: la scrittura dell’SDK fallisce con URI_ENTITY_KEY_CONFLICT e la tabella error-logs dell’app registra il rifiuto. Assegna a ogni versione di un’entità il proprio tsp. Una riga la cui colonna della chiave di entità è vuota (NULL) non appartiene ad alcuna entità: non viene mai rifiutata come ripetizione, e le letture dello stato corrente la escludono.
Aggiornare un’Entità con una Riga Parziale
Una riga accodata a una tabella entità è una nuova versione di quell’entità — e non deve necessariamente essere completa. Le colonne che la riga non fornisce vengono ereditate dalla riga più recente precedente dell’entità, quindi aggiornare un singolo campo significa pubblicare soltanto la chiave di entità, un timestamp e quel campo:
| La colonna della nuova riga è | La versione memorizzata contiene |
|---|---|
fornita (0, false e "" contano come valori forniti) | il valore fornito |
esplicitamente null | NULL — la colonna viene svuotata |
| assente | il valore della riga più recente precedente |
Per svuotare una colonna, inviala come null (None in Python, nil in Go). Ometterla mantiene il valore precedente. Fai attenzione al codice che omette un campo senza volerlo: una proprietà JavaScript con valore undefined, per esempio, viene scartata dal payload. Il riferimento dell’SDK elenca questi casi per linguaggio.
Vale la pena conoscere due dettagli:
- Una riga che fornisce tutte le colonne salta completamente la ricerca della riga precedente, quindi le righe complete restano esattamente economiche quanto lo sono sempre state — continua a inviare righe complete sui percorsi ad alta frequenza.
- L’ereditarietà rispetta il tempo: una riga che arriva con un timestamp più vecchio (un backfill) eredita solo dalle righe con
tsppari o anteriore al proprio, mai da quelle più recenti.
Le tabelle senza maintainLatestFlagFor mantengono la semantica di semplice append: una colonna che la riga non fornisce viene memorizzata come NULL.
Per interrogare solo gli stati correnti delle macchine:
SELECT DISTINCT ON (machinename) *
FROM machineform
ORDER BY machinename, tsp DESCPer visualizzare l’intera cronologia di una macchina specifica:
SELECT * FROM machineform WHERE machinename = 'Assembly-Line-01' ORDER BY tspRaramente dovrai scrivere questa query a mano, e non dovresti memorizzarla come trasformazione. I widget su una dashboard che si collegano a questa tabella dispongono di un interruttore latest nelle impostazioni del filtro, in modo che gli utenti vedano sempre i valori correnti senza lavoro extra. L’interruttore è live: il widget riceve ogni nuova riga della tabella man mano che arriva e con essa sostituisce la riga precedente dell’entità, quindi una modifica compare entro un secondo dalla scrittura, mentre la tabella conserva ogni versione. Nulla viene ricalcolato secondo una pianificazione, che è tutto ciò che una trasformazione aggiungerebbe. Dall’SDK, richiedi la stessa modalità aggiungendo {"latest": true} a filterAnd — consulta getHistory.
Migrazione da
latest_flag: le versioni precedenti di IronFlock memorizzavano una colonna booleana fisica chiamatalatest_flag. Quella colonna non esiste più — lo stato corrente viene invece derivato in SQL, il che lo mantiene corretto quando le righe arrivano fuori ordine. Le dashboard e le chiamate SDK esistenti che filtrano perlatest_flag = truecontinuano a funzionare: IronFlock le riconosce e applica la modalità stato corrente. Il codice nuovo dovrebbe usare l’interruttore latest o la voce di filtro{"latest": true}.
Modificare la Chiave di Entità
Una volta rilasciata una versione dell’app, la chiave di entità di ciascuna delle sue tabelle è fissata. Aggiorna struttura dati nel pannello laterale del Cloud IDE e la build di una nuova release rifiutano un data-template.yml che modifica la chiave di entità di una tabella già presente nella release più recente dell’app:
- aggiungere una colonna a
maintainLatestFlagFor, rimuoverne una, sostituirne una o cambiarne l’ordine; - aggiungere
maintainLatestFlagFora una tabella che non lo aveva, oppure rimuoverlo.
Una modifica del genere cambierebbe silenziosamente ciò che la tabella restituisce in ogni progetto che ha installato l’app. Una colonna nuova nella chiave è vuota in tutte le righe esistenti, e le letture dello stato corrente escludono le righe con una colonna della chiave vuota, quindi tutte le entità esistenti scomparirebbero. Una colonna rimossa dalla chiave può fondere entità le cui righe condividono un timestamp, mentre la tabella può contenere una sola riga per entità e timestamp.
Per usare una chiave di entità diversa, crea una nuova tabella con la nuova chiave e sposta i dati al suo interno, per esempio dal codice della tua app. Lo spostamento dei dati spetta all’app. Una tabella non ancora presente in alcuna release può comunque cambiare chiave di entità durante lo sviluppo. Una versione dell’app rilasciata prima che esistesse questa regola può ancora modificare una chiave di entità: un progetto che si aggiorna a quella versione rifiuta l’aggiornamento della struttura dati e mantiene quella precedente.
Righe che Ripetono un’Entità e un Timestamp
Le tabelle scritte prima che IronFlock imponesse una riga per entità e timestamp possono ancora contenere ripetizioni, e lo stesso vale per una tabella di sviluppo di cui hai modificato la chiave di entità. Quando il servizio dati configura una tabella di questo tipo, al proprio avvio o dopo un aggiornamento della struttura dati, sposta tutte le righe di ogni gruppo ripetuto in una tabella chiamata <table>__entitykey_dupes nello stesso data backend e rimette nella tabella una riga di ogni gruppo. Da quel momento la tabella rifiuta le ripetizioni. Una voce nella tabella error-logs dell’app indica quante righe sono state spostate.
La nuova tabella conserva ogni riga spostata così com’era, più due colonne: quarantined_at, il momento dello spostamento, e restored, che contrassegna la riga rimessa al suo posto. Quale riga torna al suo posto è arbitrario, come lo era già per le letture dello stato corrente. Interrogala nella console per le query della vista dati del progetto come databackend_<key>.<table>__entitykey_dupes. Se la tabella originale è privata, anche la nuova tabella è privata. Se le righe non possono essere spostate, la voce in error-logs ne indica il motivo, e il servizio dati riprova al suo avvio successivo.
Eliminazione Logica dei Record
Il modello append-only di IronFlock significa che i record non vengono mai eliminati fisicamente. Invece, usa una colonna booleana deleted per marcare un record come rimosso. Questo preserva la traccia di audit completa nascondendo i record eliminati dalle dashboard.
Aggiungi una colonna deleted a qualsiasi tabella entità:
- id: deleted
name: Deleted
dataType: booleanQuando un utente elimina un record (ad esempio tramite un form sulla dashboard), la tua app pubblica una nuova riga per quell’entità con deleted: true. Combinato con maintainLatestFlagFor, questa nuova riga diventa lo stato più recente. Le righe parziali successive ereditano il marcatore deleted come qualsiasi altra colonna, quindi un aggiornamento che non menziona deleted non resuscita l’entità né la nasconde.
Per interrogare solo i record attivi (non eliminati) correnti:
SELECT * FROM (
SELECT DISTINCT ON (machinename) *
FROM machineform
ORDER BY machinename, tsp DESC
) latest
WHERE deleted IS NULL OR deleted = falseIl controllo su deleted viene eseguito dopo che è stata selezionata la riga più recente di ogni macchina. Quest’ordine è importante: filtrare prima le righe eliminate farebbe riemergere come stato corrente la riga precedente, non eliminata.
I widget della dashboard e l’SDK applicano automaticamente lo stesso ordine — combina l’interruttore latest (o {"latest": true}) con un filtro su deleted e ottieni esattamente questo comportamento. I record eliminati spariscono dalla dashboard immediatamente dopo l’invio del form, ma rimangono nel database per la cronologia e scopi di audit.
Sulla dashboard, un form elimina un record tramite la sua impostazione Delete-Flag Column, che indica la colonna deleted: il form scrive true all’eliminazione e false a ogni salvataggio, quindi salvare un record eliminato lo ripristina (vedi Eliminazione dei Dati). I widget che elencano i record filtrano la tabella stessa. Non serve alcuna vista o trasformazione per ripulire l’elenco, e una mostrerebbe un’eliminazione solo alla sua prossima esecuzione pianificata.
Condividere Dati con Altre App
Il tuo data backend è privato della tua app: nessun’altra app installata nel progetto può vedere le tue tabelle. Due chiavi opzionali in data-template.yml cambiano questo.
Per leggere i dati di un’altra app, elenca le app da cui vuoi leggere in una sezione consumes: di primo livello — accanto a data:, non al suo interno:
consumes:
- app: machine-monitor
reason: "Calcola l'OEE dai flussi di stato macchina e contatori del monitor"
data:
tables:
- tablename: oee_results
columns:
# ... le tabelle della tua app, come sempreapp è il nome tecnico dell’app fornitrice, oppure "*" (le virgolette sono obbligatorie) per tutte le app del progetto. reason viene mostrata all’utente nella finestra di consenso — la dichiarazione da sola non concede nulla finché non viene approvata.
Per tenere per te singole tabelle, marcale con private: true. Tutto ciò che definisci è condivisibile per impostazione predefinita; una tabella o trasformazione privata non compare mai nel catalogo che vedono le altre app.
data:
tables:
- tablename: measurements # condivisa (predefinito)
columns: [ ... ]
- tablename: calibration_state # interna — mai visibile alle altre app
private: true
columns: [ ... ]L’accesso è in sola lettura, concesso dall’utente per progetto e revocabile in qualsiasi momento. Vedi Consumare Dati da Altre App per il modello completo e le chiamate SDK che leggono la cronologia e i flussi live di un’app fornitrice.
Accesso Privilegiato
I container di un’app girano isolati dal dispositivo, con i diritti predefiniti di Docker. Alcune app hanno bisogno di più: una porta seriale o un adattatore USB, GPIO, un’interfaccia CAN o Bluetooth, la rete dell’host. Questo è l’accesso privilegiato. Un’app con accesso privilegiato può controllare l’intero dispositivo, comprese le altre app e i loro dati, quindi la tua app deve richiederlo e l’utente decide se concederlo.
Per richiederlo, aggiungi una sezione privileged: di primo livello con un reason — accanto a data:, non al suo interno:
privileged:
reason: "Legge il PLC tramite la porta seriale /dev/ttyUSB0"
data:
tables:
# ... le tabelle della tua app, come semprereason è obbligatorio. Viene mostrato all’utente nella finestra di consenso, quindi scrivilo per una persona che decide se fidarsi della tua app.
Un’app senza tabelle scrive data: {} accanto a privileged: un data-template.yml senza la chiave data: non si può pubblicare.
Ciò che l’utente concede dipende da come è costruita l’app:
- Un’app a immagine singola (solo un
Dockerfile) viene eseguita in modalità privilegiata quando l’utente lo concede: con tutte le capability di Linux, il/devdel dispositivo e il D-Bus dell’host. Senza la dichiarazione gira senza i dispositivi dell’host, senza capability aggiuntive e senza il D-Bus dell’host; la release viene pubblicata comunque, e il suo log di build lo segnala. - Un’app Docker Compose riceve esattamente le impostazioni richieste dal suo
docker-compose.yml, e la finestra di consenso le elenca. Se il file compose usa una delle impostazioni qui sotto, l’app deve dichiarareprivileged, altrimenti la pubblicazione della release fallisce con un errore che indica il servizio e l’impostazione. Anche dichiarareprivilegedquando il file compose non ne usa nessuna fallisce.
Queste impostazioni compose contano come privilegiate:
| Impostazione | Privilegiata quando |
|---|---|
privileged | qualsiasi valore tranne false |
cap_add, devices, device_cgroup_rules | ha una voce |
security_opt | ha una voce diversa da no-new-privileges |
network_mode, pid, ipc, uts, userns_mode, cgroup | è impostata a host o container:… |
cgroup_parent | è impostata |
volumes di un servizio | monta un percorso dell’host (/…, ~/…, C:\…), un percorso fuori dalla cartella dell’app (../…) o una named pipe. /etc/localtime e /etc/timezone in sola lettura sono ammessi. |
volumes_from | indica container:… |
volumes di primo livello | un volume ha driver_opts, external o un name fisso |
networks di primo livello | una rete imposta un driver diverso da bridge (come macvlan, ipvlan o host) o dei driver_opts, è external o ha un name fisso |
secrets, configs di primo livello | una voce legge un file fuori dalla cartella dell’app |
I volumi con nome, le reti con il driver predefinito bridge, i bind mount all’interno della cartella dell’app (./config:/config), le porte pubblicate e le prenotazioni di GPU (gpus, deploy.resources.reservations.devices) non sono privilegiati.
Un segnaposto ${VAR} in una di queste impostazioni è ammesso e conta come privilegiato, quindi richiede anch’esso la dichiarazione. Così un parametro dell’app può far scegliere la porta seriale agli utenti:
services:
plc:
build: .
devices:
- "${SERIAL_PORT}:/dev/ttyS0"Un file compose che usa include, o extends con un file, non si può pubblicare, con o senza dichiarazione: una release contiene solo il docker-compose.yml. Unisci gli altri file al suo interno.
Non chiedere più di quanto serve all’app. Se le serve una porta seriale, scrivi un file compose con un solo servizio e devices: [/dev/ttyUSB0] invece di un’immagine singola o di privileged: true. L’utente vede esattamente cosa riceve l’app, e l’app non può fare di più.
Consenso e Revoca
- All’installazione, l’utente vede il tuo
reasone, per un’app compose, l’elenco delle impostazioni, e attiva o disattiva l’accesso privilegiato. Rifiutare non blocca mai l’installazione. - A un aggiornamento, se la nuova versione richiede l’accesso privilegiato e il progetto non l’ha concesso, la finestra di aggiornamento lo chiede. Anche in caso di rifiuto l’app viene aggiornata; la nuova versione gira poi senza accesso privilegiato.
- In seguito, l’accesso privilegiato è un interruttore nelle impostazioni delle app del progetto, dove gli utenti possono revocarlo o concederlo in qualsiasi momento. I dispositivi in esecuzione riavviano l’app con la nuova impostazione.
Un’esecuzione di sviluppo dall’IDE cloud segue la dichiarazione e non ha un interruttore: la build di sviluppo gira in modalità privilegiata quando .ironflock/data-template.yml dichiara privileged, altrimenti senza accesso privilegiato. Se il file compose usa impostazioni privilegiate senza la dichiarazione, la build di sviluppo fallisce con lo stesso errore di una pubblicazione.
Senza il Consenso
L’app si avvia comunque. Un’app a immagine singola gira con le impostazioni predefinite di Docker: niente /dev, nessuna capability aggiuntiva, niente D-Bus dell’host. Per un’app compose, l’agente del dispositivo rimuove le impostazioni privilegiate dal file compose prima di avviare l’app e scrive nel log dell’app quali impostazioni ha rimosso da quale servizio. Fai in modo che il tuo codice segnali ciò che non riesce a raggiungere, come una porta seriale che non può aprire, invece di andare in crash.
Release Precedenti
Le release pubblicate prima che le app dovessero dichiarare l’accesso privilegiato contano come se lo richiedessero, e le installazioni esistenti lo mantengono, quindi queste app funzionano come prima. Gli utenti possono revocarlo nelle impostazioni delle app del progetto, come per qualsiasi altra app. Dalla tua prossima release, dichiara privileged se l’app ne ha bisogno.