feat(boc): Complete Business Operations Center v1.0

- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
This commit is contained in:
Bernt
2026-07-12 12:41:35 +00:00
parent 4789a7fb48
commit 58ca4e68db
26666 changed files with 575891 additions and 2074516 deletions
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env node
/**
* REXO Ollama Worker
* Picks tasks from PLAN.json, sends to Ollama, writes output files, marks done.
* No Claude — uses local Ollama (qwen2.5:7b by default).
*
* Usage: node ollama-worker.mjs [--model qwen2.5:7b] [--task T799]
*/
import { readFileSync, writeFileSync, mkdirSync, renameSync, appendFileSync, existsSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const PLAN_PATH = '/opt/amos/rexo-build/plan/PLAN.json';
const ARTIFACTS_PATH = '/opt/amos/rexo-build/artifacts';
const LOG_PATH = '/opt/amos/rexo-build/logs/ollama-worker.log';
const OLLAMA_URL = 'http://127.0.0.1:11434';
const DEFAULT_MODEL = 'qwen2.5:7b';
// Parse args
const args = process.argv.slice(2);
const modelArg = args[indexOf('--model', args) + 1] || DEFAULT_MODEL;
const taskArg = args[indexOf('--task', args) + 1] || null;
function indexOf(flag, arr) {
const i = arr.indexOf(flag);
return i === -1 ? -999 : i;
}
function log(msg) {
const line = `[${new Date().toISOString()}] ${msg}\n`;
process.stdout.write(line);
try { appendFileSync(LOG_PATH, line); } catch {}
}
function readPlan() {
return JSON.parse(readFileSync(PLAN_PATH, 'utf8'));
}
function writePlanAtomic(plan) {
const tmp = PLAN_PATH + '.tmp.' + process.pid;
writeFileSync(tmp, JSON.stringify(plan, null, 2));
renameSync(tmp, PLAN_PATH);
}
function claimTask(targetId) {
const plan = readPlan();
const task = plan.tasks.find(t =>
targetId ? t.id === targetId : (t.status === 'pending' && t.priority >= 7)
);
if (!task || task.status !== 'pending') return null;
task.status = 'in_progress';
task.worker = `ollama-${process.pid}`;
task.started_at = new Date().toISOString();
writePlanAtomic(plan);
return task;
}
function markDone(taskId) {
const plan = readPlan();
const task = plan.tasks.find(t => t.id === taskId);
if (task) { task.status = 'done'; task.done_at = new Date().toISOString(); }
writePlanAtomic(plan);
}
function markFailed(taskId, reason) {
const plan = readPlan();
const task = plan.tasks.find(t => t.id === taskId);
if (task) { task.status = 'failed'; task.error = reason; }
writePlanAtomic(plan);
}
async function ollamaGenerate(model, prompt) {
const resp = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, prompt, stream: false, options: { num_ctx: 8192, temperature: 0.2 } })
});
if (!resp.ok) throw new Error(`Ollama error: ${resp.status} ${await resp.text()}`);
const data = await resp.json();
return data.response;
}
function extractCodeBlocks(text) {
// Extract ```html ... ``` or ```css ... ``` or plain code blocks
const blocks = [];
const re = /```(?:html|css|js|javascript|xml|json|bash)?\n([\s\S]*?)```/g;
let m;
while ((m = re.exec(text)) !== null) {
blocks.push(m[1]);
}
// If no blocks found, return full text (assume it's raw code)
return blocks.length ? blocks : [text];
}
function extractFilesFromOutput(output, task) {
// Look for FILE: /path/to/file.html markers in output
const files = [];
const fileRe = /FILE:\s*(\/[^\n]+)\n([\s\S]*?)(?=FILE:|$)/g;
let m;
while ((m = fileRe.exec(output)) !== null) {
files.push({ path: m[1].trim(), content: m[2].trim() });
}
// If no FILE: markers — use file_targets from task if single file
if (!files.length && task.file_targets?.length === 1) {
const code = extractCodeBlocks(output);
files.push({ path: task.file_targets[0], content: code[0] });
}
return files;
}
function writeFiles(files) {
for (const { path, content } of files) {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, content, 'utf8');
log(` Wrote ${path} (${content.length} bytes)`);
}
}
async function runTask(task, model) {
log(`→ Task ${task.id}: ${task.title}`);
// Build prompt
const prompt = `You are a precise code generator. Output ONLY the requested file content with no explanation.
TASK: ${task.title}
${task.description}
RULES:
- Output file content only — no commentary before or after
- If multiple files: use FILE: /full/path/to/file.html marker before each file's content
- For HTML: complete self-contained files, inline CSS, Inter font from Google Fonts
- Colors: #0A2540 navy, #635BFF blue, white background
- Mobile-first, no emoji, SVG icons only
- Never truncate — write complete files
OUTPUT:`;
log(` Calling Ollama (${model})...`);
const output = await ollamaGenerate(model, prompt);
log(` Got ${output.length} chars from Ollama`);
// Extract and write files
const files = extractFilesFromOutput(output, task);
if (!files.length) {
throw new Error('No files extracted from Ollama output');
}
writeFiles(files);
// Write artifact
const artifactPath = join(ARTIFACTS_PATH, `${task.id}-DONE.md`);
const artifact = `# ${task.id} DONE\n\nTask: ${task.title}\nWorker: ollama/${model}\nCompleted: ${new Date().toISOString()}\n\nFiles written:\n${files.map(f => `- ${f.path}`).join('\n')}\n`;
writeFileSync(artifactPath, artifact);
return files;
}
// Main
async function main() {
log(`Ollama Worker starting (model: ${modelArg}, task: ${taskArg || 'auto'})`);
// Check Ollama is up
try {
const r = await fetch(`${OLLAMA_URL}/api/tags`);
const d = await r.json();
const models = d.models?.map(m => m.name) || [];
log(`Ollama models available: ${models.join(', ')}`);
if (!models.some(m => m.startsWith(modelArg.split(':')[0]))) {
log(`WARNING: model ${modelArg} not found — pulling...`);
execSync(`ollama pull ${modelArg}`, { stdio: 'inherit' });
}
} catch (e) {
log(`ERROR: Ollama not reachable at ${OLLAMA_URL}: ${e.message}`);
process.exit(1);
}
// Claim task
const task = claimTask(taskArg);
if (!task) {
log('No pending tasks found. Exiting.');
process.exit(0);
}
try {
const files = await runTask(task, modelArg);
markDone(task.id);
log(`${task.id} done — ${files.length} file(s) written`);
} catch (e) {
log(`${task.id} failed: ${e.message}`);
markFailed(task.id, e.message);
// Write blocked artifact
const blockedPath = join(ARTIFACTS_PATH, `${task.id}-BLOCKED.md`);
writeFileSync(blockedPath, `# ${task.id} BLOCKED\n\nReason: ${e.message}\nWorker: ollama/${modelArg}\nTime: ${new Date().toISOString()}\n`);
process.exit(1);
}
}
main().catch(e => { log(`FATAL: ${e.message}`); process.exit(1); });