Vai al contenuto
SIMONE PIZZI
Progetti ↗
Runtime TelegramBot Desktop Titan Edition

Da IronShield a Titan Blue: otto versioni in otto giorni

Avevamo parlato di IronShield? Avevamo annunciato con orgoglio la soluzione a tutti i mali? Forse l'avevamo fatto troppo presto ma poi ci siamo arrivati.

Data
Lettura
9 minuti
Sezioni
6
Copertina: Da IronShield a Titan Blue: otto versioni in otto giorni

Wild Coding · 29 Aprile 2026


Il 20 aprile avevo scritto che il mostro dello spamming era stato ricacciato nel database dei ricordi. Lo smartphone aveva smesso di vibrare. IronShield reggeva.

Avrei dovuto fermarmi lì, almeno per qualche giorno.

Invece mi sono seduto davanti al codice e ho ricominciato a leggere.


L'audit che nessuno aveva chiesto

Non c'era un bug da correggere. Non c'era una feature in roadmap. C'era solo quella cosa che ogni tanto ti prende dopo una sessione di sviluppo intensa: la necessità di rileggere tutto, dall'inizio, con calma. Chiamatelo audit post-lancio, chiamatelo paranoia professionale. Io lo chiamo sentire che il codice non ti ha ancora detto tutto quello che sa.

Ho aperto engine.ts, parser.ts, schema.ts. Ho letto ogni funzione come se l'avesse scritta qualcun altro. E in effetti, in un certo senso, era così.

La prima cosa che ho trovato riguardava la Digest Mode.

Il meccanismo è semplice in teoria: invece di inviare ogni articolo singolarmente, il bot accumula gli item di un feed per un certo intervallo di tempo e poi li spedisce in un unico messaggio riepilogativo. Pulito, ordinato, utile. Peccato che nel codice, il timer che registrava l'ultimo invio del digest scattasse prima che Telegram confermasse il successo dell'operazione. Se l'invio falliva — per un problema di rete, per un rate-limit — il timer avanzava lo stesso. Al ciclo successivo, il bot si svegliava convinto di aver già fatto il suo lavoro. Gli item aspettavano nella coda. Nessuno li pubblicava.

Un bug silenzioso. Il tipo che non causa errori visibili, non rompe niente di ovvio, ma svuota lentamente l'affidabilità di un sistema.

Fix: prima si invia, poi si aggiorna il timer. Sembra banale. Ma non si trova senza leggere il codice con attenzione.


La seconda cosa che ho trovato era tecnicamente più sottile, e in retrospettiva più inquietante.

In JavaScript, new Date("qualcosa di non parsabile") non lancia un'eccezione. Restituisce un oggetto Date perfettamente valido, con una proprietà getTime() che vale NaN. E NaN <= x è sempre false in JavaScript, qualsiasi sia x.

Traduzione pratica: un articolo RSS con una data malformata — un campo vuoto, una stringa nel formato sbagliato, un valore corrotto — superava silenziosamente entrambi i controlli temporali che IronShield aveva appena blindato. Il filtro cutoff del 2000, il confronto con la start_date del bot: tutto saltato, come se non esistesse. L'item entrava in coda e veniva pubblicato.

La cosa più fastidiosa non era il bug in sé. Era che il safety check creato proprio per proteggere IronShield ricreava l'indice di deduplica usando la definizione vecchia, quella precedente alla v1.10.5. Un database che passava da quel path finiva con l'indice sbagliato, e la deduplica globale che avevamo faticato tanto a implementare non funzionava.

Avevo scritto un sistema di sicurezza con una falla nel sistema di sicurezza del sistema di sicurezza.

Questa versione si chiama v1.10.6 "SteelCore". Il nome era un modo per dire: adesso il nucleo è davvero solido.


Il silenzio che nessuno sentiva

La v1.10.7 "SilentGuard" nasce da una domanda che avrei dovuto farmi prima: cosa succede a un articolo che arriva mentre il bot è in Quiet Hours?

