Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,17 @@ esistono.
- **Provarla senza telefono**: `npm run android:web` e `npm run android:preview`, poi
il browser su `http://localhost:4173` (ridotto a larghezza di telefono).

### Versione 1.3.1 — avvio e chiusura

Fix dell'avvio (una PR). Prima c'erano problemi all'apertura: nessun blocco d'istanza, quindi un secondo doppio clic, o la versione nuova lanciata dall'aggiornamento mentre la vecchia si chiudeva, apriva lo stesso database cifrato in due processi (migrazioni e seed demo in parallelo, `database is locked`, copie rimaste in background).

- `index.ts`: `app.requestSingleInstanceLock()` per cartella dati (demo e versione vera hanno cartelle diverse, quindi convivono); `second-instance` riporta davanti la finestra. Con `--daprod-aggiornato` (`ARG_AGGIORNATO`, passato dall'aggiornamento dei portable) la nuova copia riprova il blocco per 15 s.
- La finestra si mostra comunque dopo 10 s anche senza `ready-to-show`; se il renderer muore si ricarica.
- Chiusura in `will-quit`: si fermano i timer (backup automatico, controllo aggiornamenti), poi il server (`closeAllConnections`), poi il database.
- `busy_timeout` 10 s impostato subito dopo la chiave, prima della prima lettura e delle migrazioni.

Provato sull'app compilata: seconda copia esce subito (codice 0) e resta un solo processo principale; dopo la chiusura zero processi; con `--daprod-aggiornato` la nuova copia prende il posto della vecchia.

### Versione 1.3.0 — interfaccia più semplice, portale dell'azienda, costi, margini e tasse

Quattro pezzi, una PR ciascuno (#21, #22, #40, #41), roadmap su GitHub nell'issue fissata #39.
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Un gestionale desktop per consulenti finanziari aziendali: riclassifica il bilan
calcola gli indici che contano, previene le tensioni di cassa prima che arrivino,
e risponde a "cosa succede se" senza toccare un foglio Excel.

![stato](https://img.shields.io/badge/versione-1.3.0-3ddbff)
![stato](https://img.shields.io/badge/versione-1.3.1-3ddbff)
![piattaforma](https://img.shields.io/badge/Windows-x64-3ddbff)
![demo mac](https://img.shields.io/badge/demo-macOS-3ddbff)
![demo android](https://img.shields.io/badge/demo-Android-3ddbff)
Expand Down Expand Up @@ -131,6 +131,14 @@ I due programmi si parlano su una rete privata (Tailscale, con un collegamento d
riserva per quando un'azienda non riesce a configurarlo). Nessun server pubblico,
nessun dominio da comprare, nessun dato che passa da terzi per l'uso quotidiano.

## Novità della 1.3.1

- **Avvio più sicuro.** Il programma si apre una volta sola: un secondo doppio clic
riporta davanti la finestra già aperta invece di aprire un'altra copia sullo stesso
database. Quando si chiude, si chiude davvero (niente processi rimasti in
background), e dopo un aggiornamento la versione nuova aspetta che la vecchia abbia
finito.

## Novità della 1.3.0

- **Più semplice, su Windows e sul telefono.** Da qualsiasi schermata **Ctrl+K** (o la
Expand Down Expand Up @@ -300,7 +308,7 @@ due programmi.
| 1.0 | Aggiornamenti automatici, dati nel programma, report PDF, cruscotto, riquadri spostabili | ✅ Versione 1.0.0 |
| 1.1 | Pannelli liberi e ridimensionabili, zoom, scenari pronti, demo per Mac | ✅ Versione 1.1.0 |
| 1.2 | Attività e Tempi (bacheca, timer, ore), demo per Android | ✅ Versione 1.2.0 |
| 1.3 | Interfaccia più semplice (Ctrl+K, telefono), impostazioni, documenti, richieste e chiamate, Personale, Area fiscale, Marginalità | ✅ Versione 1.3.0 |
| 1.3 | Interfaccia più semplice (Ctrl+K, telefono), impostazioni, documenti, richieste e chiamate, Personale, Area fiscale, Marginalità | ✅ Versione 1.3.1 |
| 8-9 | Collegamento fra i due programmi (senza account), import Excel più tollerante | ⬜ Prossima |
| 10 | Eseguibili: installer, portable, demo | 🟡 Pubblicati a ogni aggiornamento importante; mancano le varianti Consulente/Azienda |

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "daprodfinanza",
"version": "1.3.0",
"version": "1.3.1",
"description": "Gestionale desktop di controllo di gestione per consulenti finanziari aziendali",
"main": "./out/main/index.js",
"author": "DaProdProduzioni",
Expand Down
4 changes: 3 additions & 1 deletion src/main/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export function openDatabase(): Database.Database {

db.pragma(`cipher = 'sqlcipher'`)
db.pragma(`key = "x'${databaseKey()}'"`)
// Subito dopo la chiave: se un'altra copia sta ancora chiudendo il file,
// anche la prima lettura e le migrazioni aspettano invece di fallire.
db.pragma('busy_timeout = 10000')

// Prima lettura: fallisce subito e con un errore chiaro se la chiave non apre il file.
try {
Expand All @@ -39,7 +42,6 @@ export function openDatabase(): Database.Database {

db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = ON')
db.pragma('busy_timeout = 5000')

const version = runMigrations(db)
console.log(`[db] aperto ${file} — schema v${version}`)
Expand Down
72 changes: 68 additions & 4 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,50 @@ import { basename, join, resolve, sep } from 'node:path'
import { eseguibile } from '@shared/documents'
import { backupDatabase, closeDatabase, openDatabase } from './db'
import { seedDemoData } from './db/seed'
import { avviaBackupAutomatico } from './lib/auto-backup'
import { avviaBackupAutomatico, fermaBackupAutomatico } from './lib/auto-backup'
import { companiesRoot, dataRoot } from './lib/paths'
import { apiBaseUrl, startServer, stopServer } from './server'
import { registerReportIpc } from './report'
import { checkForUpdates, downloadUpdate, initUpdates, installUpdate, updateState } from './updates'
import {
ARG_AGGIORNATO,
checkForUpdates,
downloadUpdate,
fermaUpdates,
initUpdates,
installUpdate,
updateState
} from './updates'

let mainWindow: BrowserWindow | null = null
const SHOW_FALLBACK_MS = 10_000

/** Riporta davanti la finestra già aperta (secondo doppio clic sull'icona). */
function mettiDavanti(): void {
if (!mainWindow || mainWindow.isDestroyed()) return
if (mainWindow.isMinimized()) mainWindow.restore()
if (!mainWindow.isVisible()) mainWindow.show()
mainWindow.focus()
}

/**
* Una sola copia per cartella dati. Senza questo blocco un secondo doppio clic
* (o la versione nuova lanciata dall'aggiornamento mentre la vecchia si chiude)
* apriva lo stesso database cifrato in due processi: migrazioni e dati demo
* scritti due volte, "database is locked" e copie rimaste in background.
* La demo ha la sua cartella dati (build-flags), quindi convive con la versione vera.
*
* Dopo un aggiornamento la copia vecchia può metterci qualche secondo a
* chiudersi: la nuova riprova per un po' invece di arrendersi subito.
*/
async function prendiIstanzaUnica(): Promise<boolean> {
if (app.requestSingleInstanceLock()) return true
if (!process.argv.includes(ARG_AGGIORNATO)) return false
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 500))
if (app.requestSingleInstanceLock()) return true
}
return false
}

function createWindow(): void {
mainWindow = new BrowserWindow({
Expand All @@ -38,7 +75,20 @@ function createWindow(): void {
}
})

mainWindow.on('ready-to-show', () => mainWindow?.show())
// Se la pagina non dice mai "pronta" (un errore nel renderer) la finestra si
// mostra lo stesso: mai un programma che gira nascosto senza finestra.
const mostra = (): void => {
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isVisible()) mainWindow.show()
}
mainWindow.on('ready-to-show', mostra)
setTimeout(mostra, SHOW_FALLBACK_MS)
mainWindow.webContents.on('render-process-gone', (_event, details) => {
console.error('[finestra] renderer terminato:', details.reason)
if (details.reason !== 'clean-exit' && mainWindow && !mainWindow.isDestroyed()) mainWindow.reload()
})
mainWindow.on('closed', () => {
mainWindow = null
})
// Il lampeggio per una chiamata in arrivo si ferma quando si torna sulla finestra.
mainWindow.on('focus', () => mainWindow?.flashFrame(false))

Expand Down Expand Up @@ -148,7 +198,14 @@ function registerIpc(): void {
registerReportIpc()
}

app.on('second-instance', () => mettiDavanti())

app.whenReady().then(async () => {
if (!(await prendiIstanzaUnica())) {
// C'è già una copia aperta: le ha pensato 'second-instance', qui si esce e basta.
app.exit(0)
return
}
electronApp.setAppUserModelId('com.daprodproduzioni.daprodfinanza')

app.on('browser-window-created', (_event, window) => optimizer.watchWindowShortcuts(window))
Expand Down Expand Up @@ -181,7 +238,14 @@ app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

app.on('before-quit', () => {
// In chiusura si fermano prima i controlli periodici, poi il server e per
// ultimo il database: niente timer che scrivono su un database già chiuso.
let chiuso = false
app.on('will-quit', () => {
if (chiuso) return
chiuso = true
fermaBackupAutomatico()
fermaUpdates()
stopServer()
closeDatabase()
})
5 changes: 5 additions & 0 deletions src/main/lib/auto-backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ export function avviaBackupAutomatico(): void {
applicaBackupAutomatico()
if (!timer) timer = setInterval(applicaBackupAutomatico, OGNI_ORA_MS)
}

export function fermaBackupAutomatico(): void {
if (timer) clearInterval(timer)
timer = null
}
3 changes: 3 additions & 0 deletions src/main/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ export function apiBaseUrl(): string {
}

export function stopServer(): void {
// Le connessioni keep-alive del renderer terrebbero il server in piedi:
// si chiudono subito, così il processo esce davvero.
server?.close()
server?.closeAllConnections()
server = null
port = 0
}
24 changes: 20 additions & 4 deletions src/main/updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const CHECK_EVERY_MS = 6 * 60 * 60 * 1000
const FIRST_CHECK_DELAY_MS = 8_000
/** File che dice alla versione nuova quale copia portable ha sostituito. */
const CLEANUP_FILE = 'aggiornamento-portable.json'
/** Con questo argomento la versione nuova aspetta che la vecchia si chiuda (index.ts). */
export const ARG_AGGIORNATO = '--daprod-aggiornato'

let state: UpdateState
let offer: UpdateOffer | null = null
Expand Down Expand Up @@ -221,7 +223,7 @@ export function installUpdate(): void {
const customData =
app.commandLine.hasSwitch('user-data-dir') ||
process.argv.some((arg) => arg.startsWith('--user-data-dir'))
const args = customData ? [`--user-data-dir=${app.getPath('userData')}`] : []
const args = customData ? [`--user-data-dir=${app.getPath('userData')}`, ARG_AGGIORNATO] : [ARG_AGGIORNATO]
spawn(downloaded, args, { detached: true, stdio: 'ignore', cwd: dirname(downloaded) }).unref()
}
app.quit()
Expand Down Expand Up @@ -283,10 +285,24 @@ export function initUpdates(): void {
// L'impostazione si rilegge a ogni giro: spenta o riaccesa dalle Impostazioni
// vale subito, senza riavviare. Il pulsante "Aggiornamenti" funziona sempre.
const automatico = (): void => {
if (getAppSettings().aggiornamentiAutomatici) void checkForUpdates()
try {
if (getAppSettings().aggiornamentiAutomatici) void checkForUpdates()
} catch (err) {
// Database non disponibile (programma in chiusura): si salta il giro.
console.error('[aggiornamenti] controllo automatico saltato:', err)
}
}
setTimeout(automatico, FIRST_CHECK_DELAY_MS)
setInterval(automatico, CHECK_EVERY_MS)
primoControllo = setTimeout(automatico, FIRST_CHECK_DELAY_MS)
controlloPeriodico = setInterval(automatico, CHECK_EVERY_MS)
}

let primoControllo: ReturnType<typeof setTimeout> | null = null
let controlloPeriodico: ReturnType<typeof setInterval> | null = null

export function fermaUpdates(): void {
if (primoControllo) clearTimeout(primoControllo)
if (controlloPeriodico) clearInterval(controlloPeriodico)
primoControllo = controlloPeriodico = null
}

function messageOf(error: unknown, fallback: string): string {
Expand Down
1 change: 1 addition & 0 deletions src/web/lib/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export const jwtSecret = (): string => ''
// `lib/auto-backup.ts`: sul telefono non ci sono backup (sono dati di esempio).
export const applicaBackupAutomatico = (): null => null
export const avviaBackupAutomatico = (): void => undefined
export const fermaBackupAutomatico = (): void => undefined
Loading