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
Binary file not shown.
+166
View File
@@ -0,0 +1,166 @@
"""
VIMS Core — Base Detector
Generisk bas-klass för alla avvikelsedetektorer.
Varje ämne/artikel subklassar denna.
"""
import torch
import numpy as np
from abc import ABC, abstractmethod
from typing import List, Dict, Tuple, Optional
from pathlib import Path
import cv2
class VIMSBaseDetector(ABC):
"""
Bas-klass för alla VIMS-detektorer.
Usage:
class ATMDetector(VIMSBaseDetector):
TOPIC = "atm-monitoring"
CLASS_NAMES = {0: "skimming", 1: "vandalism", ...}
def preprocess(self, image):
# ATM-specifik preprocessning
...
"""
TOPIC: str = "base"
CLASS_NAMES: Dict[int, str] = {}
SEVERITY_MAP: Dict[str, int] = {}
def __init__(
self,
model_path: Optional[str] = None,
conf_threshold: float = 0.25,
device: str = "auto"
):
self.conf_threshold = conf_threshold
if device == "auto":
self.device = "cuda" if torch.cuda.is_available() else "cpu"
else:
self.device = device
self.model = self._load_model(model_path)
def _load_model(self, model_path: Optional[str]):
"""Ladda modell. Override i subklass om annan arkitektur."""
from ultralytics import YOLO
if model_path and Path(model_path).exists():
return YOLO(model_path)
else:
# Placeholder — ladda pretrained som bas
return YOLO("yolov8n.pt")
@abstractmethod
def preprocess(self, image: np.ndarray) -> np.ndarray:
"""
Ämnes-specifik preprocessning.
Args:
image: RGB image som numpy array
Returns:
Preprocessad bild redo för modellen
"""
pass
@abstractmethod
def postprocess(self, raw_output) -> List[Dict]:
"""
Ämnes-specifik postprocessning.
Args:
raw_output: Rå modell-output
Returns:
Lista med detekterade anomalier
"""
pass
def predict(self, image_path: str) -> Dict:
"""
Kör full prediction pipeline.
Args:
image_path: Sökväg till bild
Returns:
Resultat-dict med detektioner och metadata
"""
# Ladda bild
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Preprocess
processed = self.preprocess(image)
# Inferens
raw_output = self.model(processed, verbose=False)
# Postprocess
detections = self.postprocess(raw_output)
return {
"topic": self.TOPIC,
"image_path": image_path,
"detections": detections,
"summary": self._summarize(detections)
}
def _summarize(self, detections: List[Dict]) -> Dict:
"""Summera detektioner."""
if not detections:
return {
"total": 0,
"max_severity": 0,
"requires_action": False,
"types": []
}
severities = [d.get("severity", 1) for d in detections]
return {
"total": len(detections),
"max_severity": max(severities),
"requires_action": any(d.get("requires_action", False) for d in detections),
"types": list(set(d["class_name"] for d in detections))
}
def train(self, data_yaml: str, epochs: int = 100, **kwargs):
"""Träna modell på annoterad data."""
self.model.train(
data=data_yaml,
epochs=epochs,
device=self.device,
**kwargs
)
def export(self, format: str = "onnx"):
"""Exportera modell för deployment."""
self.model.export(format=format)
class VIMSInstanceRegistry:
"""Registry för alla VIMS-instanser."""
_instances: Dict[str, VIMSBaseDetector] = {}
@classmethod
def register(cls, topic: str, detector: VIMSBaseDetector):
"""Registrera en detektor."""
cls._instances[topic] = detector
@classmethod
def get(cls, topic: str) -> Optional[VIMSBaseDetector]:
"""Hämta detektor för ämne."""
return cls._instances.get(topic)
@classmethod
def list_topics(cls) -> List[str]:
"""Lista alla registrerade ämnen."""
return list(cls._instances.keys())
+220
View File
@@ -0,0 +1,220 @@
"""
VIMS Core — Database Manager
Hanterar databas per instans/ämne.
"""
import sqlite3
import json
from pathlib import Path
from typing import List, Dict, Optional
from datetime import datetime
class VIMSDatabase:
"""
Databas-hanterare för VIMS-instanser.
Usage:
db = VIMSDatabase("atm-monitoring")
db.create_schema()
db.add_detection(atm_id="ATM-001", anomaly_type="skimming", confidence=0.94)
"""
def __init__(self, topic: str, db_dir: str = "data"):
self.topic = topic
self.table_prefix = topic.replace("-", "_")
self.db_path = Path(db_dir) / f"{topic}.db"
self.db_path.parent.mkdir(parents=True, exist_ok=True)
def _connect(self):
"""Skapa databas-anslutning."""
conn = sqlite3.connect(str(self.db_path))
conn.row_factory = sqlite3.Row
return conn
def create_schema(self, anomaly_classes: List[str]):
"""
Skapa databas-schema för ämne.
Args:
anomaly_classes: Lista med anomaliklasser
"""
conn = self._connect()
cursor = conn.cursor()
# Huvudtabell för detektioner
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {self.table_prefix}_detections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
asset_id TEXT NOT NULL,
image_path TEXT,
anomaly_type TEXT NOT NULL,
confidence REAL,
severity INTEGER,
bbox TEXT,
detected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
verified BOOLEAN DEFAULT FALSE,
status TEXT DEFAULT 'open',
metadata TEXT
)
""")
# Assets-tabell
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {self.table_prefix}_assets (
id TEXT PRIMARY KEY,
name TEXT,
location TEXT,
status TEXT DEFAULT 'active',
last_check DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
# Alerts-tabell
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {self.table_prefix}_alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
detection_id INTEGER,
severity INTEGER,
message TEXT,
sent_at DATETIME,
status TEXT DEFAULT 'pending',
FOREIGN KEY (detection_id) REFERENCES {self.table_prefix}_detections(id)
)
""")
# Index
cursor.execute(f"""
CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_detections_asset
ON {self.table_prefix}_detections(asset_id)
""")
cursor.execute(f"""
CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_detections_type
ON {self.table_prefix}_detections(anomaly_type)
""")
cursor.execute(f"""
CREATE INDEX IF NOT EXISTS idx_{self.table_prefix}_detections_status
ON {self.table_prefix}_detections(status)
""")
conn.commit()
conn.close()
print(f"Schema created for {self.topic}")
def add_detection(
self,
asset_id: str,
anomaly_type: str,
confidence: float,
severity: int,
image_path: Optional[str] = None,
bbox: Optional[List[float]] = None,
metadata: Optional[Dict] = None
) -> int:
"""
Lägg till ny detektion.
Returns:
ID för insatt rad
"""
conn = self._connect()
cursor = conn.cursor()
cursor.execute(f"""
INSERT INTO {self.table_prefix}_detections
(asset_id, image_path, anomaly_type, confidence, severity, bbox, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
asset_id,
image_path,
anomaly_type,
confidence,
severity,
json.dumps(bbox) if bbox else None,
json.dumps(metadata) if metadata else None
))
detection_id = cursor.lastrowid
# Skapa alert om severity >= 4
if severity >= 4:
cursor.execute(f"""
INSERT INTO {self.table_prefix}_alerts
(detection_id, severity, message)
VALUES (?, ?, ?)
""", (
detection_id,
severity,
f"Critical {anomaly_type} detected on {asset_id}"
))
conn.commit()
conn.close()
return detection_id
def get_detections(
self,
asset_id: Optional[str] = None,
status: Optional[str] = None,
limit: int = 100
) -> List[Dict]:
"""Hämta detektioner med filter."""
conn = self._connect()
cursor = conn.cursor()
query = f"SELECT * FROM {self.table_prefix}_detections WHERE 1=1"
params = []
if asset_id:
query += " AND asset_id = ?"
params.append(asset_id)
if status:
query += " AND status = ?"
params.append(status)
query += " ORDER BY detected_at DESC LIMIT ?"
params.append(limit)
cursor.execute(query, params)
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def get_stats(self) -> Dict:
"""Hämta statistik för ämne."""
conn = self._connect()
cursor = conn.cursor()
cursor.execute(f"""
SELECT
COUNT(*) as total_detections,
COUNT(DISTINCT asset_id) as assets_monitored,
COUNT(CASE WHEN status = 'open' THEN 1 END) as open_anomalies,
MAX(severity) as max_severity
FROM {self.table_prefix}_detections
""")
row = cursor.fetchone()
conn.close()
return dict(row) if row else {}
def verify_detection(self, detection_id: int, verdict: str, notes: str = ""):
"""Verifiera detektion manuellt."""
conn = self._connect()
cursor = conn.cursor()
cursor.execute(f"""
UPDATE {self.table_prefix}_detections
SET verified = TRUE, status = ?
WHERE id = ?
""", (verdict, detection_id))
conn.commit()
conn.close()