Le Quiet Hours sono una delle funzioni più utili di Titan: puoi dire al bot di non pubblicare nulla tra le 23:00 e le 7:00, per esempio. Semplice, rispettoso. Peccato che il meccanismo originale, quando incontrava un item nuovo durante il silenzio, lo scartasse con un continue e andasse avanti.

Se il feed aveva un backlog corto — diciamo che mostra solo gli ultimi cinque articoli — e nel frattempo ne arrivavano di nuovi, quelli che erano stati saltati durante il silenzio sparivano dal feed prima che le Quiet Hours finissero. Nessun errore. Nessun log di allarme. Solo contenuto perso, in modo pulito e invisibile.

Per un software il cui scopo principale è non perdere notizie, era un punto critico.

La soluzione è stata introdurre una nuova tabella SQLite: pending_queue. Una coda persistente dove gli item vengono parcheggiati durante il silenzio e recuperati, nell'ordine corretto, alla riapertura. Sopravvive ai riavvii. Gestisce i casi limite — feed eliminati nel frattempo, item già processati da un altro path, Quiet Hours che rientrano a metà drenaggio. Lo schema del database sale alla versione 11.

Adesso, quando il bot si sveglia la mattina, sa esattamente cosa aveva messo da parte la sera prima.


YouTube, il locale e la regressione che mi sono costruito da solo

Qui la storia si fa più istruttiva, nel senso che insegna qualcosa sull'effetto collaterale delle buone intenzioni.

Il problema di partenza era reale: su macchine con sistema operativo in tedesco, spagnolo o francese, YouTube InnerTube restituiva le date in formato localizzato — "vor 3 Stunden", "hace 2 días" — che il parser non riconosceva. Risultato: fallback alla data 2000-01-01, attivazione del filtro IronShield, item validi scartati.

La soluzione sembrava ovvia: forzare InnerTube a rispondere sempre in inglese, con gl: 'US', hl: 'en'. Deterministico, prevedibile, cross-platform. L'ho implementata nella v1.10.8 "Alignment" e sono andato avanti.

Il problema è che forzando il locale, il formato delle date cambiava. Le forme estese — "3 months ago", "2 years ago" — diventavano forme abbreviate InnerTube: "3mo ago", "1y ago", "22h ago". Il parser cercava includes('month'), includes('year'), e non trovava nulla. Nessun ramo corrispondeva. La variabile date rimaneva quella inizializzata in cima al blocco: la data odierna.

Ogni singolo item YouTube, indipendentemente da quanto fosse vecchio, risultava pubblicato oggi. Tutti passavano il filtro cutoff. Centinaia di item finivano nella pending_queue.

Ho sistemato il parser nella v1.10.9 con una regex che gestisce le abbreviazioni — 3mo, 1y, 22h, 2w, 7d — prima della catena di controlli full-word. Ma il bello arriva con la v1.10.11 "CleanTube": analizzando i log del 28 aprile, ho visto che diversi canali YouTube restituivano zero item con session reset ripetuti. Il locale forcing in sé — quei parametri gl e hl che nel tipo SessionOptions di youtubei.js v17 non esistono nemmeno, passati con un cast as any che TypeScript inghiottiva senza fiatare — stava rompendo la sessione su canali specifici.

La conclusione logica: rimuovere il locale forcing del tutto. Il parser di v1.10.9 gestisce già tutte le forme, abbreviate e full-word, in inglese e in italiano. Non aveva mai avuto bisogno di quel forzamento.

// DA (problematico):
youtube = await Innertube.create({ gl: 'US', hl: 'en' } as any);

// A (corretto):
youtube = await Innertube.create();

Tre versioni per tornare a una riga senza argomenti. Accade.


La memoria del log

La v1.10.10 "LogVault" risolve due cose distinte.

