Initial commit — Mek-Tech AI Consultancy Framework v2.0

This commit is contained in:
mo
2026-07-12 21:54:22 +00:00
commit 18921c021b
180 changed files with 28518 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const { getDb } = require('../db');
const AI_CONFIG_PATH = path.join(__dirname, '..', 'data', 'ai_config.json');
const PROVIDER_PRESETS = {
openrouter: {
label: 'OpenRouter',
base_url: 'https://openrouter.ai/api/v1',
models: ['qwen/qwen3-coder:free', 'meta-llama/llama-3.3-70b-instruct:free', 'google/gemma-3-27b-it:free', 'anthropic/claude-3.5-sonnet', 'openai/gpt-4o-mini']
},
openai: {
label: 'OpenAI',
base_url: 'https://api.openai.com/v1',
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-mini']
},
anthropic: {
label: 'Anthropic',
base_url: 'https://api.anthropic.com/v1',
models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229']
},
deepseek: {
label: 'DeepSeek',
base_url: 'https://api.deepseek.com/v1',
models: ['deepseek-chat', 'deepseek-reasoner']
},
groq: {
label: 'Groq',
base_url: 'https://api.groq.com/openai/v1',
models: ['llama-3.3-70b-versatile', 'mixtral-8x7b-32768']
},
ollama: {
label: 'Ollama (local)',
base_url: 'http://localhost:11434/v1',
models: ['llama3.2', 'mistral', 'codellama', 'qwen2.5']
},
azure: {
label: 'Azure OpenAI',
base_url: '',
models: ['gpt-4o', 'gpt-4o-mini']
},
custom: {
label: 'Custom (OpenAI-compatible)',
base_url: '',
models: []
}
};
function initAiSchema() {
const db = getDb();
db.exec(`
CREATE TABLE IF NOT EXISTS ai_providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
provider_type TEXT NOT NULL DEFAULT 'openrouter',
api_key TEXT DEFAULT '',
base_url TEXT DEFAULT '',
default_model TEXT DEFAULT '',
is_active INTEGER DEFAULT 1,
is_default INTEGER DEFAULT 0,
extra_headers TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS integrations (
id TEXT PRIMARY KEY,
name TEXT DEFAULT '',
api_key TEXT DEFAULT '',
webhook_url TEXT DEFAULT '',
config TEXT DEFAULT '{}',
status TEXT DEFAULT 'inactive',
created_at TEXT DEFAULT (datetime('now'))
);
`);
const count = db.prepare('SELECT COUNT(*) as c FROM ai_providers').get().c;
if (count === 0 && process.env.DEEPSEEK_API_KEY) {
db.prepare(`INSERT INTO ai_providers (name, provider_type, api_key, base_url, default_model, is_default)
VALUES (?, ?, ?, ?, ?, 1)`).run(
'OpenRouter Default', 'openrouter', process.env.DEEPSEEK_API_KEY,
'https://openrouter.ai/api/v1', 'qwen/qwen3-coder:free'
);
}
}
let _aiSchemaReady = false;
function ensureAiSchema() {
if (_aiSchemaReady) return;
initAiSchema();
_aiSchemaReady = true;
}
function getProviders() {
ensureAiSchema();
return getDb().prepare('SELECT id, name, provider_type, base_url, default_model, is_active, is_default, created_at, updated_at FROM ai_providers ORDER BY is_default DESC, name').all();
}
function getProvider(id) {
ensureAiSchema();
return getDb().prepare('SELECT * FROM ai_providers WHERE id = ?').get(id);
}
function getDefaultProvider() {
ensureAiSchema();
let p = getDb().prepare('SELECT * FROM ai_providers WHERE is_default = 1 AND is_active = 1 LIMIT 1').get();
if (!p) p = getDb().prepare('SELECT * FROM ai_providers WHERE is_active = 1 ORDER BY id LIMIT 1').get();
return p;
}
function saveProvider(data) {
ensureAiSchema();
const db = getDb();
if (data.is_default) {
db.prepare('UPDATE ai_providers SET is_default = 0').run();
}
if (data.id) {
db.prepare(`UPDATE ai_providers SET name=?, provider_type=?, api_key=?, base_url=?, default_model=?,
is_active=?, is_default=?, updated_at=datetime('now') WHERE id=?`).run(
data.name, data.provider_type, data.api_key || '', data.base_url || '',
data.default_model || '', data.is_active ? 1 : 0, data.is_default ? 1 : 0, data.id
);
syncAiConfigFile();
return data.id;
}
const r = db.prepare(`INSERT INTO ai_providers (name, provider_type, api_key, base_url, default_model, is_active, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
data.name, data.provider_type, data.api_key || '', data.base_url || '',
data.default_model || '', data.is_active !== false ? 1 : 0, data.is_default ? 1 : 0
);
syncAiConfigFile();
return r.lastInsertRowid;
}
function deleteProvider(id) {
ensureAiSchema();
getDb().prepare('DELETE FROM ai_providers WHERE id = ?').run(id);
syncAiConfigFile();
}
function syncAiConfigFile() {
ensureAiSchema();
const providers = getDb().prepare('SELECT * FROM ai_providers WHERE is_active = 1').all();
const defaultProvider = providers.find(p => p.is_default) || providers[0] || null;
const config = {
default_provider_id: defaultProvider?.id || null,
providers: providers.map(p => ({
id: p.id,
name: p.name,
provider_type: p.provider_type,
api_key: p.api_key,
base_url: p.base_url || PROVIDER_PRESETS[p.provider_type]?.base_url || '',
default_model: p.default_model,
is_default: !!p.is_default
}))
};
const dir = path.dirname(AI_CONFIG_PATH);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2));
const leadgenConfig = {
api_key: defaultProvider?.api_key || process.env.DEEPSEEK_API_KEY || '',
base_url: defaultProvider?.base_url || 'https://openrouter.ai/api/v1',
model: defaultProvider?.default_model || 'qwen/qwen3-coder:free',
provider_type: defaultProvider?.provider_type || 'openrouter'
};
fs.writeFileSync(path.join(dir, 'leadgen_config.json'), JSON.stringify(leadgenConfig, null, 2));
}
function maskKey(key) {
if (!key || key.length < 8) return key ? '••••••••' : '';
return key.slice(0, 6) + '••••' + key.slice(-4);
}
function chatCompletion(messages, options = {}) {
return new Promise((resolve, reject) => {
const provider = options.providerId
? getProvider(options.providerId)
: getDefaultProvider();
if (!provider || !provider.api_key) {
return reject(new Error('Geen actieve AI provider geconfigureerd. Ga naar /ai om een provider toe te voegen.'));
}
const preset = PROVIDER_PRESETS[provider.provider_type] || PROVIDER_PRESETS.custom;
const baseUrl = provider.base_url || preset.base_url;
const model = options.model || provider.default_model || preset.models[0];
let url;
try {
url = new URL('/chat/completions', baseUrl.endsWith('/') ? baseUrl : baseUrl + '/');
} catch (e) {
return reject(new Error('Ongeldige base URL: ' + baseUrl));
}
const body = JSON.stringify({
model,
messages,
temperature: options.temperature ?? 0.7,
max_tokens: options.max_tokens ?? 4096
});
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body)
};
if (provider.provider_type === 'anthropic') {
headers['x-api-key'] = provider.api_key;
headers['anthropic-version'] = '2023-06-01';
} else {
headers['Authorization'] = `Bearer ${provider.api_key}`;
}
if (provider.provider_type === 'openrouter') {
headers['HTTP-Referer'] = process.env.BASE_URL || 'http://localhost:3000';
headers['X-Title'] = 'Mek-Tech Platform';
}
const reqOpts = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname,
method: 'POST',
headers,
timeout: 120000
};
const req = lib.request(reqOpts, (res) => {
let data = '';
res.on('data', c => { data += c; });
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 400) {
reject(new Error(parsed.error?.message || parsed.message || data.slice(0, 200)));
} else {
resolve(parsed);
}
} catch (e) {
reject(new Error('AI response parse error: ' + data.slice(0, 200)));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('AI request timeout')); });
req.write(body);
req.end();
});
}
module.exports = {
PROVIDER_PRESETS,
initAiSchema: ensureAiSchema,
getProviders,
getProvider,
getDefaultProvider,
saveProvider,
deleteProvider,
syncAiConfigFile,
maskKey,
chatCompletion
};
+279
View File
@@ -0,0 +1,279 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
const { getDb } = require('../db');
const APP_VERSION = '2.0.0';
const MAX_STORED_BACKUPS = 20;
function getPaths() {
const root = path.join(__dirname, '..');
const dataDir = process.env.DB_PATH
? path.dirname(path.resolve(process.env.DB_PATH))
: path.join(root, 'data');
return {
root,
dataDir,
dbPath: process.env.DB_PATH || path.join(dataDir, 'consulting.db'),
sessionsPath: path.join(dataDir, 'sessions.db'),
uploadsDir: path.join(dataDir, 'uploads'),
backupsDir: path.join(dataDir, 'backups')
};
}
function ensureBackupsDir() {
const { backupsDir } = getPaths();
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
return backupsDir;
}
function checkpointDatabases() {
try {
const db = getDb();
db.pragma('wal_checkpoint(TRUNCATE)');
} catch (e) { /* ignore */ }
const { sessionsPath } = getPaths();
if (fs.existsSync(sessionsPath)) {
try {
const Database = require('better-sqlite3');
const sdb = new Database(sessionsPath);
sdb.pragma('wal_checkpoint(TRUNCATE)');
sdb.close();
} catch (e) { /* ignore */ }
}
}
function collectStats() {
const db = getDb();
const tables = ['clients', 'engagements', 'racks', 'rack_devices', 'data_diagrams', 'projects', 'invoices', 'assessments', 'users'];
const counts = {};
for (const t of tables) {
try {
counts[t] = db.prepare(`SELECT COUNT(*) as c FROM ${t}`).get().c;
} catch (e) {
counts[t] = 0;
}
}
const { dbPath, sessionsPath, uploadsDir, dataDir } = getPaths();
let uploadsBytes = 0;
let uploadFiles = 0;
if (fs.existsSync(uploadsDir)) {
const walk = (dir) => {
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name);
const st = fs.statSync(p);
if (st.isDirectory()) walk(p);
else { uploadsBytes += st.size; uploadFiles++; }
}
};
walk(uploadsDir);
}
return {
counts,
dbSizeBytes: fs.existsSync(dbPath) ? fs.statSync(dbPath).size : 0,
sessionsSizeBytes: fs.existsSync(sessionsPath) ? fs.statSync(sessionsPath).size : 0,
uploadsBytes,
uploadFiles,
dataDir
};
}
function formatBytes(n) {
if (n < 1024) return n + ' B';
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB';
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB';
}
function timestampLabel() {
const d = new Date();
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}
function safeBackupName(name) {
return /^mek-tech-backup-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.tar\.gz$/.test(name);
}
function pruneOldBackups() {
const list = listBackups();
if (list.length <= MAX_STORED_BACKUPS) return;
const toRemove = list.slice(MAX_STORED_BACKUPS);
for (const b of toRemove) {
try { fs.unlinkSync(b.path); } catch (e) { /* ignore */ }
}
}
function createBackup() {
ensureBackupsDir();
checkpointDatabases();
const paths = getPaths();
const stats = collectStats();
const label = timestampLabel();
const bundleName = `mek-tech-backup-${label}`;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mek-backup-'));
const bundleDir = path.join(tmpDir, bundleName);
try {
fs.mkdirSync(bundleDir, { recursive: true });
const manifest = {
app: 'mek-tech-consulting-framework',
version: APP_VERSION,
created_at: new Date().toISOString(),
stats: stats.counts,
includes: []
};
if (fs.existsSync(paths.dbPath)) {
fs.copyFileSync(paths.dbPath, path.join(bundleDir, 'consulting.db'));
manifest.includes.push('consulting.db');
}
if (fs.existsSync(paths.sessionsPath)) {
fs.copyFileSync(paths.sessionsPath, path.join(bundleDir, 'sessions.db'));
manifest.includes.push('sessions.db');
}
if (fs.existsSync(paths.uploadsDir)) {
copyDirSync(paths.uploadsDir, path.join(bundleDir, 'uploads'));
manifest.includes.push('uploads/');
}
fs.writeFileSync(path.join(bundleDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
const archiveName = `${bundleName}.tar.gz`;
const archivePath = path.join(paths.backupsDir, archiveName);
execSync(`tar -czf "${archivePath}" -C "${tmpDir}" "${bundleName}"`, { stdio: 'pipe' });
const size = fs.statSync(archivePath).size;
pruneOldBackups();
return {
filename: archiveName,
path: archivePath,
size,
sizeLabel: formatBytes(size),
created_at: manifest.created_at,
stats: stats.counts
};
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
function copyDirSync(src, dest) {
fs.mkdirSync(dest, { recursive: true });
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name);
const d = path.join(dest, name);
if (fs.statSync(s).isDirectory()) copyDirSync(s, d);
else fs.copyFileSync(s, d);
}
}
function listBackups() {
ensureBackupsDir();
const { backupsDir } = getPaths();
if (!fs.existsSync(backupsDir)) return [];
return fs.readdirSync(backupsDir)
.filter(f => f.endsWith('.tar.gz') && safeBackupName(f))
.map(f => {
const p = path.join(backupsDir, f);
const st = fs.statSync(p);
return {
filename: f,
path: p,
size: st.size,
sizeLabel: formatBytes(st.size),
created_at: st.mtime.toISOString()
};
})
.sort((a, b) => b.created_at.localeCompare(a.created_at));
}
function getBackupPath(filename) {
if (!safeBackupName(filename)) return null;
const p = path.join(getPaths().backupsDir, filename);
return fs.existsSync(p) ? p : null;
}
function deleteBackup(filename) {
const p = getBackupPath(filename);
if (!p) return false;
fs.unlinkSync(p);
return true;
}
function validateArchive(archivePath) {
if (!fs.existsSync(archivePath)) throw new Error('Backupbestand niet gevonden');
const listing = execSync(`tar -tzf "${archivePath}"`, { encoding: 'utf8' });
if (!listing.includes('manifest.json')) throw new Error('Ongeldig backup: manifest.json ontbreekt');
if (!listing.includes('consulting.db')) throw new Error('Ongeldig backup: consulting.db ontbreekt');
return listing;
}
function restoreBackup(archivePath) {
validateArchive(archivePath);
const auto = createBackup();
const paths = getPaths();
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mek-restore-'));
try {
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' });
const entries = fs.readdirSync(tmpDir);
const bundleDir = entries.length === 1 ? path.join(tmpDir, entries[0]) : tmpDir;
const manifestPath = path.join(bundleDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) throw new Error('manifest.json niet gevonden in backup');
checkpointDatabases();
const dbDest = paths.dbPath;
const dbSrc = path.join(bundleDir, 'consulting.db');
if (fs.existsSync(dbSrc)) {
for (const ext of ['-wal', '-shm']) {
const sidecar = dbDest + ext;
if (fs.existsSync(sidecar)) fs.unlinkSync(sidecar);
}
fs.copyFileSync(dbSrc, dbDest);
}
const sessSrc = path.join(bundleDir, 'sessions.db');
if (fs.existsSync(sessSrc)) {
for (const ext of ['-wal', '-shm']) {
const sidecar = paths.sessionsPath + ext;
if (fs.existsSync(sidecar)) fs.unlinkSync(sidecar);
}
fs.copyFileSync(sessSrc, paths.sessionsPath);
}
const uploadsSrc = path.join(bundleDir, 'uploads');
if (fs.existsSync(uploadsSrc)) {
if (fs.existsSync(paths.uploadsDir)) fs.rmSync(paths.uploadsDir, { recursive: true, force: true });
copyDirSync(uploadsSrc, paths.uploadsDir);
}
return {
success: true,
preRestoreBackup: auto.filename,
restored_at: new Date().toISOString()
};
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
module.exports = {
getPaths,
collectStats,
formatBytes,
createBackup,
listBackups,
getBackupPath,
deleteBackup,
restoreBackup,
safeBackupName,
APP_VERSION
};
+90
View File
@@ -0,0 +1,90 @@
/** Mek-Tech Consultancy Hub — toolkits, playbooks & delivery frameworks */
const TOOLKIT = [
{
id: 'discovery',
title: 'Discovery & Assessment',
desc: 'Intake, maturity scans & stakeholder alignment',
color: '#00d4ff',
tools: [
{ icon: 'brain', label: 'Enterprise Framework', href: '/consulting', desc: '5-staps solution architect assessment + roadmap' },
{ icon: 'clipboard-list', label: 'Quick Assessment', href: '/assess', desc: 'Snelle infra/data/AI intake per cliënt' },
{ icon: 'search', label: 'LeadGen AI Scan', href: '/?tab=leadgen', desc: 'Website tech-stack & pain point analyse' },
{ icon: 'building-2', label: 'Market Intel', href: '/market', desc: 'Prospects, datacenters & sector mapping' },
{ icon: 'users', label: 'Cliënt 360°', href: '/clients', desc: 'Volledig klantbeeld & engagement history' },
]
},
{
id: 'architecture',
title: 'Architectuur & Design',
desc: 'Data, systeem & netwerk ontwerp',
color: '#a855f7',
tools: [
{ icon: 'layers', label: 'Architectuur Designer', href: '/architecture', desc: 'Visueel data-architectuur canvas' },
{ icon: 'pen-tool', label: 'Pro Designer', href: '/architecture/designer', desc: 'Geavanceerde Python design editor' },
{ icon: 'server', label: 'Rack & DC Design', href: '/racks', desc: 'Server racks, PDU, koeling & patching' },
{ icon: 'wifi', label: 'Netwerk Topologie', href: '/networking', desc: 'Switches, firewalls & verbindingen' },
{ icon: 'calculator', label: 'Sizing Calculator', href: '/sizing', desc: 'Capaciteit & hardware sizing' },
]
},
{
id: 'data-ai',
title: 'Data, AI & Kwaliteit',
desc: 'Profiling, governance & AI readiness',
color: '#00ff88',
tools: [
{ icon: 'shield-check', label: 'Data Quality', href: '/quality', desc: 'Profiling, validatie & kwaliteitsrapporten' },
{ icon: 'bot', label: 'AI Providers', href: '/ai', desc: 'Multi-provider LLM configuratie' },
{ icon: 'database', label: 'Data Modellen', href: '/quality', desc: 'SQL schema & dbt model editor' },
{ icon: 'file-bar-chart', label: 'Rapporten', href: '/reports', desc: 'Consultancy deliverables exporteren' },
{ icon: 'plug', label: 'Integraties', href: '/integrations', desc: 'Externe systemen koppelen' },
]
},
{
id: 'delivery',
title: 'Delivery & Operations',
desc: 'Projectuitvoering, taken & tijd',
color: '#ffb347',
tools: [
{ icon: 'briefcase', label: 'Engagements', href: '/engagements', desc: 'Projecten per fase beheren' },
{ icon: 'check-square', label: 'Taken & Deadlines', href: '/?tab=tasks', desc: 'Werkpakketten & prioriteiten' },
{ icon: 'clock', label: 'Urenregistratie', href: '/time', desc: 'Billable hours & project tracking' },
{ icon: 'folder-kanban', label: 'Projecten', href: '/projects', desc: 'Portfolio & milestone overzicht' },
{ icon: 'calendar', label: 'Planning', href: '/calendar', desc: 'Afspraken, reviews & deadlines' },
]
},
{
id: 'commercial',
title: 'Commercial & Relatie',
desc: 'Offertes, facturatie & communicatie',
color: '#ff6b8a',
tools: [
{ icon: 'receipt', label: 'Facturatie', href: '/invoices', desc: 'Facturen & betalingsstatus' },
{ icon: 'bar-chart-2', label: 'Finance Dashboard', href: '/finance', desc: 'Omzet, marge & outstanding' },
{ icon: 'mail', label: 'Email Hub', href: '/email', desc: 'Klantcommunicatie & follow-ups' },
{ icon: 'bell', label: 'Notificaties', href: '/notifications', desc: 'Alerts & follow-up reminders' },
{ icon: 'file-text', label: 'Audit Trail', href: '/audit', desc: 'Alle consultancy acties gelogd' },
]
}
];
const PLAYBOOKS = [
{ phase: '1', title: 'Intake & Discovery', steps: ['Kick-off meeting', 'Stakeholder mapping', 'Enterprise assessment', 'Pain points documenteren'], duration: '1-2 weken', color: '#00d4ff' },
{ phase: '2', title: 'Analyse & Design', steps: ['As-is architectuur', 'Gap analyse', 'To-be design', 'Sizing & kostenraming'], duration: '2-4 weken', color: '#a855f7' },
{ phase: '3', title: 'Validatie & Proposal', steps: ['Design review', 'POC / pilot scope', 'Offerte & SOW', 'Go/no-go beslissing'], duration: '1-2 weken', color: '#00ff88' },
{ phase: '4', title: 'Implementatie', steps: ['Projectplan & sprints', 'Infra provisioning', 'Data pipelines', 'Test & acceptatie'], duration: '4-12 weken', color: '#ffb347' },
{ phase: '5', title: 'Run & Optimize', steps: ['Monitoring opzetten', 'Knowledge transfer', 'Retainer / SLA', 'Continuous improvement'], duration: 'Doorlopend', color: '#ff6b8a' },
];
const MATURITY_LAYERS = [
{ name: 'Data Foundation', items: ['Bronnen in kaart', 'Data governance', 'Quality profiling', 'Warehouse / Lakehouse'] },
{ name: 'Platform & Infra', items: ['Cloud / hybrid strategie', 'Netwerk & security', 'Compute sizing', 'Observability'] },
{ name: 'Analytics & AI', items: ['BI maturity', 'ML use cases', 'LLM / GenAI readiness', 'MLOps pipeline'] },
{ name: 'Organisatie', items: ['Team capabilities', 'Change management', 'Training plan', 'Operating model'] },
];
function getConsultingToolkit() {
return { toolkit: TOOLKIT, playbooks: PLAYBOOKS, maturityLayers: MATURITY_LAYERS };
}
module.exports = { getConsultingToolkit, TOOLKIT, PLAYBOOKS, MATURITY_LAYERS };
+50
View File
@@ -0,0 +1,50 @@
const { getDb } = require('../db');
function crmContextMiddleware(req, res, next) {
const db = getDb();
const qClientId = req.query.client_id || req.query.clientId;
if (qClientId && req.session) {
req.session.activeClientId = parseInt(qClientId, 10) || null;
}
const activeClientId = (req.session && req.session.activeClientId) || null;
let activeClient = null;
if (activeClientId) {
activeClient = db.prepare('SELECT id, name, status, industry FROM clients WHERE id = ?').get(activeClientId);
if (!activeClient) req.session.activeClientId = null;
}
const allClients = db.prepare('SELECT id, name, status FROM clients ORDER BY name').all();
res.locals.activeClient = activeClient;
res.locals.activeClientId = activeClient ? activeClient.id : null;
res.locals.allClients = allClients;
res.locals.clientQuery = (path) => {
if (!activeClient) return path;
const sep = path.includes('?') ? '&' : '?';
return `${path}${sep}client_id=${activeClient.id}`;
};
next();
}
function getClientHubLinks(clientId) {
const id = clientId;
const q = `?client_id=${id}`;
return [
{ href: `/clients/${id}`, icon: 'eye', label: 'Overzicht' },
{ href: `/engagements/new${q}`, icon: 'briefcase', label: 'Engagement' },
{ href: `/consulting${q}`, icon: 'clipboard-list', label: 'Framework' },
{ href: `/assess/${id}`, icon: 'file-search', label: 'Assessment' },
{ href: `/architecture/new/${id}`, icon: 'network', label: 'Design' },
{ href: `/architecture/designer${q}`, icon: 'pen-tool', label: 'Pro Designer' },
{ href: `/quality${q}`, icon: 'shield-check', label: 'Data Quality' },
{ href: `/racks${q}`, icon: 'server', label: 'Racks' },
{ href: `/networking${q}`, icon: 'share-2', label: 'Netwerk' },
{ href: `/time${q}`, icon: 'clock', label: 'Uren' },
{ href: `/invoices/new${q}`, icon: 'receipt', label: 'Factuur' },
{ href: `/email/compose${q}`, icon: 'mail', label: 'Email' }
];
}
module.exports = { crmContextMiddleware, getClientHubLinks };
+63
View File
@@ -0,0 +1,63 @@
const NAV_SECTIONS = [
{
label: 'CRM',
items: [
{ href: '/', icon: 'layout-dashboard', label: 'Dashboard', match: /^\/(\?.*)?$/ },
{ href: '/clients', icon: 'users', label: 'Cliënten', match: /^\/clients/ },
{ href: '/engagements', icon: 'briefcase', label: 'Engagements', match: /^\/engagements/ },
{ href: '/tasks', icon: 'check-circle', label: 'Taken', match: /^\/tasks/ },
{ href: '/calendar', icon: 'calendar', label: 'Agenda', match: /^\/calendar/ },
{ href: '/email', icon: 'mail', label: 'Email', match: /^\/email/ }
]
},
{
label: 'Delivery',
items: [
{ href: '/consulting', icon: 'clipboard-list', label: 'Framework', match: /^\/consulting/ },
{ href: '/assess', icon: 'file-search', label: 'Assessments', match: /^\/assess/ },
{ href: '/projects', icon: 'folder-kanban', label: 'Projecten', match: /^\/projects/ },
{ href: '/time', icon: 'clock', label: 'Uren', match: /^\/time/ },
{ href: '/invoices', icon: 'receipt', label: 'Facturen', match: /^\/invoices/ },
{ href: '/reports', icon: 'bar-chart-3', label: 'Rapporten', match: /^\/reports/ }
]
},
{
label: 'Data Engineering',
items: [
{ href: '/quality', icon: 'shield-check', label: 'Data Quality', match: /^\/quality/ },
{ href: '/architecture', icon: 'network', label: 'Architecture', match: /^\/architecture/ },
{ href: '/architecture/designer', icon: 'pen-tool', label: 'Pro Designer', match: /^\/architecture\/designer/ },
{ href: '/market', icon: 'radar', label: 'Market Intel', match: /^\/market/ },
{ href: '/sizing', icon: 'cpu', label: 'Sizing', match: /^\/sizing/ }
]
},
{
label: 'Infrastructure',
items: [
{ href: '/racks', icon: 'server', label: 'Racks', match: /^\/racks/ },
{ href: '/networking', icon: 'share-2', label: 'Netwerk', match: /^\/networking/ },
{ href: '/infrastructure', icon: 'building-2', label: 'Infra', match: /^\/infrastructure/ }
]
},
{
label: 'Platform',
items: [
{ href: '/finance', icon: 'wallet', label: 'Finance', match: /^\/finance/ },
{ href: '/ai', icon: 'sparkles', label: 'AI Providers', match: /^\/ai/ },
{ href: '/integrations', icon: 'plug', label: 'Integraties', match: /^\/integrations/ },
{ href: '/settings', icon: 'settings', label: 'Instellingen', match: /^\/settings/ }
]
}
];
function getNavSections(activePath) {
return NAV_SECTIONS.map(section => ({
...section,
items: section.items.map(item => ({
...item,
active: item.match.test(activePath || '/')
}))
}));
}
module.exports = { NAV_SECTIONS, getNavSections };
+124
View File
@@ -0,0 +1,124 @@
const DEVICE_TO_NODE = {
server: 'hw-dell-pe',
switch: 'net-catalyst',
storage: 'storage-netapp',
firewall: 'net-fortinet',
router: 'net-junper-srx',
loadbalancer: 'net-f5',
san: 'storage-pure',
pdu: 'infra-pdu',
ups: 'infra-apc-ups',
cooling: 'infra-cooling',
patch_panel: 'infra-patch-panel',
tape: 'storage-netapp',
kvm: 'infra-rack',
other: 'infra-rack'
};
const MANUFACTURER_NODE = {
hpe: 'hw-hpe-dl',
dell: 'hw-dell-pe',
lenovo: 'hw-lenovo-ts',
cisco: 'net-catalyst',
juniper: 'net-junper-ex',
arista: 'net-arista',
netapp: 'storage-netapp',
pure: 'storage-pure',
fortinet: 'net-fortinet',
f5: 'net-f5',
palo: 'net-paloalto',
nvidia: 'ai-nvidia',
apc: 'infra-apc-ups'
};
const DIAGRAM_TYPE_LABELS = {
data_architecture: 'Data Architectuur',
network_topology: 'Netwerk Topologie',
system_architecture: 'Systeem Architectuur',
application_flow: 'Applicatie Flow',
application_landscape: 'Applicatie Landschap'
};
function resolveNodeType(device) {
const m = (device.manufacturer || '').toLowerCase();
for (const [key, type] of Object.entries(MANUFACTURER_NODE)) {
if (m.includes(key)) return type;
}
return DEVICE_TO_NODE[device.device_type] || 'hw-dell-pe';
}
function rackDevicesToNodes(devices, startX = 40, startY = 40) {
return devices.map((d, i) => ({
id: 'rd-' + d.id,
type: resolveNodeType(d),
x: startX + (i % 4) * 180,
y: startY + Math.floor(i / 4) * 90,
label: d.name,
sub: (d.model || d.device_type || '').toString(),
css: 'node-hw',
w: 160,
h: 48,
ip: d.mgmt_ip || '',
vendor: d.manufacturer || '',
model: d.model || '',
specs: d.specs || '',
notes: d.notes || '',
rack_device_id: d.id,
layer: 'infra'
}));
}
function parseDiagramJson(diagram) {
let nodes = [];
let edges = [];
try {
nodes = JSON.parse(diagram.nodes || '[]');
edges = JSON.parse(diagram.edges || '[]');
} catch (e) { /* ignore */ }
if (!Array.isArray(nodes) && nodes && nodes.nodes) {
edges = nodes.edges || [];
nodes = nodes.nodes || [];
}
if (!Array.isArray(edges) && edges && edges.edges) {
nodes = edges.nodes || nodes;
edges = edges.edges || [];
}
if (!Array.isArray(nodes)) nodes = [];
if (!Array.isArray(edges)) edges = [];
return { nodes, edges };
}
function summarizeDiagram(diagram) {
const { nodes, edges } = parseDiagramJson(diagram);
const layers = {};
nodes.forEach(n => {
const layer = n.layer || (n.type || '').split('-')[0] || 'other';
layers[layer] = (layers[layer] || 0) + 1;
});
const protocols = {};
edges.forEach(e => {
const p = e.protocol || 'unspecified';
protocols[p] = (protocols[p] || 0) + 1;
});
return { nodes, edges, layers, protocols, nodeCount: nodes.length, edgeCount: edges.length };
}
function getLayerForCategory(catLabel) {
const c = (catLabel || '').toLowerCase();
if (c.includes('data bron') || c.includes('ingest') || c.includes('opslag') || c.includes('database')) return 'data';
if (c.includes('microservice') || c.includes('apps')) return 'app';
if (c.includes('api') || c.includes('messaging') || c.includes('queue')) return 'integration';
if (c.includes('netwerk') || c.includes('compute / server') || c.includes('infrastructure') || c.includes('security') || c.includes('observability') || c.includes('ci/cd')) return 'infra';
if (c.includes('ai')) return 'app';
return 'other';
}
module.exports = {
DEVICE_TO_NODE,
DIAGRAM_TYPE_LABELS,
resolveNodeType,
rackDevicesToNodes,
parseDiagramJson,
summarizeDiagram,
getLayerForCategory
};
+29
View File
@@ -0,0 +1,29 @@
const http = require('http');
function checkService(host, port, path, timeout = 3000) {
return new Promise((resolve) => {
const req = http.request({ hostname: host, port, path, method: 'GET', timeout }, (res) => {
resolve({ online: res.statusCode < 500, status: res.statusCode });
});
req.on('error', () => resolve({ online: false, status: 0 }));
req.on('timeout', () => { req.destroy(); resolve({ online: false, status: 0 }); });
req.end();
});
}
async function getServicesStatus() {
const qsHost = process.env.QS_HOST || 'python-quality';
const [quality, designer, leadgen] = await Promise.all([
checkService(qsHost, 3002, '/python-quality/'),
checkService('python-designer', 3001, '/python-editor/'),
checkService('python-leadgen', 3003, '/python-leadgen/health')
]);
return {
consulting: { online: true, label: 'Mek-Tech Core', port: 3000 },
quality: { ...quality, label: 'Data Quality', port: 3002 },
designer: { ...designer, label: 'Pro Designer', port: 3001 },
leadgen: { ...leadgen, label: 'Lead Generation', port: 3003 }
};
}
module.exports = { checkService, getServicesStatus };