livrare lot 2

This commit is contained in:
EVOTECH IT SRL 2026-07-10 03:39:53 -07:00
commit 8ecc78e729
763 changed files with 164593 additions and 0 deletions

View file

@ -0,0 +1,553 @@
/**
* INPUT PROFILES ROUTES pipeline definitions (CRUD + lifecycle).
*
* Un input profile este DEFINIȚIA DE PIPELINE a platformei: ce componente
* rulează, cu ce ponderi/roluri/praguri, per tip de input. Mounted at both
* /api/input-profiles and /api/pipelines (alias, see server.ts).
*
* GET / list all profiles with overrides
* GET /:code single profile with overrides
* PUT /:code update (snapshots previous state as a version)
* POST /:code/clone clone into a new (inactive) profile
* POST /:code/activate publish (is_active=true)
* POST /:code/deactivate unpublish (is_active=false)
* GET /:code/versions version history (snapshots)
* POST /:code/versions/:id/restore restore a snapshot (current state is versioned first)
* GET /:code/overrides override configs for profile
* PUT /:code/overrides update override configs
* GET/PUT /scoring-config/:component scoring config (component_config PG)
*/
import { Router, type Request, type Response } from 'express';
import { query } from '../config/database';
import { internalError } from '../config/error-response';
const router = Router();
// Identity for the version audit trail. The JWT signature was already
// verified by the global gate (config/jwt-verify.ts) — this only reads claims.
function changedBy(req: Request): string | null {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) return null;
const parts = auth.slice(7).split('.');
if (parts.length !== 3) return null;
try {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
return payload.email || payload.preferred_username || payload.sub || null;
} catch {
return null;
}
}
/** Snapshot the CURRENT state of a profile (+overrides) into the version table. */
async function snapshotProfile(code: string, user: string | null, note: string): Promise<number | null> {
const rows = await query('SELECT * FROM input_type_profile WHERE profile_code = $1', [code]);
if (rows.length === 0) return null;
const overrides = await query(
'SELECT * FROM profile_override_config WHERE profile_code = $1 ORDER BY override_code',
[code],
);
const result = await query(
`INSERT INTO input_type_profile_version (profile_code, version_no, snapshot, changed_by, change_note)
VALUES ($1::varchar,
COALESCE((SELECT MAX(version_no) FROM input_type_profile_version WHERE profile_code = $1::varchar), 0) + 1,
$2, $3, $4)
RETURNING version_no`,
[code, JSON.stringify({ ...rows[0], overrides }), user, note],
);
return result[0].version_no;
}
// Fields copied verbatim when importing a pipeline JSON (whitelist — ignore
// profile_id/created_date and any unknown keys so an export round-trips safely).
const IMPORTABLE_FIELDS = [
'profile_name', 'description',
'weight_techniques', 'weight_claims', 'weight_ai_tampered', 'weight_source',
'role_techniques', 'role_claims', 'role_ai_tampered', 'role_source',
'min_components', 'primary_components', 'required_any',
'missing_techniques', 'missing_claims', 'missing_ai_tampered', 'missing_source',
'override_cap', 'confidence_config', 'ai_disclosure_multipliers',
] as const;
// ============================================================================
// POST /import — Create a pipeline from an exported JSON definition
// ============================================================================
router.post('/import', async (req: Request, res: Response) => {
try {
const body = req.body || {};
// Accept either a raw profile object or { profile, overrides } (export shape).
const profile = body.profile || body;
const overrides = Array.isArray(body.overrides) ? body.overrides
: Array.isArray(profile.overrides) ? profile.overrides : [];
const code = body.new_code || profile.profile_code;
if (!code || !/^[a-z0-9_-]{2,50}$/.test(code)) {
return res.status(400).json({ success: false, error: 'profile_code / new_code required (lowercase, [a-z0-9_-], 2-50 chars)' });
}
const exists = await query('SELECT 1 FROM input_type_profile WHERE profile_code = $1', [code]);
if (exists.length > 0) {
return res.status(409).json({ success: false, error: `Profile '${code}' already exists (delete or pick a new_code)` });
}
// Weight sum guard (same rule as PUT) when all four are present.
const w = ['weight_techniques', 'weight_claims', 'weight_ai_tampered', 'weight_source'];
if (w.every(k => profile[k] != null)) {
const total = w.reduce((s, k) => s + Number(profile[k]), 0);
if (total !== 100) return res.status(400).json({ success: false, error: `Weights must sum to 100 (got ${total})` });
}
const cols = ['profile_code'];
const vals: any[] = [code];
for (const f of IMPORTABLE_FIELDS) {
if (profile[f] === undefined) continue;
cols.push(f);
vals.push(f.includes('config') || f.includes('multipliers')
? (typeof profile[f] === 'string' ? profile[f] : JSON.stringify(profile[f]))
: profile[f]);
}
cols.push('is_active'); vals.push(false); // imports land INACTIVE — activate explicitly
cols.push('created_date'); vals.push(new Date().toISOString().slice(0, 10));
const placeholders = vals.map((_, i) => `$${i + 1}`).join(', ');
const created = await query(
`INSERT INTO input_type_profile (${cols.join(', ')}) VALUES (${placeholders}) RETURNING *`,
vals,
);
// Import overrides too (best-effort: only rows whose override_code exists in schema).
for (const ov of overrides) {
if (!ov?.override_code) continue;
await query(
`INSERT INTO profile_override_config (profile_code, override_code, enabled, threshold, bonus_per_unit, bonus_fixed, max_bonus)
VALUES ($1,$2,$3,$4,$5,$6,$7)
ON CONFLICT DO NOTHING`,
[code, ov.override_code, ov.enabled ?? true, ov.threshold ?? null, ov.bonus_per_unit ?? null, ov.bonus_fixed ?? null, ov.max_bonus ?? null],
);
}
await snapshotProfile(code, changedBy(req), 'imported from JSON');
res.status(201).json({ success: true, data: created[0], message: `Pipeline '${code}' imported (inactive — activate to publish)` });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// GET / — List all profiles with override summary
// ============================================================================
router.get('/', async (_req: Request, res: Response) => {
try {
const profiles = await query(`
SELECT p.*,
(SELECT json_agg(json_build_object(
'override_code', o.override_code,
'enabled', o.enabled,
'threshold', o.threshold,
'bonus_per_unit', o.bonus_per_unit,
'bonus_fixed', o.bonus_fixed,
'max_bonus', o.max_bonus
) ORDER BY o.override_code)
FROM profile_override_config o WHERE o.profile_code = p.profile_code
) as overrides
FROM input_type_profile p
ORDER BY p.profile_id
`);
res.json({ success: true, data: profiles, count: profiles.length });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// GET /:code — Single profile with overrides
// ============================================================================
router.get('/:code', async (req: Request, res: Response) => {
try {
const { code } = req.params;
const profiles = await query(
'SELECT * FROM input_type_profile WHERE profile_code = $1',
[code]
);
if (profiles.length === 0) {
return res.status(404).json({ success: false, error: `Profile '${code}' not found` });
}
const overrides = await query(
'SELECT * FROM profile_override_config WHERE profile_code = $1 ORDER BY override_code',
[code]
);
res.json({ success: true, data: { ...profiles[0], overrides } });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// PUT /:code — Update profile weights, rules, confidence
// ============================================================================
router.put('/:code', async (req: Request, res: Response) => {
try {
const { code } = req.params;
const {
profile_name, description,
weight_techniques, weight_claims, weight_ai_tampered, weight_source,
role_techniques, role_claims, role_ai_tampered, role_source,
min_components, primary_components, required_any,
missing_techniques, missing_claims, missing_ai_tampered, missing_source,
override_cap, confidence_config, ai_disclosure_multipliers,
} = req.body;
// Validate weights sum to 100 if all provided
if (weight_techniques != null && weight_claims != null && weight_ai_tampered != null && weight_source != null) {
const total = weight_techniques + weight_claims + weight_ai_tampered + weight_source;
if (total !== 100) {
return res.status(400).json({ success: false, error: `Weights must sum to 100 (got ${total})` });
}
}
// Build SET clause dynamically (only update provided fields)
const updates: string[] = [];
const values: any[] = [];
let paramIdx = 1;
const addField = (field: string, value: any) => {
if (value !== undefined) {
updates.push(`${field} = $${paramIdx++}`);
values.push(field.includes('config') || field.includes('multipliers')
? (typeof value === 'string' ? value : JSON.stringify(value))
: value
);
}
};
addField('profile_name', profile_name);
addField('description', description);
addField('weight_techniques', weight_techniques);
addField('weight_claims', weight_claims);
addField('weight_ai_tampered', weight_ai_tampered);
addField('weight_source', weight_source);
addField('role_techniques', role_techniques);
addField('role_claims', role_claims);
addField('role_ai_tampered', role_ai_tampered);
addField('role_source', role_source);
addField('min_components', min_components);
addField('primary_components', primary_components);
addField('required_any', required_any);
addField('missing_techniques', missing_techniques);
addField('missing_claims', missing_claims);
addField('missing_ai_tampered', missing_ai_tampered);
addField('missing_source', missing_source);
addField('override_cap', override_cap);
addField('confidence_config', confidence_config);
addField('ai_disclosure_multipliers', ai_disclosure_multipliers);
if (updates.length === 0) {
return res.status(400).json({ success: false, error: 'No fields to update' });
}
updates.push(`updated_date = CURRENT_DATE`);
values.push(code);
// Version the previous state BEFORE mutating — GET /:code/versions shows
// the full change history; restore brings any snapshot back.
const versionNo = await snapshotProfile(code, changedBy(req), req.body.change_note || 'update');
if (versionNo === null) {
return res.status(404).json({ success: false, error: `Profile '${code}' not found` });
}
const result = await query(
`UPDATE input_type_profile SET ${updates.join(', ')} WHERE profile_code = $${paramIdx} RETURNING *`,
values
);
if (result.length === 0) {
return res.status(404).json({ success: false, error: `Profile '${code}' not found` });
}
res.json({ success: true, data: result[0], version_saved: versionNo, message: `Profile '${code}' updated` });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// POST /:code/clone — Clone a profile into a new (inactive) pipeline definition
// ============================================================================
router.post('/:code/clone', async (req: Request, res: Response) => {
try {
const { code } = req.params;
const { new_code, new_name } = req.body;
if (!new_code || !/^[a-z0-9_-]{2,50}$/.test(new_code)) {
return res.status(400).json({ success: false, error: 'new_code is required (lowercase, [a-z0-9_-], 2-50 chars)' });
}
const source = await query('SELECT * FROM input_type_profile WHERE profile_code = $1', [code]);
if (source.length === 0) {
return res.status(404).json({ success: false, error: `Profile '${code}' not found` });
}
const existing = await query('SELECT 1 FROM input_type_profile WHERE profile_code = $1', [new_code]);
if (existing.length > 0) {
return res.status(409).json({ success: false, error: `Profile '${new_code}' already exists` });
}
// Clone the profile row — new clones start UNPUBLISHED (is_active=false);
// activation is an explicit lifecycle step.
const cloned = await query(
`INSERT INTO input_type_profile (
profile_code, profile_name, description,
weight_techniques, weight_claims, weight_ai_tampered, weight_source,
role_techniques, role_claims, role_ai_tampered, role_source,
min_components, primary_components, required_any,
missing_techniques, missing_claims, missing_ai_tampered, missing_source,
override_cap, confidence_config, ai_disclosure_multipliers,
is_active, created_date, updated_date)
SELECT $2, $3, description,
weight_techniques, weight_claims, weight_ai_tampered, weight_source,
role_techniques, role_claims, role_ai_tampered, role_source,
min_components, primary_components, required_any,
missing_techniques, missing_claims, missing_ai_tampered, missing_source,
override_cap, confidence_config, ai_disclosure_multipliers,
false, CURRENT_DATE, CURRENT_DATE
FROM input_type_profile WHERE profile_code = $1
RETURNING *`,
[code, new_code, new_name || `${source[0].profile_name} (clone)`],
);
// Clone the override configs too
await query(
`INSERT INTO profile_override_config (profile_code, override_code, enabled, threshold, bonus_per_unit, bonus_fixed, max_bonus)
SELECT $2, override_code, enabled, threshold, bonus_per_unit, bonus_fixed, max_bonus
FROM profile_override_config WHERE profile_code = $1`,
[code, new_code],
);
await snapshotProfile(new_code, changedBy(req), `cloned from '${code}'`);
res.status(201).json({
success: true,
data: cloned[0],
message: `Profile '${new_code}' cloned from '${code}' (inactive — activate to publish)`,
});
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// POST /:code/activate | /:code/deactivate — publish / unpublish
// ============================================================================
for (const action of ['activate', 'deactivate'] as const) {
router.post(`/:code/${action}`, async (req: Request, res: Response) => {
try {
const { code } = req.params;
const result = await query(
'UPDATE input_type_profile SET is_active = $1, updated_date = CURRENT_DATE WHERE profile_code = $2 RETURNING profile_code, profile_name, is_active',
[action === 'activate', code],
);
if (result.length === 0) {
return res.status(404).json({ success: false, error: `Profile '${code}' not found` });
}
await snapshotProfile(code, changedBy(req), action);
res.json({ success: true, data: result[0], message: `Profile '${code}' ${action}d` });
} catch (error) {
internalError(res, error);
}
});
}
// ============================================================================
// GET /:code/versions — version history (snapshots, newest first)
// ============================================================================
router.get('/:code/versions', async (req: Request, res: Response) => {
try {
const versions = await query(
`SELECT version_id, version_no, changed_at, changed_by, change_note, snapshot
FROM input_type_profile_version
WHERE profile_code = $1
ORDER BY version_no DESC`,
[req.params.code],
);
res.json({ success: true, data: versions, count: versions.length });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// POST /:code/versions/:versionId/restore — roll back to a snapshot
// ============================================================================
router.post('/:code/versions/:versionId/restore', async (req: Request, res: Response) => {
try {
const { code, versionId } = req.params;
const versions = await query(
'SELECT snapshot, version_no FROM input_type_profile_version WHERE profile_code = $1 AND version_id = $2',
[code, versionId],
);
if (versions.length === 0) {
return res.status(404).json({ success: false, error: `Version ${versionId} not found for '${code}'` });
}
const snap = typeof versions[0].snapshot === 'string' ? JSON.parse(versions[0].snapshot) : versions[0].snapshot;
// Version current state first so restore itself is reversible.
await snapshotProfile(code, changedBy(req), `pre-restore of v${versions[0].version_no}`);
const result = await query(
`UPDATE input_type_profile SET
profile_name = $1, description = $2,
weight_techniques = $3, weight_claims = $4, weight_ai_tampered = $5, weight_source = $6,
role_techniques = $7, role_claims = $8, role_ai_tampered = $9, role_source = $10,
min_components = $11, primary_components = $12, required_any = $13,
missing_techniques = $14, missing_claims = $15, missing_ai_tampered = $16, missing_source = $17,
override_cap = $18, confidence_config = $19, ai_disclosure_multipliers = $20,
is_active = $21, updated_date = CURRENT_DATE
WHERE profile_code = $22 RETURNING *`,
[
snap.profile_name, snap.description,
snap.weight_techniques, snap.weight_claims, snap.weight_ai_tampered, snap.weight_source,
snap.role_techniques, snap.role_claims, snap.role_ai_tampered, snap.role_source,
snap.min_components, snap.primary_components, snap.required_any,
snap.missing_techniques, snap.missing_claims, snap.missing_ai_tampered, snap.missing_source,
snap.override_cap,
snap.confidence_config ? JSON.stringify(snap.confidence_config) : null,
snap.ai_disclosure_multipliers ? JSON.stringify(snap.ai_disclosure_multipliers) : null,
snap.is_active, code,
],
);
// Restore overrides from the snapshot as well
if (Array.isArray(snap.overrides)) {
for (const ov of snap.overrides) {
await query(
`UPDATE profile_override_config
SET enabled = $1, threshold = $2, bonus_per_unit = $3, bonus_fixed = $4, max_bonus = $5
WHERE profile_code = $6 AND override_code = $7`,
[ov.enabled, ov.threshold, ov.bonus_per_unit, ov.bonus_fixed, ov.max_bonus, code, ov.override_code],
);
}
}
res.json({
success: true,
data: result[0],
message: `Profile '${code}' restored to version ${versions[0].version_no}`,
});
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// GET /:code/overrides — Override configs for a profile
// ============================================================================
router.get('/:code/overrides', async (req: Request, res: Response) => {
try {
const overrides = await query(
'SELECT * FROM profile_override_config WHERE profile_code = $1 ORDER BY override_code',
[req.params.code]
);
res.json({ success: true, data: overrides, count: overrides.length });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// PUT /:code/overrides — Update override configs for a profile
// ============================================================================
router.put('/:code/overrides', async (req: Request, res: Response) => {
try {
const { code } = req.params;
const { overrides } = req.body;
if (!Array.isArray(overrides)) {
return res.status(400).json({ success: false, error: 'overrides must be an array' });
}
const results: any[] = [];
for (const ov of overrides) {
const result = await query(
`UPDATE profile_override_config
SET enabled = COALESCE($1, enabled),
threshold = COALESCE($2, threshold),
bonus_per_unit = COALESCE($3, bonus_per_unit),
bonus_fixed = COALESCE($4, bonus_fixed),
max_bonus = COALESCE($5, max_bonus)
WHERE profile_code = $6 AND override_code = $7
RETURNING *`,
[ov.enabled, ov.threshold, ov.bonus_per_unit, ov.bonus_fixed, ov.max_bonus, code, ov.override_code]
);
if (result.length > 0) results.push(result[0]);
}
res.json({ success: true, data: results, message: `Updated ${results.length} overrides for '${code}'` });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// GET /scoring-config/:component — Read scoring_config from component_config PG
// ============================================================================
router.get('/scoring-config/:component', async (req: Request, res: Response) => {
try {
const rows = await query(
'SELECT config_value FROM component_config WHERE component_code = $1 AND config_key = $2',
[req.params.component, 'scoring_config']
);
if (rows.length === 0) {
return res.status(404).json({ success: false, error: `No scoring_config for ${req.params.component}` });
}
const value = typeof rows[0].config_value === 'string'
? JSON.parse(rows[0].config_value)
: rows[0].config_value;
res.json({ success: true, data: value });
} catch (error) {
internalError(res, error);
}
});
// ============================================================================
// PUT /scoring-config/:component — Update scoring_config in component_config PG
// ============================================================================
router.put('/scoring-config/:component', async (req: Request, res: Response) => {
try {
const { component } = req.params;
const configValue = req.body;
if (!configValue || Object.keys(configValue).length === 0) {
return res.status(400).json({ success: false, error: 'Request body must contain the scoring config' });
}
const result = await query(
`UPDATE component_config SET config_value = $1 WHERE component_code = $2 AND config_key = 'scoring_config' RETURNING component_code, config_key`,
[JSON.stringify(configValue), component]
);
if (result.length === 0) {
return res.status(404).json({ success: false, error: `No scoring_config for ${component}` });
}
res.json({ success: true, message: `Scoring config updated for ${component}. Sync to Redis to apply.` });
} catch (error) {
internalError(res, error);
}
});
export default router;