igurielidze dc86f65fa7 Simplify deploy: just write files, no restart attempt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:12:57 +04:00

290 lines
10 KiB
TypeScript

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import fs from 'fs';
import path from 'path';
// @ts-ignore — xlsx has no type declarations
import XLSX from 'xlsx';
function generateProjectManifest() {
const staticDir = path.resolve('static');
const projectesDir = path.join(staticDir, 'projectes');
if (!fs.existsSync(projectesDir)) return;
const MCM_REGEX = /SYSDL[_ ]+(MCM\d+)/i;
const projects: { name: string; mcms: { name: string; excelPath: string; pdfPath: string | null }[] }[] = [];
for (const projectName of fs.readdirSync(projectesDir)) {
const projectDir = path.join(projectesDir, projectName);
if (!fs.statSync(projectDir).isDirectory()) continue;
const excelDir = path.join(projectDir, 'excel');
const pdfDir = path.join(projectDir, 'pdf');
const mcmMap = new Map<string, { excelPath: string; pdfPath: string | null }>();
if (fs.existsSync(excelDir)) {
for (const f of fs.readdirSync(excelDir)) {
if (!f.endsWith('.xlsx')) continue;
const match = f.match(MCM_REGEX);
if (!match) continue;
const mcm = match[1];
mcmMap.set(mcm, {
excelPath: `/projectes/${projectName}/excel/${f}`,
pdfPath: null,
});
}
}
if (fs.existsSync(pdfDir)) {
for (const f of fs.readdirSync(pdfDir)) {
if (!f.endsWith('.pdf')) continue;
const match = f.match(MCM_REGEX);
if (!match) continue;
const mcm = match[1];
if (mcmMap.has(mcm)) {
mcmMap.get(mcm)!.pdfPath = `/projectes/${projectName}/pdf/${f}`;
} else {
mcmMap.set(mcm, {
excelPath: '',
pdfPath: `/projectes/${projectName}/pdf/${f}`,
});
}
}
}
if (mcmMap.size > 0) {
const mcms = [...mcmMap.entries()]
.map(([name, paths]) => ({ name, ...paths }))
.sort((a, b) => a.name.localeCompare(b.name));
projects.push({ name: projectName, mcms });
}
}
projects.sort((a, b) => a.name.localeCompare(b.name));
fs.writeFileSync(
path.join(projectesDir, 'manifest.json'),
JSON.stringify(projects, null, 2)
);
}
/** Classify a device name to a symbol ID, or null to skip */
function classifyDevice(dev: string): string | null {
// Skip
if (/ENSH|ENW/.test(dev)) return null;
if (/VFD_DI?SC/.test(dev)) return null;
if (/_LT$/.test(dev)) return null;
if (/EPC\d+_\d+/.test(dev)) return null;
if (/BCN\d+_[HBWG]$/.test(dev)) return null;
if (dev === 'SPARE') return null;
if (/ESTOP/.test(dev)) return null;
if (/SCALE/.test(dev)) return null;
if (/_STO\d*$/.test(dev)) return null;
if (/PRX\d*$/.test(dev)) return null;
if (/LRPE/.test(dev)) return null;
// Match
if (/EPC\d*$/.test(dev)) return 'epc';
if (/[TLJF]PE\d*$/.test(dev)) return 'photoeye';
if (/BDS\d+_[RS]$/.test(dev)) return 'photoeye';
if (/TS\d+_[RS]$/.test(dev)) return 'photoeye';
if (/JR\d*_PB$/.test(dev)) return 'jam_reset';
if (/SS\d+_S[TP]*PB$/.test(dev)) return 'start_stop';
if (/S\d+_PB$/.test(dev) && !/^SS/.test(dev)) return 'start';
if (/EN\d+_PB$/.test(dev)) return 'chute_enable';
if (/PR\d+_PB$/.test(dev)) return 'package_release';
if (/SOL\d*$/.test(dev)) return 'solenoid';
if (/DIV\d+_LS/.test(dev)) return 'diverter';
if (/BCN\d*$/.test(dev) || /BCN\d+_[AR]$/.test(dev)) return 'beacon';
if (/^PDP\d+_CB/.test(dev)) return 'pdp';
if (/^PDP\d+_PMM/.test(dev)) return 'pdp';
if (/FIO[HM]?\d*$/.test(dev) || /SIO\d*$/.test(dev)) return 'fio_sio_fioh';
if (/PS\d*$/.test(dev) && !/^PS\d/.test(dev)) return 'pressure_sensor';
return null;
}
/** Extract zone prefix from device name, e.g. "NCP1_1_TPE1" -> "NCP1_1" */
function extractZone(dev: string): string {
// PDP devices: PDP04_CB1 -> PDP04
const pdpM = dev.match(/^(PDP\d+)/);
if (pdpM) return pdpM[1];
// Standard: ZONE_NUM_REST
const m = dev.match(/^([A-Z]+\d*_\d+[A-Z]?)/);
return m ? m[1] : 'OTHER';
}
function generateDeviceManifest() {
const projectesDir = path.resolve('..', 'projectes');
const staticDir = path.resolve('static', 'projectes');
if (!fs.existsSync(projectesDir)) return;
const manifest: Record<string, { id: string; svg: string; zone: string }[]> = {};
for (const projectName of fs.readdirSync(projectesDir)) {
const projectDir = path.join(projectesDir, projectName);
if (!fs.statSync(projectDir).isDirectory()) continue;
const excelDir = path.join(projectDir, 'excel');
if (!fs.existsSync(excelDir)) continue;
// Find device IO file and IP addresses file
const files = fs.readdirSync(excelDir).filter((f: string) => f.endsWith('.xlsx'));
const ioFile = files.find((f: string) => /Devices?\s*IO/i.test(f));
const ipFile = files.find((f: string) => /IP\s*Address/i.test(f));
if (!ioFile) continue;
const ioWb = XLSX.readFile(path.join(excelDir, ioFile));
// Also load IP addresses for VFDs, FIOMs, DPMs
let ipWb: XLSX.WorkBook | null = null;
if (ipFile) {
try { ipWb = XLSX.readFile(path.join(excelDir, ipFile)); } catch {}
}
for (const sheetName of ioWb.SheetNames) {
const mcmMatch = sheetName.match(/^(MCM\d+)$/);
if (!mcmMatch) continue;
const mcm = mcmMatch[1];
const seen = new Set<string>();
const devices: { id: string; svg: string; zone: string }[] = [];
// Parse IO sheet
const ws = ioWb.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' }) as string[][];
// Extract FIO/SIO controllers from column 0 (controller name)
for (let i = 1; i < rows.length; i++) {
const ctrl = String(rows[i][0] || '').trim();
if (!ctrl || seen.has(ctrl)) continue;
if (/FIO|SIO/i.test(ctrl)) {
seen.add(ctrl);
devices.push({ id: ctrl, svg: 'fio_sio_fioh', zone: extractZone(ctrl) });
}
}
// Extract assigned devices from column 3
for (let i = 1; i < rows.length; i++) {
const dev = String(rows[i][3] || '').trim();
if (!dev || seen.has(dev)) continue;
seen.add(dev);
const svg = classifyDevice(dev);
if (!svg) continue;
const zone = extractZone(dev);
// Consolidate PDP: only add once per PDP number
if (svg === 'pdp') {
const pdpId = zone; // e.g. PDP04
if (devices.some(d => d.id === pdpId)) continue;
devices.push({ id: pdpId, svg, zone });
continue;
}
// Consolidate beacon: BCN1_A + BCN1_H -> one BCN1 per zone
if (svg === 'beacon') {
const bcnBase = dev.replace(/_[ARHBWG]$/, '');
if (devices.some(d => d.id === bcnBase)) continue;
devices.push({ id: bcnBase, svg, zone });
continue;
}
devices.push({ id: dev, svg, zone });
}
// Parse IP/network sheet for VFDs, FIOMs, DPMs
if (ipWb) {
const ipSheets = ipWb.SheetNames.filter((s: string) => s.toUpperCase().includes(mcm));
for (const ipSheet of ipSheets) {
const ipWs = ipWb.Sheets[ipSheet];
const ipRows = XLSX.utils.sheet_to_json(ipWs, { header: 1, defval: '' }) as string[][];
for (const row of ipRows) {
for (const cell of row) {
const val = String(cell || '').trim();
if (!val || val.startsWith('11.') || seen.has(val)) continue;
if (/^[A-Z]+\d*_\d+.*VFD$/.test(val)) {
seen.add(val);
devices.push({ id: val, svg: 'conveyor', zone: extractZone(val) });
} else if (/^[A-Z]+\d*_\d+.*FIOM\d*$/.test(val)) {
seen.add(val);
devices.push({ id: val, svg: 'fio_sio_fioh', zone: extractZone(val) });
} else if (/^[A-Z]+\d*_\d+.*DPM\d*$/.test(val) && !/_P\d+$/.test(val)) {
seen.add(val);
devices.push({ id: val, svg: 'dpm', zone: extractZone(val) });
}
}
}
}
}
// Always include the MCM symbol itself
if (!devices.some(d => d.id === mcm)) {
devices.push({ id: mcm, svg: 'mcm', zone: mcm });
}
// Sort by zone then id
devices.sort((a, b) => a.zone.localeCompare(b.zone) || a.id.localeCompare(b.id));
manifest[mcm] = devices;
}
}
fs.writeFileSync(
path.join(staticDir, 'devices-manifest.json'),
JSON.stringify(manifest, null, 2)
);
}
export default defineConfig({
plugins: [
{
name: 'generate-project-manifest',
buildStart() {
generateProjectManifest();
generateDeviceManifest();
},
configureServer(server) {
generateProjectManifest();
generateDeviceManifest();
// Re-generate on file changes in static/projectes
server.watcher.add(path.resolve('static/projectes'));
server.watcher.on('add', (p) => {
if (p.includes('projectes') && !p.endsWith('manifest.json')) {
generateProjectManifest();
}
});
},
},
{
name: 'ignition-deploy',
configureServer(server) {
server.middlewares.use('/api/deploy-ignition', async (req, res) => {
if (req.method !== 'POST') { res.statusCode = 405; res.end('Method not allowed'); return; }
let body = '';
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
req.on('end', () => {
try {
const { projectName, viewName, viewJson, resourceJson } = JSON.parse(body);
const ignitionBase = 'C:/Program Files/Inductive Automation/Ignition/data/projects';
const viewDir = path.join(ignitionBase, projectName, 'com.inductiveautomation.perspective/views', viewName);
// Delete existing view folder first to force Ignition to detect the change
if (fs.existsSync(viewDir)) {
fs.rmSync(viewDir, { recursive: true, force: true });
}
fs.mkdirSync(viewDir, { recursive: true });
fs.writeFileSync(path.join(viewDir, 'view.json'), viewJson);
fs.writeFileSync(path.join(viewDir, 'resource.json'), resourceJson);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true, path: viewDir }));
} catch (err: any) {
res.statusCode = 500;
res.end(JSON.stringify({ error: err.message }));
}
});
});
},
},
sveltekit(),
],
});