new features
This commit is contained in:
27
data-service/Dockerfile
Normal file
27
data-service/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Python and pip for running traceability script
|
||||
RUN apk add --no-cache python3 py3-pip
|
||||
|
||||
# Install Python dependencies for the traceability script
|
||||
RUN pip3 install --break-system-packages requests pandas
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install Node.js dependencies
|
||||
RUN npm install --production
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /data /scripts /srv/data
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3002
|
||||
|
||||
# Start the service
|
||||
CMD ["npm", "start"]
|
||||
744
data-service/index.js
Normal file
744
data-service/index.js
Normal file
@@ -0,0 +1,744 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs').promises;
|
||||
const fsSync = require('fs');
|
||||
const path = require('path');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3002;
|
||||
const DATA_DIR = process.env.DATA_DIR || '/data';
|
||||
const SCRIPTS_DIR = process.env.SCRIPTS_DIR || '/scripts';
|
||||
const PUBLIC_DIR = process.env.PUBLIC_DIR || '/srv/data';
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// Helper functions
|
||||
const getFilePath = (collection) => path.join(DATA_DIR, `${collection}.json`);
|
||||
|
||||
const readCollection = async (collection) => {
|
||||
const filePath = getFilePath(collection);
|
||||
try {
|
||||
const data = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const writeCollection = async (collection, data) => {
|
||||
const filePath = getFilePath(collection);
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
};
|
||||
|
||||
// Simple hash function for passwords
|
||||
const simpleHash = (str) => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return hash.toString(36);
|
||||
};
|
||||
|
||||
// Initialize default data
|
||||
const initializeDefaults = async () => {
|
||||
// Initialize auth data with default admin
|
||||
const authData = await readCollection('auth');
|
||||
if (!authData) {
|
||||
const defaultAuth = {
|
||||
users: [
|
||||
{
|
||||
id: 'admin-1',
|
||||
username: 'admin',
|
||||
email: 'support@nabd-co.com',
|
||||
password: simpleHash('admin123'),
|
||||
role: 'admin',
|
||||
status: 'approved',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
await writeCollection('auth', defaultAuth);
|
||||
console.log('Initialized default auth data');
|
||||
}
|
||||
|
||||
// Initialize budget data
|
||||
const budgetData = await readCollection('budget');
|
||||
if (!budgetData) {
|
||||
const defaultBudget = {
|
||||
transactions: [],
|
||||
founders: [],
|
||||
settings: {
|
||||
currency: 'USD',
|
||||
fiscalYearStart: 1,
|
||||
runwayWarningMonths: 6,
|
||||
},
|
||||
targets: [],
|
||||
recurringTransactions: [],
|
||||
};
|
||||
await writeCollection('budget', defaultBudget);
|
||||
console.log('Initialized default budget data');
|
||||
}
|
||||
|
||||
// Initialize planning data
|
||||
const planningData = await readCollection('planning');
|
||||
if (!planningData) {
|
||||
const defaultPlanning = {
|
||||
sprints: [],
|
||||
tasks: [],
|
||||
comments: [],
|
||||
timeLogs: [],
|
||||
};
|
||||
await writeCollection('planning', defaultPlanning);
|
||||
console.log('Initialized default planning data');
|
||||
}
|
||||
|
||||
// Initialize documentation data
|
||||
const docsData = await readCollection('documentation');
|
||||
if (!docsData) {
|
||||
const defaultDocs = {
|
||||
documents: [],
|
||||
categories: ['Architecture', 'API', 'User Guide', 'Development'],
|
||||
};
|
||||
await writeCollection('documentation', defaultDocs);
|
||||
console.log('Initialized default documentation data');
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== GENERIC CRUD ROUTES ====================
|
||||
|
||||
// Get entire collection
|
||||
app.get('/api/:collection', async (req, res) => {
|
||||
try {
|
||||
const { collection } = req.params;
|
||||
const data = await readCollection(collection);
|
||||
res.json(data || {});
|
||||
} catch (error) {
|
||||
console.error(`Error reading ${req.params.collection}:`, error);
|
||||
res.status(500).json({ error: 'Failed to read data' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update entire collection
|
||||
app.put('/api/:collection', async (req, res) => {
|
||||
try {
|
||||
const { collection } = req.params;
|
||||
await writeCollection(collection, req.body);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`Error writing ${req.params.collection}:`, error);
|
||||
res.status(500).json({ error: 'Failed to write data' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== AUTH ROUTES ====================
|
||||
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
const authData = await readCollection('auth');
|
||||
const hashedPassword = simpleHash(password);
|
||||
|
||||
const user = authData.users.find(
|
||||
u => u.username === username && u.password === hashedPassword
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return res.json({ success: false, message: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
if (user.status === 'pending') {
|
||||
return res.json({ success: false, message: 'Your account is pending approval.' });
|
||||
}
|
||||
|
||||
if (user.status === 'rejected') {
|
||||
return res.json({ success: false, message: 'Your account was not approved.' });
|
||||
}
|
||||
|
||||
// Remove password from response
|
||||
const { password: _, ...safeUser } = user;
|
||||
res.json({ success: true, message: 'Login successful', user: safeUser });
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/register', async (req, res) => {
|
||||
try {
|
||||
const { username, email, password } = req.body;
|
||||
const authData = await readCollection('auth');
|
||||
|
||||
if (authData.users.some(u => u.username === username)) {
|
||||
return res.json({ success: false, message: 'Username already taken' });
|
||||
}
|
||||
|
||||
if (authData.users.some(u => u.email === email)) {
|
||||
return res.json({ success: false, message: 'Email already registered' });
|
||||
}
|
||||
|
||||
const newUser = {
|
||||
id: uuidv4(),
|
||||
username,
|
||||
email,
|
||||
password: simpleHash(password),
|
||||
role: 'member',
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
authData.users.push(newUser);
|
||||
await writeCollection('auth', authData);
|
||||
|
||||
res.json({ success: true, message: 'Registration successful! Please wait for admin approval.' });
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/auth/users', async (req, res) => {
|
||||
try {
|
||||
const authData = await readCollection('auth');
|
||||
// Remove passwords from response
|
||||
const safeUsers = authData.users.map(({ password, ...user }) => user);
|
||||
res.json(safeUsers);
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
res.status(500).json({ error: 'Failed to get users' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/auth/users/:userId', async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const updates = req.body;
|
||||
const authData = await readCollection('auth');
|
||||
|
||||
const index = authData.users.findIndex(u => u.id === userId);
|
||||
if (index === -1) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// Hash password if being updated
|
||||
if (updates.password) {
|
||||
updates.password = simpleHash(updates.password);
|
||||
}
|
||||
|
||||
authData.users[index] = {
|
||||
...authData.users[index],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await writeCollection('auth', authData);
|
||||
|
||||
const { password, ...safeUser } = authData.users[index];
|
||||
res.json(safeUser);
|
||||
} catch (error) {
|
||||
console.error('Update user error:', error);
|
||||
res.status(500).json({ error: 'Failed to update user' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/auth/users/:userId', async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const authData = await readCollection('auth');
|
||||
|
||||
const user = authData.users.find(u => u.id === userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// Prevent deleting last admin
|
||||
const admins = authData.users.filter(u => u.role === 'admin' && u.status === 'approved');
|
||||
if (user.role === 'admin' && admins.length === 1) {
|
||||
return res.status(400).json({ error: 'Cannot delete the last admin' });
|
||||
}
|
||||
|
||||
authData.users = authData.users.filter(u => u.id !== userId);
|
||||
await writeCollection('auth', authData);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete user error:', error);
|
||||
res.status(500).json({ error: 'Failed to delete user' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/users', async (req, res) => {
|
||||
try {
|
||||
const { username, email, password, role } = req.body;
|
||||
const authData = await readCollection('auth');
|
||||
|
||||
if (authData.users.some(u => u.username === username)) {
|
||||
return res.status(400).json({ error: 'Username already taken' });
|
||||
}
|
||||
|
||||
if (authData.users.some(u => u.email === email)) {
|
||||
return res.status(400).json({ error: 'Email already registered' });
|
||||
}
|
||||
|
||||
const newUser = {
|
||||
id: uuidv4(),
|
||||
username,
|
||||
email,
|
||||
password: simpleHash(password),
|
||||
role: role || 'member',
|
||||
status: 'approved',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
authData.users.push(newUser);
|
||||
await writeCollection('auth', authData);
|
||||
|
||||
const { password: _, ...safeUser } = newUser;
|
||||
res.json(safeUser);
|
||||
} catch (error) {
|
||||
console.error('Add user error:', error);
|
||||
res.status(500).json({ error: 'Failed to add user' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== BUDGET ROUTES ====================
|
||||
|
||||
app.get('/api/budget/transactions', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
res.json(budgetData?.transactions || []);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to get transactions' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/budget/transactions', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
const newTransaction = {
|
||||
...req.body,
|
||||
id: uuidv4(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
budgetData.transactions.push(newTransaction);
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(newTransaction);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to add transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/budget/transactions/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const budgetData = await readCollection('budget');
|
||||
const index = budgetData.transactions.findIndex(t => t.id === id);
|
||||
if (index === -1) {
|
||||
return res.status(404).json({ error: 'Transaction not found' });
|
||||
}
|
||||
budgetData.transactions[index] = { ...budgetData.transactions[index], ...req.body };
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(budgetData.transactions[index]);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/budget/transactions/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const budgetData = await readCollection('budget');
|
||||
budgetData.transactions = budgetData.transactions.filter(t => t.id !== id);
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
// Founders
|
||||
app.get('/api/budget/founders', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
res.json(budgetData?.founders || []);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to get founders' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/budget/founders', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
const newFounder = { ...req.body, id: uuidv4() };
|
||||
budgetData.founders.push(newFounder);
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(newFounder);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to add founder' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/budget/founders/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const budgetData = await readCollection('budget');
|
||||
const index = budgetData.founders.findIndex(f => f.id === id);
|
||||
if (index === -1) {
|
||||
return res.status(404).json({ error: 'Founder not found' });
|
||||
}
|
||||
budgetData.founders[index] = { ...budgetData.founders[index], ...req.body };
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(budgetData.founders[index]);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update founder' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/budget/founders/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const budgetData = await readCollection('budget');
|
||||
budgetData.founders = budgetData.founders.filter(f => f.id !== id);
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete founder' });
|
||||
}
|
||||
});
|
||||
|
||||
// Settings
|
||||
app.get('/api/budget/settings', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
res.json(budgetData?.settings || { currency: 'USD', fiscalYearStart: 1, runwayWarningMonths: 6 });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to get settings' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/budget/settings', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
budgetData.settings = { ...budgetData.settings, ...req.body };
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(budgetData.settings);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// Targets
|
||||
app.get('/api/budget/targets', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
res.json(budgetData?.targets || []);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to get targets' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/budget/targets', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
budgetData.targets = req.body;
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(budgetData.targets);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update targets' });
|
||||
}
|
||||
});
|
||||
|
||||
// Recurring Transactions
|
||||
app.get('/api/budget/recurring', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
res.json(budgetData?.recurringTransactions || []);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to get recurring transactions' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/budget/recurring', async (req, res) => {
|
||||
try {
|
||||
const budgetData = await readCollection('budget');
|
||||
const newRecurring = { ...req.body, id: uuidv4() };
|
||||
budgetData.recurringTransactions = budgetData.recurringTransactions || [];
|
||||
budgetData.recurringTransactions.push(newRecurring);
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(newRecurring);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to add recurring transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/budget/recurring/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const budgetData = await readCollection('budget');
|
||||
const index = budgetData.recurringTransactions.findIndex(r => r.id === id);
|
||||
if (index === -1) {
|
||||
return res.status(404).json({ error: 'Recurring transaction not found' });
|
||||
}
|
||||
budgetData.recurringTransactions[index] = { ...budgetData.recurringTransactions[index], ...req.body };
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json(budgetData.recurringTransactions[index]);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update recurring transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/budget/recurring/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const budgetData = await readCollection('budget');
|
||||
budgetData.recurringTransactions = budgetData.recurringTransactions.filter(r => r.id !== id);
|
||||
await writeCollection('budget', budgetData);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete recurring transaction' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== TRACEABILITY ROUTES ====================
|
||||
|
||||
// Run Python script to fetch and export traceability data
|
||||
app.post('/api/traceability/sync', async (req, res) => {
|
||||
console.log('[Traceability] Sync requested...');
|
||||
|
||||
const scriptPath = path.join(SCRIPTS_DIR, 'get_traceability.py');
|
||||
const outputPath = path.join(PUBLIC_DIR, 'traceability_export.csv');
|
||||
|
||||
// Check if script exists
|
||||
try {
|
||||
await fs.access(scriptPath);
|
||||
} catch {
|
||||
console.log('[Traceability] Script not found at:', scriptPath);
|
||||
return res.status(404).json({
|
||||
error: 'Python script not found',
|
||||
details: `Expected at: ${scriptPath}`,
|
||||
hint: 'Ensure get_traceability.py is mounted to /scripts in the container'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Run the Python script
|
||||
const pythonProcess = spawn('python3', [scriptPath], {
|
||||
cwd: PUBLIC_DIR,
|
||||
env: { ...process.env }
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
console.log('[Python]', data.toString().trim());
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
console.error('[Python Error]', data.toString().trim());
|
||||
});
|
||||
|
||||
pythonProcess.on('close', async (code) => {
|
||||
if (code !== 0) {
|
||||
return res.status(500).json({
|
||||
error: 'Script execution failed',
|
||||
code,
|
||||
stdout,
|
||||
stderr
|
||||
});
|
||||
}
|
||||
|
||||
// Read the generated CSV
|
||||
try {
|
||||
const csvData = await fs.readFile(outputPath, 'utf8');
|
||||
console.log(`[Traceability] CSV generated: ${csvData.length} bytes`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Traceability data synced successfully',
|
||||
csvLength: csvData.length,
|
||||
stdout
|
||||
});
|
||||
} catch (readErr) {
|
||||
res.status(500).json({
|
||||
error: 'Failed to read generated CSV',
|
||||
details: readErr.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Traceability] Sync error:', error);
|
||||
res.status(500).json({ error: 'Failed to run sync', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get traceability data (returns CSV content)
|
||||
app.get('/api/traceability', async (req, res) => {
|
||||
const csvPath = path.join(PUBLIC_DIR, 'traceability_export.csv');
|
||||
|
||||
try {
|
||||
const csvData = await fs.readFile(csvPath, 'utf8');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.send(csvData);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).json({
|
||||
error: 'Traceability data not found',
|
||||
hint: 'Run POST /api/traceability/sync first or upload CSV manually'
|
||||
});
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to read traceability data' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get traceability data as JSON (parsed)
|
||||
app.get('/api/traceability/json', async (req, res) => {
|
||||
const csvPath = path.join(PUBLIC_DIR, 'traceability_export.csv');
|
||||
|
||||
try {
|
||||
const csvData = await fs.readFile(csvPath, 'utf8');
|
||||
const workPackages = parseCSVToWorkPackages(csvData);
|
||||
res.json({ workPackages, count: workPackages.length });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).json({ error: 'Traceability data not found' });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to read traceability data' });
|
||||
}
|
||||
});
|
||||
|
||||
// Upload CSV directly
|
||||
app.post('/api/traceability/upload', async (req, res) => {
|
||||
const { csvContent } = req.body;
|
||||
|
||||
if (!csvContent) {
|
||||
return res.status(400).json({ error: 'csvContent is required' });
|
||||
}
|
||||
|
||||
const csvPath = path.join(PUBLIC_DIR, 'traceability_export.csv');
|
||||
|
||||
try {
|
||||
await fs.mkdir(PUBLIC_DIR, { recursive: true });
|
||||
await fs.writeFile(csvPath, csvContent, 'utf8');
|
||||
|
||||
const workPackages = parseCSVToWorkPackages(csvContent);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'CSV uploaded successfully',
|
||||
workPackages: workPackages.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to save CSV', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: Parse CSV to work packages
|
||||
function parseCSVToWorkPackages(csvText) {
|
||||
const cleanText = csvText.replace(/^\uFEFF/, '');
|
||||
const workPackages = [];
|
||||
const lines = cleanText.split('\n');
|
||||
|
||||
let currentRow = [];
|
||||
let inQuotedField = false;
|
||||
let currentField = '';
|
||||
|
||||
const content = lines.slice(1).join('\n');
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content[i];
|
||||
const nextChar = content[i + 1];
|
||||
|
||||
if (inQuotedField) {
|
||||
if (char === '"') {
|
||||
if (nextChar === '"') {
|
||||
currentField += '"';
|
||||
i++;
|
||||
} else {
|
||||
inQuotedField = false;
|
||||
}
|
||||
} else {
|
||||
currentField += char;
|
||||
}
|
||||
} else {
|
||||
if (char === '"' && currentField === '') {
|
||||
inQuotedField = true;
|
||||
} else if (char === ',') {
|
||||
currentRow.push(currentField);
|
||||
currentField = '';
|
||||
} else if (char === '\n') {
|
||||
currentRow.push(currentField);
|
||||
currentField = '';
|
||||
|
||||
if (currentRow.length >= 4 && /^\d+$/.test(currentRow[0]?.trim())) {
|
||||
const [id, type, status, title, description = '', parentId = '', relations = ''] = currentRow;
|
||||
workPackages.push({
|
||||
id: parseInt(id, 10),
|
||||
type: type?.toLowerCase().replace(/\s+/g, ' ').trim(),
|
||||
status: status || '',
|
||||
title: title || '',
|
||||
description: description || '',
|
||||
parentId: parentId?.trim() || '',
|
||||
relations: relations?.trim() || ''
|
||||
});
|
||||
}
|
||||
|
||||
currentRow = [];
|
||||
} else {
|
||||
currentField += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle last row
|
||||
if (currentField || currentRow.length > 0) {
|
||||
currentRow.push(currentField);
|
||||
if (currentRow.length >= 4 && /^\d+$/.test(currentRow[0]?.trim())) {
|
||||
const [id, type, status, title, description = '', parentId = '', relations = ''] = currentRow;
|
||||
workPackages.push({
|
||||
id: parseInt(id, 10),
|
||||
type: type?.toLowerCase().replace(/\s+/g, ' ').trim(),
|
||||
status: status || '',
|
||||
title: title || '',
|
||||
description: description || '',
|
||||
parentId: parentId?.trim() || '',
|
||||
relations: relations?.trim() || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return workPackages;
|
||||
}
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Start server
|
||||
initializeDefaults().then(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Data service running on port ${PORT}`);
|
||||
console.log(`Data directory: ${DATA_DIR}`);
|
||||
console.log(`Scripts directory: ${SCRIPTS_DIR}`);
|
||||
console.log(`Public directory: ${PUBLIC_DIR}`);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('Failed to initialize:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
14
data-service/package.json
Normal file
14
data-service/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "data-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Persistent data storage API for Traceability Matrix",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user