La prima è tecnica e sottile: l'auto-updater controllava la presenza di aggiornamenti nel processo main, prima che il renderer avesse montato i componenti React e registrato i listener per i messaggi IPC. Il messaggio arrivava, non c'era nessuno ad ascoltarlo, e la notifica di aggiornamento spariva nel nulla. Il fix è stato spostare il check nel renderer, dove viene già eseguito al mount — in quel momento i listener sono pronti.

La seconda è pratica. Il pannello log era limitato a 300 righe. Con sessioni di 12-24 ore e molti feed attivi, questo significava che la storia recente spariva continuamente dalla vista. Ho esteso il buffer a 5000 entry e introdotto il virtual scroll tramite @tanstack/react-virtual: il DOM renderizza solo le righe visibili — una ventina, più un buffer di overscan — indipendentemente da quante ne esistano in memoria. Scrollare 5000 righe costa esattamente quanto scrollarne 50.

Adesso il log ricorda la notte intera.


Titan Blue

Dopo otto versioni in otto giorni — tutte bugfix, tutti audit, tutto codice che si sfila i vestiti e mostra dove aveva delle crepe — avevo voglia di fare qualcosa di diverso.

Ho aperto il design system e ho cambiato tutto.

La palette Obsidian Pulse V2 — viola, lavanda, le sfumature Material Design 3 che avevo scelto nella v1.9.1 con tanta cura — è andata in pensione. Al suo posto: blu puro #3b82f6 su nero quasi assoluto #050510. Tutti i bordi, tutti i pannelli, tutti gli accenti: blu. Il punto verde del bot in esecuzione rimane verde — è un segnale semantico, non un elemento decorativo, e non si tocca.

Le icone migrano da Phosphor Icons a Lucide React. Non è solo una preferenza estetica: Lucide ha un API più pulita, è già presente nel package.json dalla v1.9.0, e la migrazione elimina una dipendenza duplicata.

La v2.0.0 "Titan Blue" non aggiunge funzioni. Non corregge bug. Fa una cosa sola: decide come vuole apparire. Dopo due mesi di lavoro tecnico invisibile — architetture, patch, indici SQL, code buffer, timezone UTC — il software si guarda allo specchio e sceglie un'identità.


Cosa significa arrivare a 2.0.0

Non è solo un numero di versione.

Quando ho rilasciato la v1.7.6 a marzo, il software era completo nel senso tecnico: funzionava, era stabile, faceva quello per cui era stato progettato. Ma sotto c'erano crepe che non vedevo ancora. Il Digest Mode che avanzava il timer prima del tempo. Le date NaN che saltavano i filtri. Gli item persi durante il silenzio. Il locale forcing di YouTube che rompeva i canali tedeschi.

Ognuna di queste versioni ha tolto qualcosa che non doveva esserci. Non ha aggiunto feature, non ha cambiato l'esperienza dell'utente in modo visibile. Ha reso il sistema più onesto — più coerente tra quello che promette di fare e quello che fa davvero.

La v2.0.0 arriva dopo tutto questo. Non perché ci fosse un piano prestabilito, ma perché a un certo punto il codice era abbastanza pulito da permettersi di pensare all'estetica. E l'estetica è arrivata come arriva sempre il design fatto bene: togliendo tutto quello che non era necessario, fino a restare col colore giusto su sfondo scuro.

Titan è blu. È stabile. È alle 2.0.0.


Se vuoi portarlo sui tuoi canali Telegram, lo trovi su Ecosystem.Runtime a 9,99€ una tantum. Funziona su Windows, macOS e Linux.

Ogni tanto scrivo una lettera

Nessuna cadenza fissa, nessun riassunto automatico: parte solo quando ho qualcosa da raccontare. In fondo a ognuna c'è il link per uscire.

↑ Su

Cerca nel sito

Condividi

L'indirizzo di questa pagina:

Scrivi a Simone

Segnalazioni, correzioni, o solo per dire che una cosa non funziona: quest'ultima è la più utile di tutte.