Compare commits

...

17 Commits

Author SHA1 Message Date
9aec9d3a50 refactor: rimuove cache destinazione e forza rescansione completa
- elimina cache su disco in destination scanner
- rimuove integrazione cache da main process
- allinea messaggi progresso UI alla scansione completa
- aggiorna README con comportamento senza cache
2026-03-16 16:27:55 +01:00
65b1777a60 feat(router): instrada in sottocartella A/M_B e ottimizza cache destinazione
- Router: aggiunge campo `subfolder` a getCadInfo (91xxxxx → A, 92xxxxx → M_B);
  getDestinationDecision appende la sottocartella al percorso finale
- Scanner (fast path): legge A e M_B dentro ogni cartella a 3 cifre per
  il rilevamento duplicati, anziché la cartella a 3 cifre stessa
- Cache: persistenza su disco del folderIndex; cadKeyIndex sempre fresco
- isCacheStillValid: validazione a due livelli (top-level + cartelle a 3
  cifre) — rileva automaticamente nuove cartelle aggiunte da colleghi
- UI: rimosso pulsante "Riscansiona destinazione"; gestione cache completamente
  automatica e trasparente per l'utente
2026-03-16 16:17:32 +01:00
a49d338741 perf(scanner): ottimizza scansione destinazione con pass unico e barra determinata
Sostituisce le due scansioni sequenziali (cartelle + file CAD) con un
unico passaggio parallelo in services/destinationScanner.js. La lettura
di primo livello fornisce il totale delle sezioni, rendendo la barra
di progresso determinata (N / totale) durante l'analisi della destinazione.
Il label mostra contemporaneamente sezioni completate e file CAD trovati
2026-03-16 15:16:27 +01:00
b0bcea2f10 feat(ui): aggiunge barra di progresso durante lo smistamento
Mostra una progress bar con messaggio contestuale nelle 4 fasi:
scansione sorgente, analisi cartella destinazione (con contatore
cartelle), ricerca duplicati (con contatore file CAD), smistamento
(N / totale). La barra è indeterminata nelle prime tre fasi e
determinata durante la copia. Il canale IPC progress viene
aperto all'inizio e chiuso al termine dell'operazione
2026-03-16 14:54:23 +01:00
57f8b230b2 docs: aggiorna documentazione cartelle speciali con percorsi corretti
Aggiunge sezione __SALTATI nella guida utente e nella sidebar.
Corregge i percorsi di __NON_SMISTATI e __DUPLICATI (Windows: C:\cadroute\,
Linux: ~/.cadroute/) e aggiorna i riferimenti da duplicati/ a __DUPLICATI
in tutta la documentazione. Aggiorna README con i percorsi corretti
2026-03-16 14:33:19 +01:00
fea0699ff1 fix(router): corregge estensione .dwr in .drw (formato Creo corretto)
Creo utilizza .drw per i disegni 2D, non .dwr. Rimossa la normalizzazione
errata drw→dwr nel router, aggiornata la regex, CAD_EXTENSIONS e la
documentazione utente
2026-03-16 13:56:10 +01:00
82becb7569 Aggiunge LICENSE e README.md 2026-03-16 13:33:10 +01:00
5e5dadba2c feat(ui): aggiunge menu Informazioni con dialog about (versione, autore, azienda) 2026-03-16 12:28:52 +01:00
6438d1a51c feat(ui): aumenta dimensione finestra e scala UI (font, pulsanti, riquadri) 2026-03-16 12:19:01 +01:00
3fa7758fbe feat: smistamento file non-CAD, rinomina cartelle speciali, guida utente
- Aggiunge cartella __SALTATI: i file non-CAD vengono ora copiati in
  __SALTATI invece di essere ignorati (folderProcessor, zipProcessor)
- Rinomina cartella duplicati in __DUPLICATI per coerenza con le altre
  cartelle speciali (__NON_SMISTATI, __SALTATI)
