Compare commits

...

7 Commits

Author SHA1 Message Date
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
17 changed files with 974 additions and 24 deletions

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"

24
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');
@@ -13,7 +13,7 @@ const {
} = require('./services/unrouted');
const CAD_EXTENSIONS = ['prt', 'asm', 'dwr'];
const DEFAULT_DESTINATION = './output/cad';
const DEFAULT_DESTINATION = 'X:\\';
const SETTINGS_FILENAME = 'cad-router-settings.json';
const LINUX_RUNTIME_DIR = '.cadroute';
@@ -68,6 +68,7 @@ function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 640,
icon: path.join(__dirname, 'build', 'icon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
@@ -78,6 +79,25 @@ 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);
}
Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: 'Help',
submenu: [{ label: 'Documentazione', click: () => createDocsWindow() }],
},
]));
app.whenReady().then(async () => {
await ensureRuntimeDirectory();
const persistedDestination = await loadPersistedDestination();

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"
}
}

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

@@ -0,0 +1,277 @@
<!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>
</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">.dwr</span></td>
<td>Drawing</td>
<td>File di disegno Creo</td>
</tr>
<tr>
<td><span class="badge skip">altri</span></td>
<td></td>
<td>Ignorati (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 saltati.</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 ignorati perché non CAD (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>Linux:</strong> <code>~/.cadroute/__NON_SMISTATI/</code></li>
<li><strong>Windows:</strong> <code>%APPDATA%\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>Linux:</strong> <code>~/.cadroute/duplicati/</code></li>
<li><strong>Windows:</strong> <code>%APPDATA%\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>
</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;
@@ -255,7 +255,7 @@
</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">

View File

@@ -3,7 +3,44 @@ const path = require('path');
const { getDestinationDecision, getCadInfo } = require('./router');
const { buildDestinationIndex } = require('./destinationIndex');
const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex');
const { getUnroutedTarget, getDuplicateTarget } = require('./unrouted');
const { prepareUnroutedTarget, prepareDuplicateTarget } = 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 = [];
@@ -33,6 +70,7 @@ async function processFolder(folder, config) {
const files = await collectFilesRecursively(folder);
const destinationIndex = await buildDestinationIndex(config?.destination);
const existingCadKeys = await buildExistingCadKeyIndex(config?.destination);
const sourceMaxVersions = buildHighestNumericVersionByCadKey(files);
const result = {
scanned: 0,
copied: 0,
@@ -53,7 +91,26 @@ async function processFolder(folder, config) {
}
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 +127,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,6 +1,7 @@
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');
@@ -83,6 +84,100 @@ 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 listFilesRecursively(rootDir) {
const files = [];
@@ -156,6 +251,8 @@ async function clearDuplicateFiles() {
module.exports = {
getUnroutedTarget,
getDuplicateTarget,
prepareUnroutedTarget,
prepareDuplicateTarget,
resolveUnroutedDir,
resolveDuplicatesDir,
listUnroutedFiles,

View File

@@ -5,12 +5,62 @@ 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 { prepareUnroutedTarget, prepareDuplicateTarget } = 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;
}
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;
}
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 sourceMaxVersions = await buildZipHighestNumericVersionByCadKey(zipPath);
const result = {
scanned: 0,
copied: 0,
@@ -38,7 +88,28 @@ async function processZip(zipPath, config) {
}
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 +126,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
```
---