Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions backend/src/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,14 +222,25 @@ router.post('/logout',

/**
* GET /api/auth/session
* Get current session info
* Probe the current session state. Always responds with HTTP 200 — the
* absence of a session is a legitimate state, not an error. Clients
* (frontend AuthContext) read `authenticated` to decide whether to
* surface a logged-in UI or redirect to the login page.
*
* Returning 401 here would force the frontend to console.error on every
* unauthenticated page load, polluting logs and dev tools without
* carrying real information.
*/
router.get('/session',
validateSession,
asyncHandler(async (req, res) => {
const user = req.user;

res.json({
if (!req.session || !req.session.userId) {
return res.json({ authenticated: false, user: null });
}
const user = await User.findByPk(req.session.userId);
if (!user || !user.is_active) {
return res.json({ authenticated: false, user: null });
}
return res.json({
authenticated: true,
user: {
id: user.id,
Expand Down
18 changes: 14 additions & 4 deletions backend/src/routes/public.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,31 @@ router.post('/parse-dot',
label: node.attributes.label || node.id,
name: node.attributes.label || node.id,
size: node.attributes.val || node.attributes.size || '8',
color: node.attributes.color || '#1976D2',
// Pass color through only when the user actually specified it in DOT.
// The frontend applies a default (and role-based tint for DES graphs),
// so we must not paper over the user's intent here.
color: node.attributes.color,
geometry: node.attributes.geometry,
dimensions: node.attributes.dimensions,
particleGeneration: node.attributes.particleGeneration,
maxParticleProcessing: node.attributes.maxParticleProcessing,
image: node.attributes.image,
autoResize: node.attributes.autoResize,
bloomEffect: node.attributes.bloomEffect
bloomEffect: node.attributes.bloomEffect,
// DES attributes (ADR-006) — consumed by the in-browser ParticleSimulator
nodeRole: node.attributes.nodeRole,
dropPolicy: node.attributes.dropPolicy,
queue_size: node.attributes.queue_size,
processing_time: node.attributes.processing_time,
failure_rate: node.attributes.failure_rate
}));

const links = parseResult.ast.edges.map(edge => ({
source: edge.from,
target: edge.to,
label: edge.attributes.label || '',
color: edge.attributes.color || '#888',
// Same reasoning as for node.color: pass through only when set.
color: edge.attributes.color,
maxParticleFlow: edge.attributes.maxParticleFlow,
particleSpeed: edge.attributes.particleSpeed,
style: edge.attributes.style || 'solid'
Expand Down
70 changes: 68 additions & 2 deletions backend/src/utils/dotValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ class DotValidator {
// VortexFlow 3D extensions
'geometry', 'dimensions', 'particleGeneration', 'maxParticleProcessing',
'particleSpeed', 'maxParticleFlow', 'image', 'autoResize',
'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize'
'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize',
// DES (Discrete Event Simulation) attributes — ADR-006
'nodeRole', 'dropPolicy'
]);

// DES enums (ADR-006)
this.nodeRoles = new Set(['generator', 'relay', 'sink']);
this.dropPolicies = new Set(['tail', 'head', 'reject']);

this.nodeShapes = new Set([
// Standard DOT shapes
'box', 'circle', 'ellipse', 'point', 'egg', 'triangle',
Expand Down Expand Up @@ -118,6 +124,10 @@ class DotValidator {
result.metadata.hasVortexFlowExtensions = extensionResult.hasExtensions;
result.warnings.push(...extensionResult.warnings);

// DES coherence checks (ADR-006) — cross-attribute warnings on the parsed AST
const coherenceResult = this.validateDESCoherence(parseResult.ast);
result.warnings.push(...coherenceResult.warnings);

// Performance warnings
this.addPerformanceWarnings(result);

Expand Down Expand Up @@ -571,7 +581,9 @@ class DotValidator {
const vortexFlow3DAttrs = [
'geometry', 'dimensions', 'particleGeneration', 'maxParticleProcessing',
'particleSpeed', 'maxParticleFlow', 'image', 'autoResize',
'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize'
'bloomEffect', 'particlesEnabled', 'autoColors', 'defaultNodeSize',
// DES — ADR-006
'nodeRole', 'dropPolicy'
];

// Check legacy attributes
Expand Down Expand Up @@ -710,6 +722,60 @@ class DotValidator {
result.warnings.push(`Image path "${value}" may not be a valid image file`);
}
break;

case 'nodeRole':
if (!this.nodeRoles.has(value)) {
result.warnings.push(
`Invalid nodeRole: "${value}". Valid roles: ${Array.from(this.nodeRoles).join(', ')}`
);
}
break;

case 'dropPolicy':
if (!this.dropPolicies.has(value)) {
result.warnings.push(
`Invalid dropPolicy: "${value}". Valid policies: ${Array.from(this.dropPolicies).join(', ')}`
);
}
break;
}
}

return result;
}

/**
* Validate cross-attribute coherence for DES (ADR-006).
*
* These are warnings, not errors — the graph is still accepted, but
* the validator surfaces the inconsistency so the user understands why
* the runtime ignores a given attribute.
*/
validateDESCoherence(ast) {
const result = { warnings: [] };

for (const node of ast.nodes) {
const attrs = node.attributes || {};
const role = attrs.nodeRole;
const dropPolicy = attrs.dropPolicy;
const queueSize = attrs.queue_size;
const particleGeneration = attrs.particleGeneration;

// dropPolicy without queue_size: meaningless (queue is unbounded)
if (dropPolicy !== undefined && queueSize === undefined) {
result.warnings.push(
`Node "${node.id}": dropPolicy="${dropPolicy}" has no effect without queue_size — the queue is unbounded and never drops.`
);
}

// particleGeneration > 0 on a non-generator role: ignored at runtime
if (particleGeneration !== undefined && role !== undefined && role !== 'generator') {
const numGen = parseFloat(particleGeneration);
if (!Number.isNaN(numGen) && numGen > 0) {
result.warnings.push(
`Node "${node.id}": particleGeneration=${particleGeneration} is ignored because nodeRole="${role}" (only "generator" emits).`
);
}
}
}

Expand Down
Loading
Loading