- Aggiunge pulsante "Anteprima saltati" in UI con anteprima e pulizia
- Aggiunge guida utente HTML/CSS in renderer/docs/ con sidebar navigabile
- Aggiunge menu Help > Documentazione che apre la guida in una finestra
- Imposta DEFAULT_DESTINATION a X:\
2026-03-16 12:10:54 +01:00
22801f6b75 feat(ui): aggiunge guida utente e menu Help > Documentazione 2026-03-16 11:58:55 +01:00
105a23853a Merge branch 'wip-build'
Aggiunge infrastruttura Docker per build riproducibile (Linux e Windows)
2026-03-16 11:44:48 +01:00
1c758d68fc feat(build): aggiunge infrastruttura Docker per build Windows con Wine 2026-03-16 11:28:28 +01:00
fc1c7990be feat(build): aggiunge infrastruttura Docker per build Linux riproducibile
- contrib/linux: Dockerfile (node:20-bookworm-slim) e build.sh con mount CA certs
- package.json: target linux (AppImage/deb), files whitelist, electron in devDependencies
- main.js: icona finestra impostata esplicitamente per Linux
- build/.gitkeep: cartella build tracciata per icon.ico e icon.png
2026-03-16 11:21:11 +01:00
647bd37fad chore(build): aggiunge cartella build/ per icona Electron 2026-03-16 11:01:44 +01:00
e2f9b209d6 feat(router): gestisce versioni duplicate mantenendo solo la più alta nello smistamento 2026-03-16 10:46:00 +01:00
a58a3c438c chore(branding): rinomina il progetto in CadRoute 2026-03-16 10:31:59 +01:00
25 changed files with 1462 additions and 86 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Davide Grilli
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# CadRoute
Smistatore automatico di file CAD (Creo) per Windows e Linux.
Analizza una cartella o un archivio `.zip`, riconosce i file CAD (`.prt`, `.asm`, `.drw`) e li copia automaticamente nella sottocartella di destinazione corretta, basandosi sulla struttura numerica del nome file.
## Funzionalità
- Smistamento da cartella o archivio ZIP (con drag & drop)
- Riscansione completa della destinazione a ogni processo (senza cache)
- Routing automatico basato sul gruppo di cifre nel nome file
- Gestione versioni: mantiene solo la versione più alta
- Cartelle speciali: `__NON_SMISTATI`, `__DUPLICATI`, `__SALTATI`
- Anteprima e pulizia delle cartelle speciali
- Guida utente integrata (Help → Documentazione)
- Destinazione persistente tra le sessioni
## Avvio in sviluppo
```bash
npm install
npm run dev
```
## Build
### Linux (Docker)
```bash
cd contrib/linux
./build.sh
```
### Windows (Docker + Wine)
```bash
cd contrib/windows
./build.sh
```
Gli artefatti vengono generati in `dist/`.
## Struttura cartelle speciali
| Cartella | Contenuto |
|----------|-----------|
| `__NON_SMISTATI` | File CAD senza destinazione valida |
| `__DUPLICATI` | File CAD già presenti in destinazione |
| `__SALTATI` | File non CAD (PDF, immagini, ecc.) |
- **Windows:** `C:\cadroute\<cartella>\`
- **Linux:** `~/.cadroute/<cartella>/`
## Autore
Davide Grilli — Cevolani Italia s.r.l.
## Licenza
MIT

0
build/.gitkeep Normal file
View File

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

12
contrib/linux/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libudev-dev \
libusb-1.0-0-dev \
fuse \
rpm \
dpkg-dev \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /project

25
contrib/linux/build.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
IMAGE_NAME="cadroute-builder-linux"
echo "[cadroute] Building Docker image..."
docker build -t "$IMAGE_NAME" "$SCRIPT_DIR"
echo "[cadroute] Running Linux build..."
docker run --rm \
--device /dev/fuse \
--cap-add SYS_ADMIN \
-v "$PROJECT_ROOT":/project \
-w /project \
-v cadroute-electron-cache:/root/.cache/electron \
-v cadroute-electronbuilder-cache:/root/.cache/electron-builder \
-v /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro \
-e SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
-e NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt \
"$IMAGE_NAME" \
bash -c "npm ci && npm run build -- --linux --x64"
echo "[cadroute] Artefatti in: $PROJECT_ROOT/dist"

View File

@@ -0,0 +1,2 @@
FROM electronuserland/builder:wine-mono
WORKDIR /project

20
contrib/windows/build.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
IMAGE_NAME="cadroute-builder-windows"
echo "[cadroute] Building Docker image..."
docker build -t "$IMAGE_NAME" "$SCRIPT_DIR"
echo "[cadroute] Running Windows build..."
docker run --rm \
-v "$PROJECT_ROOT":/project \
-w /project \
-v cadroute-electron-cache:/root/.cache/electron \
-v cadroute-electronbuilder-cache:/root/.cache/electron-builder \
"$IMAGE_NAME" \
bash -c "npm ci && npm run build -- --win --x64"
echo "[cadroute] Artefatti in: $PROJECT_ROOT/dist"

78
main.js
View File

@@ -1,4 +1,4 @@
const { app, BrowserWindow, dialog, ipcMain, shell } = require('electron');
const { app, BrowserWindow, dialog, ipcMain, shell, Menu } = require('electron');
const path = require('path');
const fs = require('fs-extra');
@@ -8,12 +8,14 @@ const {
resolveUnroutedDir,
listUnroutedFiles,
listDuplicateFiles,
listSkippedFiles,
clearUnroutedFiles,
clearDuplicateFiles,
clearSkippedFiles,
} = require('./services/unrouted');
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr'];
const DEFAULT_DESTINATION = './output/cad';
const CAD_EXTENSIONS = ['prt', 'asm', 'drw'];
const DEFAULT_DESTINATION = 'X:\\';
const SETTINGS_FILENAME = 'cad-router-settings.json';
const LINUX_RUNTIME_DIR = '.cadroute';
@@ -66,8 +68,9 @@ let config = buildConfig(DEFAULT_DESTINATION);
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 640,
width: 1000,
height: 800,
icon: path.join(__dirname, 'build', 'icon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
@@ -78,6 +81,50 @@ function createWindow() {
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
}
function createDocsWindow() {
const docs = new BrowserWindow({
width: 900,
height: 700,
title: 'CadRoute — Documentazione',
icon: path.join(__dirname, 'build', 'icon.png'),
webPreferences: { contextIsolation: true, nodeIntegration: false },
});
docs.loadFile(path.join(__dirname, 'renderer', 'docs', 'index.html'));
docs.setMenuBarVisibility(false);
}
function showAboutDialog() {
dialog.showMessageBox({
type: 'info',
title: 'Informazioni su CadRoute',
message: 'CadRoute',
detail: [
`Versione: ${app.getVersion()}`,
`Autore: Davide Grilli`,
`Azienda: Cevolani Italia s.r.l.`,
`Licenza: MIT`,
``,
`Smistatore automatico di file CAD (Creo).`,
``,
`Electron: ${process.versions.electron}`,
`Node: ${process.versions.node}`,
].join('\n'),
buttons: ['OK'],
noLink: true,
});
}
Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: 'Informazioni',
submenu: [{ label: 'Informazioni su CadRoute...', click: () => showAboutDialog() }],
},
{
label: 'Help',
submenu: [{ label: 'Documentazione', click: () => createDocsWindow() }],
},
]));
app.whenReady().then(async () => {
await ensureRuntimeDirectory();
const persistedDestination = await loadPersistedDestination();
@@ -95,7 +142,7 @@ app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
ipcMain.handle('select-folder', async () => {
ipcMain.handle('select-folder', async (event) => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
});
@@ -104,11 +151,12 @@ ipcMain.handle('select-folder', async () => {
return { canceled: true };
}
const routingResult = await processFolder(result.filePaths[0], config);
const onProgress = (data) => event.sender.send('progress', data);
const routingResult = await processFolder(result.filePaths[0], config, onProgress);
return { canceled: false, ...routingResult };
});
ipcMain.handle('select-zip', async () => {
ipcMain.handle('select-zip', async (event) => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Zip', extensions: ['zip'] }],
@@ -118,11 +166,12 @@ ipcMain.handle('select-zip', async () => {
return { canceled: true };
}
const routingResult = await processZip(result.filePaths[0], config);
const onProgress = (data) => event.sender.send('progress', data);
const routingResult = await processZip(result.filePaths[0], config, onProgress);
return { canceled: false, ...routingResult };
});
ipcMain.handle('process-dropped-path', async (_event, payload) => {
ipcMain.handle('process-dropped-path', async (event, payload) => {
const droppedPath = String(payload?.path || '').trim();
if (!droppedPath) {
throw new Error('Percorso non valido');
@@ -133,13 +182,15 @@ ipcMain.handle('process-dropped-path', async (_event, payload) => {
throw new Error('Percorso non trovato');
}
const onProgress = (data) => event.sender.send('progress', data);
if (stats.isDirectory()) {
const routingResult = await processFolder(droppedPath, config);
const routingResult = await processFolder(droppedPath, config, onProgress);
return { canceled: false, sourceType: 'folder', ...routingResult };
}
if (stats.isFile() && path.extname(droppedPath).toLowerCase() === '.zip') {
const routingResult = await processZip(droppedPath, config);
const routingResult = await processZip(droppedPath, config, onProgress);
return { canceled: false, sourceType: 'zip', ...routingResult };
}
@@ -176,6 +227,7 @@ ipcMain.handle('update-destination', async (_event, payload) => {
return { destination: config.destination };
});
ipcMain.handle('open-unrouted-folder', async () => {
const unroutedDir = await resolveUnroutedDir();
const openResult = await shell.openPath(unroutedDir);
@@ -189,5 +241,7 @@ ipcMain.handle('open-unrouted-folder', async () => {
ipcMain.handle('list-unrouted-files', async () => listUnroutedFiles());
ipcMain.handle('list-duplicates-files', async () => listDuplicateFiles());
ipcMain.handle('list-skipped-files', async () => listSkippedFiles());
ipcMain.handle('clear-unrouted-files', async () => clearUnroutedFiles());
ipcMain.handle('clear-duplicates-files', async () => clearDuplicateFiles());
ipcMain.handle('clear-skipped-files', async () => clearSkippedFiles());

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "progetto",
"name": "cadroute",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "progetto",
"name": "cadroute",
"version": "1.0.0",
"license": "ISC",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "progetto",
"name": "cadroute",
"version": "1.0.0",
"description": "CAD File Router MVP",
"description": "CadRoute MVP",
"main": "main.js",
"scripts": {
"dev": "env -u ELECTRON_RUN_AS_NODE electron .",
@@ -12,17 +12,32 @@
"author": "",
"license": "ISC",
"build": {
"appId": "com.cad.router",
"appId": "com.cadroute",
"productName": "CadRoute",
"files": [
"main.js",
"preload.js",
"renderer/**/*",
"services/**/*",
"build/icon.png",
"renderer/docs/**/*"
],
"win": {
"target": "nsis"
"target": "nsis",
"icon": "build/icon.ico"
},
"linux": {
"target": ["AppImage", "deb"],
"icon": "build/icon.png",
"category": "Utility"
}
},
"dependencies": {
"electron": "^40.7.0",
"fs-extra": "^11.3.4",
"unzipper": "^0.12.3"
},
"devDependencies": {
"electron": "^40.7.0",
"electron-builder": "^26.8.1"
}
}

View File

@@ -11,6 +11,10 @@ contextBridge.exposeInMainWorld('api', {
openUnroutedFolder: () => ipcRenderer.invoke('open-unrouted-folder'),
listUnroutedFiles: () => ipcRenderer.invoke('list-unrouted-files'),
listDuplicatesFiles: () => ipcRenderer.invoke('list-duplicates-files'),
listSkippedFiles: () => ipcRenderer.invoke('list-skipped-files'),
clearUnroutedFiles: () => ipcRenderer.invoke('clear-unrouted-files'),
clearDuplicatesFiles: () => ipcRenderer.invoke('clear-duplicates-files'),
clearSkippedFiles: () => ipcRenderer.invoke('clear-skipped-files'),
onProgress: (cb) => ipcRenderer.on('progress', (_e, data) => cb(data)),
offProgress: () => ipcRenderer.removeAllListeners('progress'),
});

303
renderer/docs/index.html Normal file
View File

@@ -0,0 +1,303 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CadRoute — Documentazione</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<nav>
<div class="nav-brand">
<div class="logo">CadRoute</div>
<div class="version">Guida utente</div>
</div>
<span class="nav-section-label">Introduzione</span>
<a href="#intro">Cos'è CadRoute</a>
<a href="#requisiti">Requisiti</a>
<span class="nav-section-label">Utilizzo</span>
<a href="#destinazione">Configurare la destinazione</a>
<a href="#cartella">Smistare i file</a>
<a href="#risultati">Leggere i risultati</a>
<span class="nav-section-label">Approfondimenti</span>
<a href="#nomi">Convenzione nomi file</a>
<a href="#nonsmistati">File non smistati</a>
<a href="#duplicati">File duplicati</a>
<a href="#saltati">File saltati</a>
</nav>
<main>
<!-- ── INTRODUZIONE ──────────────────────── -->
<section id="intro">
<h1>CadRoute — Guida utente</h1>
<div class="intro-card">
<p><strong>CadRoute</strong> automatizza lo smistamento di file CAD provenienti da Creo. Analizza una cartella o un archivio ZIP, riconosce i file CAD e li copia automaticamente nella sottocartella di destinazione corretta, basandosi sulla struttura numerica del nome file.</p>
</div>
<h3>Formati supportati</h3>
<table>
<thead>
<tr><th>Estensione</th><th>Tipo</th><th>Descrizione</th></tr>
</thead>
<tbody>
<tr>
<td><span class="badge prt">.prt</span></td>
<td>Part</td>
<td>File di componente singolo Creo</td>
</tr>
<tr>
<td><span class="badge asm">.asm</span></td>
<td>Assembly</td>
<td>File di assemblaggio Creo</td>
</tr>
<tr>
<td><span class="badge dwr">.drw</span></td>
<td>Drawing</td>
<td>File di disegno Creo</td>
</tr>
<tr>
<td><span class="badge skip">altri</span></td>
<td></td>
<td>Copiati in <code>__SALTATI</code> (PDF, immagini, documenti, ecc.)</td>
</tr>
</tbody>
</table>
</section>
<!-- ── REQUISITI ────────────────────────── -->
<section id="requisiti">
<h2>Requisiti</h2>
<p>Prima di iniziare, assicurati che la <strong>cartella di destinazione</strong> sia quella corretta.</p>
</section>
<hr />
<!-- ── CONFIGURARE DESTINAZIONE ─────────── -->
<section id="destinazione">
<h2>Configurare la destinazione</h2>
<p>La <strong>destinazione</strong> è la cartella radice dove CadRoute copierà i file CAD smistati. Va impostata una volta sola e viene ricordata tra le sessioni.</p>
<ol class="steps">
<li><span>Individua la sezione <strong>Destinazione file CAD</strong> nella finestra principale.</span></li>
<li><span>Clicca <strong>Sfoglia</strong> per aprire il selettore cartelle, oppure digita il percorso direttamente nel campo di testo.</span></li>
<li><span>Clicca <strong>Salva</strong>. La destinazione viene memorizzata e sarà attiva anche alla prossima apertura dell'app.</span></li>
</ol>
<div class="callout tip">
<strong>Tip:</strong> il percorso di default è <code>X:\</code>. Assicurati che sia quello corretto prima di avviare lo smistamento.
</div>
</section>
<!-- ── SMISTARE FILE ──────────────────────── -->
<section id="cartella">
<h2>Smistare i file</h2>
<p>È possibile smistare una <strong>cartella</strong> (anche con sottocartelle annidate) o un archivio <strong>.zip</strong>, tramite i pulsanti o trascinando direttamente nell'area apposita.</p>
<ol class="steps">
<li><span>Clicca <strong>Process Folder</strong> per selezionare una cartella, oppure <strong>Process ZIP</strong> per selezionare un archivio. In alternativa, trascina la cartella o il file <code>.zip</code> nell'area tratteggiata.</span></li>
<li><span>L'app scansiona tutti i file e identifica automaticamente quelli CAD.</span></li>
<li><span>I file CAD vengono copiati nella sottocartella di destinazione corrispondente. I file non CAD vengono copiati in <code>__SALTATI</code>.</span></li>
<li><span>Al termine vengono mostrate le statistiche nell'area di output.</span></li>
</ol>
<div class="callout info">
<strong>Nota:</strong> i file originali non vengono mai modificati né cancellati.
</div>
</section>
<hr />
<!-- ── RISULTATI ─────────────────────────── -->
<section id="risultati">
<h2>Leggere i risultati</h2>
<p>Al termine di ogni operazione l'area di output mostra un riepilogo. Ecco il significato di ogni voce.</p>
<div class="stat-grid">
<div class="stat-item info">
<div class="stat-label">Scansionati</div>
<div class="stat-desc">Totale file analizzati (CAD e non).</div>
</div>
<div class="stat-item ok">
<div class="stat-label">Copiati</div>
<div class="stat-desc">File effettivamente scritti su disco (destinazione, non smistati o duplicati).</div>
</div>
<div class="stat-item muted">
<div class="stat-label">Saltati</div>
<div class="stat-desc">File non CAD copiati in <code>__SALTATI</code> (PDF, immagini, ecc.).</div>
</div>
<div class="stat-item warn">
<div class="stat-label">Non smistati</div>
<div class="stat-desc">File CAD per cui non è stata trovata una destinazione valida.</div>
</div>
<div class="stat-item warn">
<div class="stat-label">Duplicati</div>
<div class="stat-desc">File CAD già presenti nella destinazione al momento dello smistamento.</div>
</div>
</div>
<p>Sotto le statistiche sono elencati i dettagli di ogni file (massimo 20 voci) con il percorso di destinazione e la motivazione, nel caso di file non smistati o duplicati.</p>
</section>
<hr />
<!-- ── NOMI FILE ─────────────────────────── -->
<section id="nomi">
<h2>Convenzione nomi file CAD</h2>
<p>CadRoute determina automaticamente la sottocartella di destinazione leggendo il nome del file. È fondamentale che i file seguano la convenzione corretta.</p>
<h3>Struttura del nome</h3>
<pre class="code-block"><code>CODICE.tipo[.versione]
Esempi:
ABC12345.prt → Part, nessuna versione
ABC12345.prt.6 → Part, versione 6
XYZ67890.asm.10 → Assembly, versione 10
DEF11111.drw.15 → Drawing, versione 15</code></pre>
<h3>Gestione versioni</h3>
<p>Se sono presenti più versioni dello stesso file nella sorgente (es. <code>ABC12345.prt.8</code> e <code>ABC12345.prt.9</code>), CadRoute mantiene <strong>solo la versione più alta</strong> e scarta le inferiori.</p>
<table>
<thead>
<tr><th>Scenario</th><th>Comportamento</th></tr>
</thead>
<tbody>
<tr>
<td>File non presente in destinazione</td>
<td>Copia diretta nella sottocartella corretta</td>
</tr>
<tr>
<td>File già presente in destinazione</td>
<td>Copiato in <code>__DUPLICATI</code> per revisione</td>
</tr>
<tr>
<td>Versione inferiore presente nella sorgente</td>
<td>Saltata — la versione più alta prende precedenza</td>
</tr>
</tbody>
</table>
</section>
<!-- ── NON SMISTATI ──────────────────────── -->
<section id="nonsmistati">
<h2>File non smistati</h2>
<p>Un file CAD finisce in <strong>non smistati</strong> quando CadRoute non riesce a determinare una destinazione valida. Le cause più comuni sono:</p>
<table>
<thead>
<tr><th>Causa</th><th>Messaggio</th></tr>
</thead>
<tbody>
<tr>
<td>Codice con meno di 5 cifre</td>
<td><em>"Nome file non conforme: servono almeno 5 cifre…"</em></td>
</tr>
<tr>
<td>Sottocartella non trovata in destinazione</td>
<td><em>"Sottocartella XXX non trovata nella destinazione"</em></td>
</tr>
<tr>
<td>Sottocartella trovata in più percorsi (ambiguo)</td>
<td><em>"Sottocartella XXX trovata in N percorsi diversi"</em></td>
</tr>
</tbody>
</table>
<h3>Dove si trovano</h3>
<p>I file non smistati vengono copiati nella cartella <code>__NON_SMISTATI</code>:</p>
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
<li><strong>Windows:</strong> <code>C:\cadroute\__NON_SMISTATI\</code></li>
<li><strong>Linux:</strong> <code>~/.cadroute/__NON_SMISTATI/</code></li>
</ul>
<h3>Visualizzare e pulire</h3>
<ol class="steps">
<li><span>Clicca <strong>Anteprima non smistati</strong> per aprire la lista dei file presenti.</span></li>
<li><span>Verifica i file, controlla il motivo dell'esclusione nell'area di output.</span></li>
<li><span>Se vuoi svuotare la cartella, clicca <strong>Pulisci cartella</strong> (rosso) e conferma. L'operazione è irreversibile.</span></li>
</ol>
<div class="callout tip">
<strong>Suggerimento:</strong> prima di pulire, sposta manualmente i file che vuoi conservare. La pulizia elimina tutto il contenuto della cartella.
</div>
</section>
<!-- ── DUPLICATI ─────────────────────────── -->
<section id="duplicati">
<h2>File duplicati</h2>
<p>Un file CAD viene classificato come <strong>duplicato</strong> quando la sua chiave (<code>CODICE.tipo</code>, senza versione) è già presente nella cartella di destinazione al momento dello smistamento.</p>
<p>I duplicati non sovrascrivono i file già presenti in destinazione: vengono messi da parte nella cartella <code>__DUPLICATI</code> per permettere una revisione manuale.</p>
<h3>Dove si trovano</h3>
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
<li><strong>Windows:</strong> <code>C:\cadroute\__DUPLICATI\</code></li>
<li><strong>Linux:</strong> <code>~/.cadroute/__DUPLICATI/</code></li>
</ul>
<h3>Gestione versioni nei duplicati</h3>
<p>Anche all'interno della cartella <code>__DUPLICATI</code> viene applicata la logica delle versioni: se arriva una versione più alta di un file già presente nei duplicati, la versione vecchia viene eliminata e sostituita con quella nuova.</p>
<h3>Visualizzare e pulire</h3>
<ol class="steps">
<li><span>Clicca <strong>Anteprima duplicati</strong> per aprire la lista dei file presenti.</span></li>
<li><span>Verifica se i duplicati sono effettivamente superati o se richiedono un intervento manuale.</span></li>
<li><span>Se vuoi svuotare la cartella, clicca <strong>Pulisci cartella</strong> (rosso) e conferma.</span></li>
</ol>
<div class="callout warning">
<strong>Attenzione:</strong> la pulizia è definitiva. Assicurati di non aver bisogno dei file prima di procedere.
</div>
</section>
<!-- ── SALTATI ────────────────────────────── -->
<section id="saltati">
<h2>File saltati</h2>
<p>Un file viene classificato come <strong>saltato</strong> quando non è riconosciuto come file CAD, cioè non ha estensione <code>.prt</code>, <code>.asm</code> o <code>.drw</code>. Rientrano in questa categoria PDF, immagini, documenti Word e qualsiasi altro formato non CAD.</p>
<p>I file saltati non vengono ignorati completamente: vengono copiati nella cartella <code>__SALTATI</code> per permettere una verifica manuale.</p>
<h3>Dove si trovano</h3>
<ul style="margin: 8px 0 12px 20px; color: var(--muted);">
<li><strong>Windows:</strong> <code>C:\cadroute\__SALTATI\</code></li>
<li><strong>Linux:</strong> <code>~/.cadroute/__SALTATI/</code></li>
</ul>
<h3>Visualizzare e pulire</h3>
<ol class="steps">
<li><span>Clicca <strong>Anteprima saltati</strong> per aprire la lista dei file presenti.</span></li>
<li><span>Verifica che nessun file CAD sia finito qui per errore di denominazione.</span></li>
<li><span>Se vuoi svuotare la cartella, clicca <strong>Pulisci cartella</strong> (rosso) e conferma.</span></li>
</ol>
<div class="callout info">
<strong>Nota:</strong> la cartella <code>__SALTATI</code> è utile per individuare file CAD rinominati erroneamente o con estensione mancante.
</div>
</section>
</main>
<script>
// Evidenzia il link attivo nella sidebar durante lo scroll
const sections = document.querySelectorAll('section[id]');
const links = document.querySelectorAll('nav a[href^="#"]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
links.forEach(l => l.classList.remove('active'));
const active = document.querySelector(`nav a[href="#${entry.target.id}"]`);
if (active) active.classList.add('active');
}
});
}, { rootMargin: '-20% 0px -70% 0px' });
sections.forEach(s => observer.observe(s));
</script>
</body>
</html>

335
renderer/docs/style.css Normal file
View File

@@ -0,0 +1,335 @@
:root {
color-scheme: light;
--bg: #f6f8fb;
--card: #ffffff;
--text: #0f172a;
--muted: #475569;
--accent: #0b5fff;
--accent-light: #eef3ff;
--border: #dbe2ea;
--sidebar-w: 220px;
--code-bg: #f1f5f9;
--success: #15803d;
--warning: #b45309;
--danger: #b91c1c;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 14px;
line-height: 1.65;
color: var(--text);
background: var(--bg);
display: flex;
min-height: 100vh;
}
/* ── SIDEBAR ─────────────────────────────────── */
nav {
width: var(--sidebar-w);
min-width: var(--sidebar-w);
background: var(--card);
border-right: 1px solid var(--border);
padding: 24px 0 24px;
position: fixed;
top: 0;
left: 0;
height: 100vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.nav-brand {
padding: 0 18px 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
}
.nav-brand .logo {
font-size: 16px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.3px;
}
.nav-brand .version {
font-size: 11px;
color: var(--muted);
margin-top: 2px;
}
nav a {
display: block;
padding: 7px 18px;
text-decoration: none;
color: var(--muted);
font-size: 13px;
border-left: 3px solid transparent;
transition: color 100ms, border-color 100ms, background 100ms;
}
nav a:hover {
color: var(--text);
background: var(--accent-light);
}
nav a.active {
color: var(--accent);
border-left-color: var(--accent);
background: var(--accent-light);
font-weight: 600;
}
.nav-section-label {
padding: 14px 18px 4px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.8px;
text-transform: uppercase;
color: #94a3b8;
}
/* ── MAIN CONTENT ────────────────────────────── */
main {
margin-left: var(--sidebar-w);
flex: 1;
padding: 40px 48px 80px;
max-width: 860px;
}
/* ── TYPOGRAPHY ──────────────────────────────── */
h1 {
font-size: 26px;
font-weight: 700;
margin-bottom: 8px;
color: var(--text);
}
h2 {
font-size: 19px;
font-weight: 700;
margin: 48px 0 14px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
color: var(--text);
scroll-margin-top: 24px;
}
h3 {
font-size: 15px;
font-weight: 600;
margin: 22px 0 8px;
color: var(--text);
}
p {
color: var(--muted);
margin-bottom: 12px;
}
p:last-child { margin-bottom: 0; }
/* ── INTRO CARD ──────────────────────────────── */
.intro-card {
background: var(--accent-light);
border: 1px solid #c7d9ff;
border-radius: 12px;
padding: 18px 20px;
margin-bottom: 4px;
}
.intro-card p {
color: #1e3a8a;
margin: 0;
}
/* ── STEP LIST ───────────────────────────────── */
.steps {
list-style: none;
counter-reset: step;
display: flex;
flex-direction: column;
gap: 10px;
margin: 12px 0;
}
.steps li {
display: flex;
gap: 14px;
align-items: flex-start;
counter-increment: step;
}
.steps li::before {
content: counter(step);
min-width: 26px;
height: 26px;
border-radius: 50%;
background: var(--accent);
color: white;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
}
.steps li span {
color: var(--muted);
line-height: 1.6;
}
/* ── CALLOUT ─────────────────────────────────── */
.callout {
border-radius: 10px;
padding: 13px 16px;
margin: 16px 0;
font-size: 13px;
border-left: 4px solid;
}
.callout.info {
background: #f0f9ff;
border-color: #38bdf8;
color: #075985;
}
.callout.warning {
background: #fffbeb;
border-color: #f59e0b;
color: #78350f;
}
.callout.tip {
background: #f0fdf4;
border-color: #22c55e;
color: #14532d;
}
.callout strong { font-weight: 700; }
/* ── CODE & INLINE CODE ──────────────────────── */
code {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1px 5px;
font-family: 'Cascadia Code', 'Consolas', monospace;
font-size: 12.5px;
color: #be185d;
}
pre.code-block {
background: #0f172a;
color: #e2e8f0;
border-radius: 10px;
padding: 16px;
margin: 12px 0;
overflow-x: auto;
font-family: 'Cascadia Code', 'Consolas', monospace;
font-size: 13px;
line-height: 1.55;
}
pre.code-block code {
background: none;
border: none;
padding: 0;
color: inherit;
font-size: inherit;
}
/* ── BADGE ───────────────────────────────────── */
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2px;
vertical-align: middle;
margin: 0 2px;
}
.badge.prt { background: #dbeafe; color: #1e40af; }
.badge.asm { background: #dcfce7; color: #166534; }
.badge.dwr { background: #fef3c7; color: #92400e; }
.badge.skip { background: #f1f5f9; color: #475569; }
/* ── TABLE ───────────────────────────────────── */
table {
width: 100%;
border-collapse: collapse;
margin: 14px 0;
font-size: 13px;
}
th {
text-align: left;
padding: 9px 12px;
background: var(--code-bg);
border: 1px solid var(--border);
font-weight: 600;
color: var(--text);
}
td {
padding: 8px 12px;
border: 1px solid var(--border);
color: var(--muted);
vertical-align: top;
}
tr:nth-child(even) td { background: #fafbfc; }
/* ── STAT GRID ───────────────────────────────── */
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px;
margin: 14px 0;
}
.stat-item {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 14px;
}
.stat-item .stat-label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--muted);
margin-bottom: 4px;
}
.stat-item .stat-desc {
font-size: 13px;
color: var(--text);
line-height: 1.4;
}
.stat-item.ok { border-top: 3px solid #22c55e; }
.stat-item.info { border-top: 3px solid var(--accent); }
.stat-item.warn { border-top: 3px solid #f59e0b; }
.stat-item.muted { border-top: 3px solid #94a3b8; }
/* ── SEPARATOR ───────────────────────────────── */
hr {
border: none;
border-top: 1px solid var(--border);
margin: 32px 0;
}
/* ── SECTION ─────────────────────────────────── */
section {
margin-bottom: 8px;
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CAD File Router</title>
<title>CadRoute</title>
<style>
:root {
color-scheme: light;
@@ -24,32 +24,35 @@
background: linear-gradient(130deg, #eaf0ff 0%, var(--bg) 45%, #f9fbff 100%);
color: var(--text);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 16px;
}
.page {
max-width: 820px;
max-width: 980px;
margin: 40px auto;
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 26px;
padding: 32px;
}
h1 {
margin-top: 0;
margin-bottom: 6px;
margin-bottom: 8px;
font-size: 26px;
}
p {
margin: 0;
color: var(--muted);
font-size: 15px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 18px;
gap: 14px;
margin-top: 22px;
}
button {
@@ -57,8 +60,8 @@
background: var(--accent);
color: white;
border-radius: 10px;
padding: 11px 16px;
font-size: 15px;
padding: 13px 20px;
font-size: 16px;
cursor: pointer;
}
@@ -116,43 +119,43 @@
}
.rules h2 {
margin: 0 0 10px;
font-size: 17px;
margin: 0 0 12px;
font-size: 19px;
}
.rule-row {
display: grid;
grid-template-columns: 180px 1fr auto auto;
gap: 8px;
gap: 10px;
align-items: center;
margin-bottom: 8px;
margin-bottom: 10px;
}
.rule-row.single-row {
grid-template-columns: 1fr auto auto;
margin-top: 10px;
margin-top: 12px;
}
.rule-row.preview-actions {
grid-template-columns: auto auto;
grid-template-columns: auto auto auto;
justify-content: start;
}
.rule-label {
font-size: 13px;
font-size: 15px;
color: var(--muted);
}
.rule-row input {
min-width: 0;
padding: 9px 10px;
padding: 11px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
font-size: 16px;
}
.rule-row button {
padding: 9px 12px;
padding: 11px 16px;
border-radius: 8px;
}
@@ -162,7 +165,7 @@
.rule-status {
margin-top: 4px;
font-size: 12px;
font-size: 14px;
color: var(--muted);
}
@@ -251,11 +254,58 @@
overflow: auto;
min-height: 180px;
}
.progress-container {
margin-top: 18px;
display: none;
}
.progress-label {
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
progress {
width: 100%;
height: 6px;
border-radius: 4px;
border: none;
appearance: none;
-webkit-appearance: none;
background: var(--border);
overflow: hidden;
}
progress::-webkit-progress-bar {
background: var(--border);
border-radius: 4px;
}
progress::-webkit-progress-value {
background: var(--accent);
border-radius: 4px;
transition: width 80ms ease;
}
progress:indeterminate {
background: linear-gradient(90deg, var(--border) 25%, var(--accent) 50%, var(--border) 75%);
background-size: 200% 100%;
animation: progress-indeterminate 1.2s linear infinite;
}
@keyframes progress-indeterminate {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
</style>
</head>
<body>
<main class="page">
<h1>Smistatore automatico</h1>
<h1>CadRoute - Smistatore automatico</h1>
<p>Seleziona una cartella o uno ZIP con i disegni da smistare</p>
<div class="actions">
@@ -267,6 +317,11 @@
Trascina qui una cartella o un file .zip
</section>
<div class="progress-container" id="progressContainer">
<div class="progress-label" id="progressLabel">In corso...</div>
<progress id="progressBar"></progress>
</div>
<section class="rules">
<h2>Destinazione file CAD</h2>
<div class="rule-row single-row">
@@ -277,6 +332,7 @@
<div class="rule-row single-row preview-actions">
<button id="openUnroutedBtn" class="browse">Anteprima non smistati</button>
<button id="openDuplicatesBtn" class="browse">Anteprima duplicati</button>
<button id="openSkippedBtn" class="browse">Anteprima saltati</button>
</div>
<div class="rule-status" id="destinationStatus"></div>
</section>

View File

@@ -7,6 +7,7 @@ const browseDestinationBtn = document.getElementById('browseDestinationBtn');
const saveDestinationBtn = document.getElementById('saveDestinationBtn');
const openUnroutedBtn = document.getElementById('openUnroutedBtn');
const openDuplicatesBtn = document.getElementById('openDuplicatesBtn');
const openSkippedBtn = document.getElementById('openSkippedBtn');
const destinationStatus = document.getElementById('destinationStatus');
const previewOverlay = document.getElementById('previewOverlay');
const closePreviewBtn = document.getElementById('closePreviewBtn');
@@ -14,10 +15,52 @@ const clearPreviewBtn = document.getElementById('clearPreviewBtn');
const previewTitle = document.getElementById('previewTitle');
const previewMeta = document.getElementById('previewMeta');
const previewList = document.getElementById('previewList');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressLabel = document.getElementById('progressLabel');
const defaultDropZoneText = 'Trascina qui una cartella o un file .zip';
let isProcessing = false;
let currentPreviewKind = null;
function showProgress() {
progressContainer.style.display = 'block';
progressBar.removeAttribute('value');
progressLabel.textContent = 'In corso...';
}
function hideProgress() {
progressContainer.style.display = 'none';
}
let _indexFilesFound = 0;
function updateProgress({ phase, current, total, file, scanned, folder }) {
if (phase === 'scan') {
_indexFilesFound = 0;
progressLabel.textContent = 'Scansione file sorgente...';
progressBar.removeAttribute('value');
} else if (phase === 'index-dest') {
progressBar.max = total;
progressBar.value = current;
const filesInfo = _indexFilesFound > 0 ? `${_indexFilesFound} file CAD trovati` : '';
progressLabel.textContent = `Analisi destinazione: ${current} / ${total}${folder ? `${folder}` : ''}${filesInfo}`;
} else if (phase === 'index-dup') {
if (current !== undefined && total !== undefined) {
progressBar.max = total;
progressBar.value = current;
progressLabel.textContent = `Analisi destinazione: ${current} / ${total} cartelle`;
} else {
_indexFilesFound = scanned;
const dirInfo = progressBar.max > 0 ? `${progressBar.value} / ${progressBar.max}` : '';
progressLabel.textContent = `Analisi destinazione: ${dirInfo}${scanned} file CAD trovati`;
}
} else if (phase === 'copy') {
progressBar.max = total;
progressBar.value = current;
progressLabel.textContent = `Smistamento: ${current} / ${total}${file ? `${file}` : ''}`;
}
}
function setLoading(isLoading) {
folderBtn.disabled = isLoading;
zipBtn.disabled = isLoading;
@@ -29,6 +72,7 @@ function setDestinationLoading(isLoading) {
saveDestinationBtn.disabled = isLoading;
openUnroutedBtn.disabled = isLoading;
openDuplicatesBtn.disabled = isLoading;
openSkippedBtn.disabled = isLoading;
clearPreviewBtn.disabled = isLoading;
}
@@ -60,18 +104,14 @@ function hidePreview() {
}
async function loadPreviewData(kind) {
if (kind === 'duplicates') {
return window.api.listDuplicatesFiles();
}
if (kind === 'duplicates') return window.api.listDuplicatesFiles();
if (kind === 'skipped') return window.api.listSkippedFiles();
return window.api.listUnroutedFiles();
}
async function clearPreviewData(kind) {
if (kind === 'duplicates') {
return window.api.clearDuplicatesFiles();
}
if (kind === 'duplicates') return window.api.clearDuplicatesFiles();
if (kind === 'skipped') return window.api.clearSkippedFiles();
return window.api.clearUnroutedFiles();
}
@@ -103,6 +143,8 @@ async function handleAction(actionName, actionFn) {
isProcessing = true;
setLoading(true);
showProgress();
window.api.onProgress(updateProgress);
output.textContent = `${actionName} in corso...`;
try {
@@ -117,6 +159,8 @@ async function handleAction(actionName, actionFn) {
} catch (error) {
output.textContent = `${actionName} fallito:\n${error.message}`;
} finally {
window.api.offProgress();
hideProgress();
setLoading(false);
isProcessing = false;
}
@@ -248,7 +292,22 @@ openDuplicatesBtn.addEventListener('click', async () => {
destinationStatus.textContent = 'Caricamento anteprima duplicati...';
const result = await window.api.listDuplicatesFiles();
showPreview('duplicates', 'Anteprima duplicati', result, 'Nessun file presente in duplicati.');
showPreview('duplicates', 'Anteprima __DUPLICATI', result, 'Nessun file presente in __DUPLICATI.');
destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`;
} catch (error) {
destinationStatus.textContent = `Errore: ${error.message}`;
} finally {
setDestinationLoading(false);
}
});
openSkippedBtn.addEventListener('click', async () => {
try {
setDestinationLoading(true);
destinationStatus.textContent = 'Caricamento anteprima saltati...';
const result = await window.api.listSkippedFiles();
showPreview('skipped', 'Anteprima __SALTATI', result, 'Nessun file presente in __SALTATI.');
destinationStatus.textContent = `Anteprima caricata (${result.files?.length || 0} file).`;
} catch (error) {
destinationStatus.textContent = `Errore: ${error.message}`;
@@ -263,9 +322,12 @@ clearPreviewBtn.addEventListener('click', async () => {
}
const isDuplicates = currentPreviewKind === 'duplicates';
const isSkipped = currentPreviewKind === 'skipped';
const confirmMessage = isDuplicates
? 'Confermi la pulizia della cartella duplicati?'
: 'Confermi la pulizia della cartella non smistati?';
? 'Confermi la pulizia della cartella __DUPLICATI?'
: isSkipped
? 'Confermi la pulizia della cartella saltati?'
: 'Confermi la pulizia della cartella non smistati?';
if (!window.confirm(confirmMessage)) {
return;
@@ -277,10 +339,12 @@ clearPreviewBtn.addEventListener('click', async () => {
const clearResult = await clearPreviewData(currentPreviewKind);
const refreshed = await loadPreviewData(currentPreviewKind);
const title = isDuplicates ? 'Anteprima duplicati' : 'Anteprima __NON_SMISTATI';
const title = isDuplicates ? 'Anteprima __DUPLICATI' : isSkipped ? 'Anteprima __SALTATI' : 'Anteprima __NON_SMISTATI';
const emptyMessage = isDuplicates
? 'Nessun file presente in duplicati.'
: 'Nessun file presente in __NON_SMISTATI.';
? 'Nessun file presente in __DUPLICATI.'
: isSkipped
? 'Nessun file presente in __SALTATI.'
: 'Nessun file presente in __NON_SMISTATI.';
showPreview(currentPreviewKind, title, refreshed, emptyMessage);
destinationStatus.textContent = `Cartella pulita: ${clearResult.directory}`;
@@ -291,6 +355,7 @@ clearPreviewBtn.addEventListener('click', async () => {
}
});
closePreviewBtn.addEventListener('click', hidePreview);
previewOverlay.addEventListener('click', (event) => {
if (event.target === previewOverlay) {

View File

@@ -4,13 +4,15 @@ const path = require('path');
const CODE_FOLDER_REGEX = /^\d{3}$/;
const MAX_SCAN_DEPTH = 6;
async function buildDestinationIndex(destinationRoot) {
async function buildDestinationIndex(destinationRoot, onProgress) {
const index = new Map();
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
return index;
}
let scanned = 0;
async function walk(currentDir, depth) {
if (depth > MAX_SCAN_DEPTH) {
return;
@@ -29,6 +31,9 @@ async function buildDestinationIndex(destinationRoot) {
}
const fullPath = path.join(currentDir, entry.name);
scanned += 1;
onProgress?.({ phase: 'index-dest', scanned, folder: entry.name });
if (CODE_FOLDER_REGEX.test(entry.name)) {
const rows = index.get(entry.name) || [];
rows.push(fullPath);

View File

@@ -0,0 +1,99 @@
const fs = require('fs-extra');
const path = require('path');
const { getCadInfo } = require('./router');
const CODE_FOLDER_REGEX = /^\d{3}$/;
const MAX_SCAN_DEPTH = 6;
function toCadKey(cadInfo) {
return String(cadInfo?.key || '').toLowerCase();
}
async function buildDestinationIndexes(destinationRoot, onProgress) {
const folderIndex = new Map();
const cadKeyIndex = new Set();
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
return { folderIndex, cadKeyIndex };
}
let topEntries;
try {
topEntries = await fs.readdir(destinationRoot, { withFileTypes: true });
} catch {
return { folderIndex, cadKeyIndex };
}
const topDirs = topEntries
.filter((e) => e.isDirectory())
.map((e) => ({ name: e.name, fullPath: path.join(destinationRoot, e.name) }));
const total = topDirs.length;
let completed = 0;
let scannedFiles = 0;
for (const entry of topEntries) {
if (!entry.isFile()) continue;
const cadInfo = getCadInfo(entry.name);
if (cadInfo) {
scannedFiles += 1;
cadKeyIndex.add(toCadKey(cadInfo));
}
}
async function walkSubtree(dirPath, depth) {
if (depth > MAX_SCAN_DEPTH) return;
let entries;
try {
entries = await fs.readdir(dirPath, { withFileTypes: true });
} catch {
return;
}
const subdirPromises = [];
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
if (CODE_FOLDER_REGEX.test(entry.name)) {
const rows = folderIndex.get(entry.name) || [];
rows.push(fullPath);
folderIndex.set(entry.name, rows);
}
subdirPromises.push(walkSubtree(fullPath, depth + 1));
} else if (entry.isFile()) {
const cadInfo = getCadInfo(entry.name);
if (cadInfo) {
scannedFiles += 1;
onProgress?.({ phase: 'index-dup', scanned: scannedFiles, file: entry.name });
cadKeyIndex.add(toCadKey(cadInfo));
}
}
}
await Promise.all(subdirPromises);
}
await Promise.all(
topDirs.map(async ({ name, fullPath }) => {
onProgress?.({ phase: 'index-dest', current: completed, total, folder: name });
if (CODE_FOLDER_REGEX.test(name)) {
const rows = folderIndex.get(name) || [];
rows.push(fullPath);
folderIndex.set(name, rows);
}
await walkSubtree(fullPath, 1);
completed += 1;
onProgress?.({ phase: 'index-dest', current: completed, total, folder: name });
})
);
return { folderIndex, cadKeyIndex };
}
module.exports = { buildDestinationIndexes, toCadKey };

View File

@@ -8,13 +8,15 @@ function toCadKey(cadInfo) {
return String(cadInfo?.key || '').toLowerCase();
}
async function buildExistingCadKeyIndex(destinationRoot) {
async function buildExistingCadKeyIndex(destinationRoot, onProgress) {
const keys = new Set();
if (!destinationRoot || !(await fs.pathExists(destinationRoot))) {
return keys;
}
let scanned = 0;
async function walk(currentDir, depth) {
if (depth > MAX_SCAN_DEPTH) {
return;
@@ -44,6 +46,8 @@ async function buildExistingCadKeyIndex(destinationRoot) {
continue;
}
scanned += 1;
onProgress?.({ phase: 'index-dup', scanned, file: entry.name });
keys.add(toCadKey(cadInfo));
}
}

View File

@@ -1,9 +1,45 @@
const fs = require('fs-extra');
const path = require('path');
const { getDestinationDecision, getCadInfo } = require('./router');
const { buildDestinationIndex } = require('./destinationIndex');
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
function parseNumericVersion(version) {
const rawVersion = String(version || '').trim();
if (!rawVersion || !/^\d+$/.test(rawVersion)) {
return null;
}
try {
return BigInt(rawVersion);
} catch {
return null;
}
}
function buildHighestNumericVersionByCadKey(files) {
const index = new Map();
for (const row of files) {
const cadInfo = getCadInfo(row.fileName);
if (!cadInfo) {
continue;
}
const version = parseNumericVersion(cadInfo.version);
if (version === null) {
continue;
}
const key = toCadKey(cadInfo);
const current = index.get(key);
if (current === undefined || version > current) {
index.set(key, version);
}
}
return index;
}
async function collectFilesRecursively(rootDir) {
const files = [];
@@ -29,10 +65,12 @@ async function collectFilesRecursively(rootDir) {
return files;
}
async function processFolder(folder, config) {
async function processFolder(folder, config, onProgress) {
onProgress?.({ phase: 'scan' });
const files = await collectFilesRecursively(folder);
const destinationIndex = await buildDestinationIndex(config?.destination);
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress);
const sourceMaxVersions = buildHighestNumericVersionByCadKey(files);
const result = {
scanned: 0,
copied: 0,
@@ -42,18 +80,44 @@ async function processFolder(folder, config) {
details: [],
};
for (const { fullPath, fileName } of files) {
const total = files.length;
for (let i = 0; i < files.length; i++) {
const { fullPath, fileName } = files[i];
const file = fileName;
result.scanned += 1;
onProgress?.({ phase: 'copy', current: i + 1, total, file });
const cadInfo = getCadInfo(file);
if (!cadInfo) {
const skippedTarget = await getSkippedTarget(file);
await fs.copy(fullPath, skippedTarget.destinationPath);
result.skipped += 1;
result.copied += 1;
continue;
}
if (existingCadKeys.has(toCadKey(cadInfo))) {
const duplicateTarget = await getDuplicateTarget(file);
const incomingVersion = parseNumericVersion(cadInfo.version);
const highestInSource = sourceMaxVersions.get(toCadKey(cadInfo));
if (incomingVersion !== null && highestInSource !== undefined && incomingVersion < highestInSource) {
result.details.push({
file,
reason: 'Versione piu alta presente nei file da smistare',
});
continue;
}
const duplicateTarget = await prepareDuplicateTarget(file);
if (!duplicateTarget.shouldCopy) {
result.details.push({
file,
destination: duplicateTarget.destinationDir,
reason: duplicateTarget.reason || 'Versione piu alta gia presente',
});
continue;
}
await fs.copy(fullPath, duplicateTarget.destinationPath, { overwrite: false });
result.copied += 1;
@@ -70,7 +134,16 @@ async function processFolder(folder, config) {
const destDir = decision.destination;
if (!destDir) {
const unroutedTarget = await getUnroutedTarget(file);
const unroutedTarget = await prepareUnroutedTarget(file);
if (!unroutedTarget.shouldCopy) {
result.details.push({
file,
destination: unroutedTarget.destinationDir,
reason: unroutedTarget.reason || 'Versione piu alta gia presente',
});
continue;
}
await fs.copy(fullPath, unroutedTarget.destinationPath, { overwrite: false });
result.copied += 1;

View File

@@ -1,13 +1,12 @@
const path = require('path');
function normalizeCadType(ext) {
const lowerExt = String(ext || '').toLowerCase();
return lowerExt === 'drw' ? 'dwr' : lowerExt;
return String(ext || '').toLowerCase();
}
function getCadInfo(filename) {
const baseName = path.basename(filename);
const match = baseName.match(/^(.*)\.(prt|asm|drw|dwr)(?:\.([^.]+))?$/i);
const match = baseName.match(/^(.*)\.(prt|asm|drw)(?:\.([^.]+))?$/i);
if (!match) {
return null;
}
@@ -17,6 +16,9 @@ function getCadInfo(filename) {
const version = match[3] || '';
const digits = rawCode.replace(/\D/g, '');
const routingGroup = digits.length >= 5 ? digits.slice(2, 5) : null;
const subfolder = digits.length >= 2
? (digits[1] === '1' ? 'A' : digits[1] === '2' ? 'M_B' : null)
: null;
return {
code: rawCode,
@@ -25,6 +27,7 @@ function getCadInfo(filename) {
version,
key: `${rawCode}.${type}`,
routingGroup,
subfolder,
};
}
@@ -58,7 +61,14 @@ function getDestinationDecision(filename, config, destinationIndex) {
};
}
return { destination: candidates[0], reason: null };
if (!cadInfo.subfolder) {
return {
destination: null,
reason: `Prefisso non riconosciuto (${cadInfo.digits[1]}): atteso 91 (→ A) o 92 (→ M_B)`,
};
}
return { destination: path.join(candidates[0], cadInfo.subfolder), reason: null };
}
function findDestination(filename, config, destinationIndex) {

View File

@@ -1,11 +1,14 @@
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const { getCadInfo } = require('./router');
const PRIMARY_UNROUTED_DIR = '/cadroute/__NON_SMISTATI';
const HOME_UNROUTED_DIR = path.join(os.homedir(), '.cadroute', '__NON_SMISTATI');
const PRIMARY_DUPLICATES_DIR = '/cadroute/duplicati';
const HOME_DUPLICATES_DIR = path.join(os.homedir(), '.cadroute', 'duplicati');
const PRIMARY_DUPLICATES_DIR = '/cadroute/__DUPLICATI';
const HOME_DUPLICATES_DIR = path.join(os.homedir(), '.cadroute', '__DUPLICATI');
const PRIMARY_SKIPPED_DIR = '/cadroute/__SALTATI';
const HOME_SKIPPED_DIR = path.join(os.homedir(), '.cadroute', '__SALTATI');
const SPECIAL_TARGETS = {
unrouted: {
@@ -16,6 +19,10 @@ const SPECIAL_TARGETS = {
primary: PRIMARY_DUPLICATES_DIR,
fallback: HOME_DUPLICATES_DIR,
},
skipped: {
primary: PRIMARY_SKIPPED_DIR,
fallback: HOME_SKIPPED_DIR,
},
};
const resolvedDirs = new Map();
@@ -83,6 +90,116 @@ async function getDuplicateTarget(fileName) {
return getTarget('duplicates', fileName);
}
function parseNumericVersion(version) {
const rawVersion = String(version || '').trim();
if (!rawVersion || !/^\d+$/.test(rawVersion)) {
return null;
}
try {
return BigInt(rawVersion);
} catch {
return null;
}
}
function normalizeCadKey(cadInfo) {
return String(cadInfo?.key || '').toLowerCase();
}
async function findComparableVersionFiles(destinationDir, incomingCadInfo) {
const incomingKey = normalizeCadKey(incomingCadInfo);
const entries = await fs.readdir(destinationDir, { withFileTypes: true }).catch(() => []);
const comparable = [];
for (const entry of entries) {
if (!entry.isFile()) {
continue;
}
const cadInfo = getCadInfo(entry.name);
if (!cadInfo || normalizeCadKey(cadInfo) !== incomingKey) {
continue;
}
const numericVersion = parseNumericVersion(cadInfo.version);
if (numericVersion === null) {
continue;
}
comparable.push({
path: path.join(destinationDir, entry.name),
version: numericVersion,
name: entry.name,
});
}
return comparable;
}
async function prepareSpecialTarget(kind, fileName) {
const destinationDir = await resolveTargetDir(kind);
const incomingCadInfo = getCadInfo(fileName);
if (!incomingCadInfo) {
const destinationPath = await getUniquePath(destinationDir, fileName);
return { shouldCopy: true, destinationDir, destinationPath };
}
const incomingVersion = parseNumericVersion(incomingCadInfo.version);
if (incomingVersion === null) {
const destinationPath = await getUniquePath(destinationDir, fileName);
return { shouldCopy: true, destinationDir, destinationPath };
}
const comparable = await findComparableVersionFiles(destinationDir, incomingCadInfo);
if (!comparable.length) {
const destinationPath = await getUniquePath(destinationDir, fileName);
return { shouldCopy: true, destinationDir, destinationPath };
}
const highest = comparable.reduce((max, row) => (row.version > max.version ? row : max));
if (incomingVersion <= highest.version) {
return {
shouldCopy: false,
destinationDir,
reason: `Versione piu alta gia presente (${highest.name})`,
};
}
await Promise.all(comparable.map((row) => fs.remove(row.path)));
const destinationPath = await getUniquePath(destinationDir, fileName);
return {
shouldCopy: true,
destinationDir,
destinationPath,
cleaned: comparable.length,
};
}
async function prepareUnroutedTarget(fileName) {
return prepareSpecialTarget('unrouted', fileName);
}
async function prepareDuplicateTarget(fileName) {
return prepareSpecialTarget('duplicates', fileName);
}
async function resolveSkippedDir() {
return resolveTargetDir('skipped');
}
async function getSkippedTarget(fileName) {
return getTarget('skipped', fileName);
}
async function listSkippedFiles() {
return listTargetFiles('skipped');
}
async function clearSkippedFiles() {
return clearTargetFiles('skipped');
}
async function listFilesRecursively(rootDir) {
const files = [];
@@ -156,14 +273,22 @@ async function clearDuplicateFiles() {
module.exports = {
getUnroutedTarget,
getDuplicateTarget,
getSkippedTarget,
prepareUnroutedTarget,
prepareDuplicateTarget,
resolveUnroutedDir,
resolveDuplicatesDir,
resolveSkippedDir,
listUnroutedFiles,
listDuplicateFiles,
listSkippedFiles,
clearUnroutedFiles,
clearDuplicateFiles,
clearSkippedFiles,
PRIMARY_UNROUTED_DIR,
HOME_UNROUTED_DIR,
PRIMARY_DUPLICATES_DIR,
HOME_DUPLICATES_DIR,
PRIMARY_SKIPPED_DIR,
HOME_SKIPPED_DIR,
};

View File

@@ -3,14 +3,65 @@ const fs = require('fs-extra');
const path = require('path');
const { pipeline } = require('stream/promises');
const { getDestinationDecision, getCadInfo } = require('./router');
const { buildDestinationIndex } = require('./destinationIndex');
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
const { buildDestinationIndexes, toCadKey } = require('./destinationScanner');
const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted');
function parseNumericVersion(version) {
const rawVersion = String(version || '').trim();
if (!rawVersion || !/^\d+$/.test(rawVersion)) {
return null;
}
try {
return BigInt(rawVersion);
} catch {
return null;
}
}
async function buildZipHighestNumericVersionByCadKey(zipPath) {
const index = new Map();
let directory;
try {
directory = await unzipper.Open.file(zipPath);
} catch {
return { index, total: 0 };
}
for (const row of directory.files || []) {
if (row.type !== 'File') {
continue;
}
const baseName = path.basename(row.path || '');
const cadInfo = getCadInfo(baseName);
if (!cadInfo) {
continue;
}
const version = parseNumericVersion(cadInfo.version);
if (version === null) {
continue;
}
const key = toCadKey(cadInfo);
const current = index.get(key);
if (current === undefined || version > current) {
index.set(key, version);
}
}
return { index, total: (directory.files || []).filter((f) => f.type === 'File').length };
}
async function processZip(zipPath, config, onProgress) {
onProgress?.({ phase: 'scan' });
const { index: sourceMaxVersions, total } = await buildZipHighestNumericVersionByCadKey(zipPath);
const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress);
async function processZip(zipPath, config) {
const stream = fs.createReadStream(zipPath).pipe(unzipper.Parse({ forceStream: true }));
const destinationIndex = await buildDestinationIndex(config?.destination);
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
const result = {
scanned: 0,
copied: 0,
@@ -20,6 +71,8 @@ async function processZip(zipPath, config) {
details: [],
};
let current = 0;
for await (const entry of stream) {
if (entry.type !== 'File') {
entry.autodrain();
@@ -28,17 +81,42 @@ async function processZip(zipPath, config) {
const file = entry.path;
const baseName = path.basename(file);
current += 1;
result.scanned += 1;
onProgress?.({ phase: 'copy', current, total, file: baseName });
const cadInfo = getCadInfo(baseName);
if (!cadInfo) {
const skippedTarget = await getSkippedTarget(baseName);
await pipeline(entry, fs.createWriteStream(skippedTarget.destinationPath));
result.skipped += 1;
entry.autodrain();
result.copied += 1;
continue;
}
if (existingCadKeys.has(toCadKey(cadInfo))) {
const duplicateTarget = await getDuplicateTarget(baseName);
const incomingVersion = parseNumericVersion(cadInfo.version);
const highestInSource = sourceMaxVersions.get(toCadKey(cadInfo));
if (incomingVersion !== null && highestInSource !== undefined && incomingVersion < highestInSource) {
result.details.push({
file: baseName,
reason: 'Versione piu alta presente nei file da smistare',
});
entry.autodrain();
continue;
}
const duplicateTarget = await prepareDuplicateTarget(baseName);
if (!duplicateTarget.shouldCopy) {
result.details.push({
file: baseName,
destination: duplicateTarget.destinationDir,
reason: duplicateTarget.reason || 'Versione piu alta gia presente',
});
entry.autodrain();
continue;
}
await pipeline(entry, fs.createWriteStream(duplicateTarget.destinationPath));
result.copied += 1;
@@ -55,7 +133,17 @@ async function processZip(zipPath, config) {
const destDir = decision.destination;
if (!destDir) {
const unroutedTarget = await getUnroutedTarget(baseName);
const unroutedTarget = await prepareUnroutedTarget(baseName);
if (!unroutedTarget.shouldCopy) {
result.details.push({
file: baseName,
destination: unroutedTarget.destinationDir,
reason: unroutedTarget.reason || 'Versione piu alta gia presente',
});
entry.autodrain();
continue;
}
await pipeline(entry, fs.createWriteStream(unroutedTarget.destinationPath));
result.copied += 1;

14
sop.md
View File

@@ -1,4 +1,4 @@
# SOP CAD File Router MVP
# SOP CadRoute MVP
## Obiettivo
Realizzare un'app desktop (.exe) che:
@@ -35,8 +35,8 @@ npm -v
## 2. Creazione progetto
```
mkdir cad-file-router
cd cad-file-router
mkdir cadroute
cd cadroute
npm init -y
```
@@ -52,7 +52,7 @@ npm install electron-builder --save-dev
## 3. Struttura progetto
```
cad-file-router
cadroute
├── package.json
├── main.js
@@ -261,7 +261,7 @@ contextBridge.exposeInMainWorld("api", {
`renderer/index.html`
```
<h2>CAD File Router</h2>
<h2>CadRoute</h2>
<button onclick="window.api.selectFolder()">
Process Folder
@@ -298,7 +298,7 @@ Aggiungere nel package.json:
```
"build": {
"appId": "com.cad.router",
"appId": "com.cadroute",
"win": {
"target": "nsis"
}
@@ -315,7 +315,7 @@ Output:
```
dist/
cad-file-router Setup.exe
CadRoute Setup.exe
```
---