{% extends "base.html" %} {% block title %}MLOps{% endblock %} {% block breadcrumb %}MLOps{% endblock %} {% block extra_head %} {% endblock %} {% block content %} {# ═══════════════════════════════════════════════════════════════════ AQUILIA ADMIN — MLOPS DASHBOARD (v1 — Comprehensive) Model Registry · Serving Metrics · Drift Detection · Rollouts Experiments · Plugins · Circuit Breaker · Memory · Lineage ═══════════════════════════════════════════════════════════════════ #}

MLOps

Model registry, inference serving, drift detection, rollouts, experiments, and observability

{# ═══ STATUS BAR ═══ #}
{% if available %}
Available {% else %}
Not Configured {% endif %}
· {{ total_models }} model{{ 's' if total_models != 1 else '' }} · {{ total_plugins }} plugin{{ 's' if total_plugins != 1 else '' }} · {{ total_experiments }} experiment{{ 's' if total_experiments != 1 else '' }} · {{ total_rollouts }} rollout{{ 's' if total_rollouts != 1 else '' }} · {{ "{:,}".format(total_inferences) }} inferences {% if rbac and rbac.total_users|default(0) > 0 %} · {{ rbac.total_users }} RBAC user{{ 's' if rbac.total_users != 1 else '' }} {% endif %}
{# ═══ METRIC CARDS (row 1) ═══ #}
0
Models
Registered in registry
0
Inferences
{{ total_errors }} errors
{{ latency.p50|default('—') }}
P50 Latency (ms)
P95: {{ latency.p95|default('—') }}ms · P99: {{ latency.p99|default('—') }}ms
0
Tokens Generated
{{ throughput.ewma|default('—') }} tok/s
0
Stream Requests
TTFT: {{ throughput.ttft_ewma|default('—') }}ms
{{ active_experiments }}
Active Experiments
{{ active_rollouts }} active rollout{{ 's' if active_rollouts != 1 else '' }}
{# ═══ CHARTS ROW 1: Model States + Framework Distribution ═══ #}
Analytics
{# ── Model States Doughnut ── #}
Model States
{% if charts.model_states %} {% else %}
No model state data
{% endif %}
{# ── Framework Distribution ── #}
Framework Distribution
{% if charts.frameworks %} {% else %}
No framework data
{% endif %}
{# ═══ CHARTS ROW 2: Plugin States + Rollout Phases ═══ #}
{# ── Plugin States ── #}
Plugin States
{% if charts.plugin_states %} {% else %}
No plugins loaded
{% endif %}
{# ── Rollout Phases ── #}
Rollout Phases
{% if charts.rollout_phases %} {% else %}
No rollouts yet
{% endif %}
{# ═══ CHARTS ROW 3: Experiment Statuses + Memory Allocations ═══ #}
{# ── Experiment Statuses ── #}
Experiment Statuses
{% if charts.experiment_statuses %} {% else %}
No experiments configured
{% endif %}
{# ── Memory Allocations ── #}
Memory Allocations by Model
{% if charts.memory_allocations %} {% else %}
No memory data available
{% endif %}
{# ═══ INFRASTRUCTURE PANELS: Circuit Breaker + Rate Limiter + Memory ═══ #}
Infrastructure Health
{# ── Circuit Breaker ── #}
Circuit Breaker
{% set cb_state = circuit_breaker.state|default('closed') %}
{{ cb_state|upper }}
Total
{{ circuit_breaker.total_requests|default(0) }}
Failures
{{ circuit_breaker.total_failures|default(0) }}
Trips
{{ circuit_breaker.total_trips|default(0) }}
Successes
{{ circuit_breaker.total_successes|default(0) }}
{# ── Rate Limiter ── #}
Rate Limiter
{% set rl_allowed = rate_limiter.total_allowed|default(0) %} {% set rl_rejected = rate_limiter.total_rejected|default(0) %} {% set rl_total = rl_allowed + rl_rejected %} {% set rl_rate = (rl_allowed * 100 / rl_total)|round(1) if rl_total > 0 else 100 %}
{{ rl_rate }}%
Accept Rate
Allowed
{{ rl_allowed }}
Rejected
{{ rl_rejected }}
{% if rate_limiter.tokens_available is defined %}
Tokens: {{ "%.1f"|format(rate_limiter.tokens_available) }} / {{ rate_limiter.capacity|default('∞') }}
{% endif %}
{# ── Memory Tracker ── #}
Memory Tracker
{% set mem_used = memory.current_bytes|default(0) %} {% set mem_soft = memory.soft_limit|default(0) %} {% set mem_hard = memory.hard_limit|default(0) %} {% set mem_pct = (mem_used * 100 / mem_hard)|round(1) if mem_hard > 0 else 0 %}
{{ mem_pct }}%
Memory Usage
{% if mem_soft > 0 and mem_hard > 0 %}
{% endif %}
Used
{{ "%.1f"|format(mem_used / 1048576) }}MB
Soft
{{ "%.1f"|format(mem_soft / 1048576) }}MB
Hard
{{ "%.1f"|format(mem_hard / 1048576) }}MB
{% if memory.tracked_models|default(0) > 0 %}
{{ memory.tracked_models }} tracked models · {{ memory.evictions|default(0) }} evictions
{% endif %}
{# ═══ DRIFT DETECTION PANEL ═══ #} {% if drift and drift.method is defined %}
Drift Detection
Data Drift Monitor
Method
{{ drift.method }}
Threshold
{{ drift.threshold|default('—') }}
Reference Data
{% if drift.has_reference %}
Loaded
{% else %}
Not loaded
{% endif %}
{% endif %} {# ═══ AUTOSCALER PANEL ═══ #} {% if autoscaler and autoscaler.policy is defined %}
Autoscaler
Scaling Policy & State
{# ── Current Replicas Gauge ── #} {% set as_policy = autoscaler.policy %} {% set as_replicas = autoscaler.current_replicas|default(1) %} {% set as_min = as_policy.min_replicas|default(1) %} {% set as_max = as_policy.max_replicas|default(10) %} {% set as_pct = ((as_replicas - as_min) * 100 / (as_max - as_min))|round if as_max > as_min else 50 %}
{# ── Replica Gauge ── #}
{{ as_replicas }}
Current Replicas
min {{ as_min }} max {{ as_max }}
{# ── Policy Details ── #}
Target Concurrency
{{ as_policy.target_concurrency|default('—') }}
Target P95 (ms)
{{ as_policy.target_latency_p95_ms|default('—') }}
Cooldown (s)
{{ as_policy.cooldown_seconds|default('—') }}
{% if as_policy.target_gpu_utilization is defined %}
GPU Target
{{ (as_policy.target_gpu_utilization * 100)|round }}%
{% endif %} {% if as_policy.target_tokens_per_second is defined %}
Target tok/s
{{ as_policy.target_tokens_per_second }}
{% endif %}
{# ── Window Stats ── #} {% if autoscaler.window_stats %} {% set ws = autoscaler.window_stats %}
Sliding Window Metrics
Window RPS
{{ "%.1f"|format(ws.window_rps|default(0)) }}
Avg Latency
{{ "%.1f"|format(ws.avg_latency|default(0)) }}ms
Error Rate
{{ "%.2f"|format(ws.error_rate|default(0) * 100) }}%
Samples
{{ ws.samples|default(0) }}
{% endif %} {# ── Last Scaling Decision ── #} {% if autoscaler.last_decision %} {% set ld = autoscaler.last_decision %} {% set ld_action = 'scale_up' if ld.desired|default(0) > ld.current|default(0) else ('scale_down' if ld.desired|default(0) < ld.current|default(0) else 'none') %}
Last Decision
{{ ld_action }} {% if ld.reason %}{{ ld.reason }}{% endif %} {% if ld.desired is defined %}{{ ld.current }} → {{ ld.desired }} replicas{% endif %}
{% endif %}
{% endif %} {# ═══ RBAC / SECURITY PANEL ═══ #} {% if rbac and rbac.roles|default([])|length > 0 %}
Security & RBAC
Role-Based Access Control — {{ rbac.roles|length }} Roles · {{ rbac.total_users|default(0) }} Users
{% for role in rbac.roles %} {% endfor %}
Role Permissions Users
{{ role.name }}
{% for perm in role.permissions|default([]) %} {{ perm }} {% endfor %}
{{ role.user_count|default(0) }}
{% endif %} {# ═══ BATCH QUEUE + LRU CACHE PANELS ═══ #} {% if batch_queue or lru_cache %}
Queue & Cache
{# ── Batch Queue ── #} {% if batch_queue and batch_queue.capacity|default(0) > 0 %}
Adaptive Batch Queue
{% set bq_pct = (batch_queue.size|default(0) * 100 / batch_queue.capacity)|round if batch_queue.capacity > 0 else 0 %}
{{ bq_pct }}%
Queue Utilization ({{ batch_queue.size|default(0) }}/{{ batch_queue.capacity }})
Enqueued
{{ batch_queue.enqueued|default(0) }}
Dequeued
{{ batch_queue.dequeued|default(0) }}
Dropped
{{ batch_queue.dropped|default(0) }}
{% if batch_queue.pending_tokens is defined %}
Pending tokens: {{ batch_queue.pending_tokens }}
{% endif %}
{% else %}
Adaptive Batch Queue
Not configured
{% endif %} {# ── LRU Cache ── #} {% if lru_cache and lru_cache.capacity|default(0) > 0 %}
LRU Model Cache
{% set lru_pct = (lru_cache.size|default(0) * 100 / lru_cache.capacity)|round if lru_cache.capacity > 0 else 0 %} {% set hit_rate = lru_cache.hit_rate|default(0) %}
{{ "%.0f"|format(hit_rate * 100) }}%
Hit Rate
{{ lru_cache.size|default(0) }}/{{ lru_cache.capacity }}
Entries
Hits
{{ lru_cache.hits|default(0) }}
Misses
{{ lru_cache.misses|default(0) }}
{% else %}
LRU Model Cache
Not configured
{% endif %}
{% endif %} {# ═══ HOT MODELS ═══ #} {% if hot_models|length > 0 %}
Hot Models
Most Requested Models (by inference count)
{% for hm in hot_models %} {% set max_score = hot_models[0].score if hot_models|length > 0 else 1 %} {% set bar_pct = (hm.score * 100 / max_score)|round if max_score > 0 else 0 %}
#{{ loop.index }}
{{ hm.name }}
{{ "%.0f"|format(hm.score) }}
{% endfor %}
{% endif %} {# ═══════════════════════════════════════════════════════════════════ ADVANCED MLOPS FEATURES Live Playground · Model Comparison · Batch Inference · Health Inference History · Alert Rules · Export ═══════════════════════════════════════════════════════════════════ #} {# ═══ LIVE INFERENCE PLAYGROUND ═══ #} {% if models|length > 0 %}
Live Inference Playground Interactive
Test Models in Real-Time

Send JSON input to any registered model and inspect the output, latency, and preprocessing pipeline.

{{ models[0].name|default('Select model') }}
{% if models|length > 4 %} {% endif %} {% for model in models %}
{{ model.name|default('unknown') }} {{ model.version|default('?') }}
{% endfor %}
Input (JSON)
Output
Run inference to see output...
{% endif %} {# ═══ MODEL COMPARISON ═══ #} {% if models|length > 1 %}
Model Comparison
Side-by-Side Model Output Comparison

Run the same input through multiple models and compare outputs, latency, and behavior differences.

{{ models[0].name|default('Model A') }}
{% for model in models %}
{{ model.name|default('unknown') }}
{% endfor %}
vs
{{ models[1].name|default('Model B') if models|length > 1 else 'Model B' }}
{% for model in models %}
{{ model.name|default('unknown') }}
{% endfor %}
Shared Input (JSON)
{% endif %} {# ═══ BATCH INFERENCE ═══ #} {% if models|length > 0 %}
Batch Inference
Batch Prediction Runner

Submit multiple inputs as a JSON array and process them in a single batch. Max 50 items per batch.

{{ models[0].name|default('Select model') }}
{% for model in models %}
{{ model.name|default('unknown') }}
{% endfor %}
Inputs (JSON Array)
{% endif %} {# ═══ MODEL HEALTH MATRIX ═══ #} {% if models|length > 0 %}
Model Health Matrix
Health Check Dashboard
{% for model in models %}
{{ model.name|default('unknown') }}
{{ model.state|default('unknown') }}
{% endfor %}
{% endif %} {# ═══ ALERT RULES & NOTIFICATIONS ═══ #}
Alert Rules & Monitoring
Alert Configuration
{% if alert_rules|default([])|length > 0 %} {% for rule in alert_rules %}
{% set metric_opts = [('error_rate','Error Rate'),('p50_latency','P50 Latency'),('p95_latency','P95 Latency'),('p99_latency','P99 Latency'),('total_errors','Total Errors'),('drift_score','Drift Score'),('memory_usage_pct','Memory Usage %')] %}
{% for v,l in metric_opts %}{% if v == rule.metric %}{{ l }}{% endif %}{% endfor %}
{% for v,l in metric_opts %}
{{ l }}
{% endfor %}
{% set op_opts = [('>','>'),('>=','≥'),('<','<'),('==','=')] %} {% set op_labels = {'>': '>', '>=': '≥', '<': '<', '==': '='} %}
{{ op_labels[rule.operator]|default(rule.operator) }}
{% for v,l in op_opts %}
{{ l|safe }}
{% endfor %}
{% set sev_opts = [('info','Info'),('warning','Warning'),('critical','Critical')] %}
{{ rule.severity|default('warning')|capitalize }}
{% for v,l in sev_opts %}
{{ l }}
{% endfor %}
{% endfor %} {% else %}
No alert rules configured. Click "Add Rule" to create one.
{% endif %}
{% if triggered_alerts|default([])|length > 0 %}
{{ triggered_alerts|length }} Active Alert{{ 's' if triggered_alerts|length != 1 else '' }}
{% for alert in triggered_alerts %}
{{ alert.severity|default('warning') }} {{ alert.metric }} {{ alert.operator }} {{ alert.threshold }} Current: {{ alert.current_value }}
{% endfor %}
{% endif %}
{# ═══ INFERENCE HISTORY / AUDIT LOG ═══ #}
Inference History Audit Log
{% if inference_history|default([])|length > 0 %} {% for h in inference_history %} {% endfor %}
Timestamp Model Status Latency User Details
{{ h.timestamp|default('—') }}
{{ h.model|default('—') }} {{ h.version|default('?') }}
{{ h.status|default('error') }} {{ h.latency_ms|default(0) }}ms {{ h.user|default('—') }}
{% else %}

No inference history yet

Use the Live Inference Playground above to test models and build history.

{% endif %}
{# ═══ EXPORT & SNAPSHOT ═══ #}
Export & Snapshots
Export MLOps State

Download a complete snapshot of all MLOps data including models, metrics, experiments, alert rules, and inference history.

{# ═══ MODEL REGISTRY ═══ #}
Model Registry
{% if models|length > 0 %}
{# ── Card Grid View ── #}
{% for model in models %} {% set mstate = model.state|default('unknown') %} {% set icon_class = 'loaded' if mstate == 'loaded' else ('failed' if mstate == 'failed' else ('loading' if mstate == 'loading' else 'default')) %}
{{ model.name|default('unknown') }}
v{{ model.version|default('?') }}
{{ mstate }}
Framework{{ model.framework|default('custom') }}
Type{{ model.model_type|default('—') }}
Device{{ model.device|default('cpu') }}
Runtime{{ model.runtime|default('python') }}
{% endfor %}
{# ── Table View (hidden by default) ── #} {% else %}

No models registered

Register models using @model or @serve decorators.

{% endif %}
{# ═══ ROLLOUTS TABLE ═══ #}
Rollouts
{% if rollouts|length > 0 %} {% for ro in rollouts %} {% endfor %}
ID From → To Strategy Phase Progress Steps
{{ ro.id[:12] }}… {{ ro.from_version }}{{ ro.to_version }} {{ ro.strategy }} {{ ro.phase }}
{{ ro.percentage }}%
{{ ro.steps }}
{% else %}

No rollouts active

Deploy model versions using RolloutEngine with canary, A/B, blue-green, or rolling strategies.

{% endif %}
{# ═══ EXPERIMENTS TABLE ═══ #}
A/B Experiments
{% if experiments|length > 0 %} {% for exp in experiments %} {% endfor %}
Experiment Status Variants Total Assignments Winner
{{ exp.name|default(exp.id|default('—')) }}
{{ exp.status|default('inactive') }} {% if exp.variants|default([])|length > 0 %}
{% for v in exp.variants %} {{ v.name|default(v) }}{% if v.weight is defined %} ({{ "%.0f"|format(v.weight * 100) }}%){% endif %} {% endfor %}
{% else %} 0 {% endif %}
{{ exp.total_assignments|default(0) }} {% if exp.winner is defined and exp.winner %} 🏆 {{ exp.winner }} {% else %} {% endif %}
{% else %}

No experiments configured

Use ExperimentLedger to manage A/B tests with deterministic assignment.

{% endif %}
{# ═══ PER-MODEL METRICS TABLE ═══ #} {% if per_model_metrics|default([])|length > 0 %}
Per-Model Metrics
{% for pm in per_model_metrics %} {% endfor %}
Model Inferences Errors Avg Latency (ms) P95 Latency (ms) Tokens Error Rate
{{ pm.model }}
{{ pm.inferences|default(0) }} {{ pm.errors|default(0) }} {{ "%.1f"|format(pm.avg_latency|default(0)) }} {{ "%.1f"|format(pm.p95_latency|default(0)) }} {{ pm.tokens|default(0) }} {% set err_rate = pm.error_rate|default(0) %} {{ "%.2f"|format(err_rate * 100) }}%
{% endif %} {# ═══ PLUGINS TABLE ═══ #}
Plugins
{% if plugins|length > 0 %} {% for plugin in plugins %} {% endfor %}
Name Version State Error
{{ plugin.name }}
{{ plugin.version }} {{ plugin.state }} {{ plugin.error or '—' }}
{% else %}

No plugins loaded

Plugins are discovered via aquilia.mlops entry points.

{% endif %}
{# ═══ MODEL LINEAGE ═══ #} {% if lineage_nodes > 0 %}
Model Lineage
Lineage DAG — {{ lineage_nodes }} nodes
{% for node_id, node_data in lineage.items() %}
{{ node_id }}
{% if node_data is mapping %}
{% if node_data.parents is defined and node_data.parents|length > 0 %} Parents: {{ node_data.parents|join(', ') }} {% else %} Root node {% endif %}
{% endif %}
{% endfor %}
{% endif %} {# ═══ CAPABILITIES REFERENCE ═══ #}
Capabilities Reference
Supported Enums & Configurations
{% for fw in frameworks %}{{ fw }}{% endfor %}
{% for rt in runtime_kinds %}{{ rt }}{% endfor %}
{% for mt in model_types %}{{ mt }}{% endfor %}
{% for dt in device_types %}{{ dt }}{% endfor %}
{% for bs in batching_strategies %}{{ bs }}{% endfor %}
{% for rs in rollout_strategies %}{{ rs }}{% endfor %}
{% for dm in drift_methods %}{{ dm }}{% endfor %}
{% for qp in quantize_presets %}{{ qp }}{% endfor %}
{% for et in export_targets %}{{ et }}{% endfor %}
{% for im in inference_modes %}{{ im }}{% endfor %}
{% for dtype in dtypes %}{{ dtype }}{% endfor %}
{% if dtypes|length == 0 %} No DTypes available {% endif %}
{% for perm in permissions %}{{ perm }}{% endfor %}
{% if permissions|length == 0 %} No permissions available {% endif %}
{# ═══ PROMETHEUS EXPORT ═══ #} {% if prometheus_text %}
Prometheus Export
Prometheus Text Format

        
{% endif %} {# ═══ QUICK SETUP — CODE SNIPPETS ═══ #}
Quick Setup

Quick Setup — Code Snippets

Copy-paste examples for Aquilia MLOps subsystem.

models/sentiment.py
from aquilia.mlops import AquiliaModel, model

@model(name="sentiment", version="1.0.0", framework="pytorch")
class SentimentModel(AquiliaModel):
    """Sentiment analysis model with auto-registration."""

    async def load(self):
        import torch
        self.tokenizer = AutoTokenizer.from_pretrained("bert-base")
        self.model = AutoModelForSequenceClassification.from_pretrained("bert-base")

    async def predict(self, request):
        tokens = self.tokenizer(request.input, return_tensors="pt")
        output = self.model(**tokens)
        return {"sentiment": output.logits.argmax().item()}

    async def health(self):
        return {"status": "healthy", "loaded": True}
models/classifier.py
from aquilia.mlops import serve

# Functional decorator for quick model serving
@serve(name="text-classifier", version="2.1.0")
async def classify(request):
    """Classify text into categories."""
    text = request.input.get("text", "")
    # Your inference logic here
    return {
        "category": "technology",
        "confidence": 0.95,
        "model": "text-classifier"
    }

# Routes auto-generated:
#   POST /predict/text-classifier
#   GET  /health/text-classifier
#   GET  /metrics/text-classifier
models/pipeline.py
from aquilia.mlops.engine import HookRegistry, on_load, preprocess, postprocess

hooks = HookRegistry()

@hooks.on_load
async def warm_up(model, context):
    """Run warmup inference after model load."""
    await model.predict({"input": "warmup"})

@hooks.preprocess
async def validate_input(data, context):
    """Validate and normalize input before inference."""
    if "input" not in data:
        raise ValueError("Missing 'input' field")
    data["input"] = data["input"].strip().lower()
    return data

@hooks.postprocess
async def add_metadata(result, context):
    """Enrich result with model metadata."""
    result["model_version"] = context.model_version
    result["latency_ms"] = context.elapsed_ms
    return result
deploy/rollout.py
from aquilia.mlops import RolloutEngine, RolloutConfig, RolloutStrategy

engine = app.di.get(RolloutEngine)

# Canary deployment: gradually shift traffic
config = RolloutConfig(
    from_version="1.0.0",
    to_version="2.0.0",
    strategy=RolloutStrategy.CANARY,
    steps=[10, 25, 50, 75, 100],
    metric_gate="error_rate < 0.01",
)
rollout = await engine.start(config)

# Monitor and advance
await engine.advance(rollout.id)  # 10% -> 25%
await engine.rollback(rollout.id) # auto-rollback on failure
observe/drift_check.py
from aquilia.mlops.observe import DriftDetector, DriftMethod
import numpy as np

detector = app.di.get(DriftDetector)

# Set baseline reference distribution
reference_data = np.random.normal(0, 1, size=10000)
detector.set_reference(reference_data)

# Check incoming production data for drift
production_data = np.random.normal(0.3, 1.2, size=500)
report = detector.detect(production_data)

print(f"Method: {report.method}")      # psi, ks_test, etc.
print(f"Score:  {report.score:.4f}")    # drift magnitude
print(f"Drifted: {report.is_drifted}")  # True/False
print(f"Threshold: {report.threshold}") # configured threshold

# Supported methods:
#   DriftMethod.PSI           - Population Stability Index
#   DriftMethod.KS_TEST       - Kolmogorov-Smirnov test
#   DriftMethod.DISTRIBUTION  - Distribution distance
#   DriftMethod.EMBEDDING     - Embedding cosine drift
#   DriftMethod.PERPLEXITY    - LLM perplexity drift
scheduler/scaling.py
from aquilia.mlops.scheduler import Autoscaler, ScalingPolicy

policy = ScalingPolicy(
    min_replicas=1,
    max_replicas=10,
    target_concurrency=50,
    target_latency_p95=200.0,    # ms
    target_gpu_utilization=0.75,
    cooldown=60,                 # seconds
)

scaler = Autoscaler(policy=policy, current_replicas=2)

# Record inference metrics
scaler.record(latency_ms=45.0, error=False)
scaler.record(latency_ms=180.0, error=False)

# Evaluate scaling decision
decision = scaler.evaluate()
print(f"Action: {decision.action}")        # scale_up / scale_down / none
print(f"Target: {decision.target_replicas}")
print(f"Reason: {decision.reason}")

# Generate Kubernetes HPA manifest
hpa_yaml = scaler.generate_hpa_manifest(
    name="sentiment-model",
    namespace="ml-serving",
)
print(hpa_yaml)
security/access.py
from aquilia.mlops.security import RBACManager, Permission, Role

rbac = app.di.get(RBACManager)

# Built-in roles: VIEWER, DEVELOPER, DEPLOYER, ADMIN
rbac.assign_role("alice", "ADMIN")
rbac.assign_role("bob", "DEVELOPER")
rbac.assign_role("charlie", "VIEWER")

# Check permissions before operations
if rbac.check_permission("bob", Permission.PACK_WRITE):
    await registry.register(model)

if rbac.check_permission("bob", Permission.ROLLOUT_MANAGE):
    await engine.start(rollout_config)  # denied!

# Custom roles
rbac.add_role(Role(
    name="ML_ENGINEER",
    permissions=[
        Permission.PACK_READ,
        Permission.PACK_WRITE,
        Permission.REGISTRY_ADMIN,
    ],
))

# Available permissions:
#   PACK_READ, PACK_WRITE, PACK_DELETE, PACK_PROMOTE,
#   PACK_SIGN, REGISTRY_ADMIN, PLUGIN_INSTALL, ROLLOUT_MANAGE
config/base.yaml
# aquilia.toml — MLOps configuration
[mlops]
enabled = true
device = "auto"         # auto-detect CPU/CUDA/MPS
max_models = 50
memory_limit = "4GB"

[mlops.serving]
host = "0.0.0.0"
port = 8080
workers = 4
batch_size = 32
batch_timeout_ms = 50

[mlops.circuit_breaker]
failure_threshold = 5
recovery_timeout = 30
half_open_max = 3

[mlops.drift]
method = "psi"          # psi, ks_test, distribution, embedding
threshold = 0.2
check_interval = 3600   # seconds

[mlops.models.sentiment]
class_path = "models.sentiment.SentimentModel"
version = "1.0.0"
framework = "pytorch"
{# ═══ SLIDE-OUT DETAIL DRAWER ═══ #}

Detail

{# Content injected by JS #}
{% endblock %} {% block extra_js %} // ── Chart.js configuration ────────────────────────────────────── (function() { 'use strict'; if (typeof Chart === 'undefined') return; var isDark = document.documentElement.getAttribute('data-theme') !== 'light'; var gridColor = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.08)'; var textColor = isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'; Chart.defaults.color = textColor; Chart.defaults.borderColor = gridColor; Chart.defaults.font.family = "'Outfit', system-ui, sans-serif"; Chart.defaults.font.size = 11; Chart.defaults.plugins.legend.labels.boxWidth = 10; Chart.defaults.plugins.legend.labels.padding = 12; var charts = {{ charts | tojson }}; // ── Model States Doughnut ──────────────────────────────────── var ms = charts.model_states || {}; var msLabels = Object.keys(ms); var msValues = Object.values(ms); if (msLabels.length) { var stateColorMap = { 'loaded': '#22c55e', 'unloaded': '#a1a1aa', 'loading': '#3b82f6', 'failed': '#ef4444', 'prepared': '#a855f7', 'unloading': '#fbbf24', 'unknown': '#6b7280', }; var msColors = msLabels.map(function(l) { return stateColorMap[l] || '#6b7280'; }); new Chart(document.getElementById('chart-model-states'), { type: 'doughnut', data: { labels: msLabels, datasets: [{ data: msValues, backgroundColor: msColors, borderWidth: 0, hoverOffset: 6 }], }, options: { responsive: true, maintainAspectRatio: false, cutout: '68%', plugins: { legend: { position: 'bottom', labels: { padding: 10 } } }, }, }); } // ── Framework Distribution ─────────────────────────────────── var fw = charts.frameworks || {}; var fwLabels = Object.keys(fw); var fwValues = Object.values(fw); if (fwLabels.length) { var fwColorMap = { 'pytorch': '#ee4c2c', 'tensorflow': '#ff6f00', 'onnx': '#005CED', 'jax': '#8b5cf6', 'sklearn': '#f97316', 'xgboost': '#0ea5e9', 'lightgbm': '#22c55e', 'huggingface': '#fbbf24', 'custom': '#a1a1aa', 'triton': '#76b900', 'tflite': '#ff6f00', }; var fwColors = fwLabels.map(function(l) { return fwColorMap[l] || '#6b7280'; }); new Chart(document.getElementById('chart-frameworks'), { type: 'doughnut', data: { labels: fwLabels, datasets: [{ data: fwValues, backgroundColor: fwColors, borderWidth: 0, hoverOffset: 6 }], }, options: { responsive: true, maintainAspectRatio: false, cutout: '68%', plugins: { legend: { position: 'bottom', labels: { padding: 10 } } }, }, }); } // ── Plugin States ──────────────────────────────────────────── var ps = charts.plugin_states || {}; var psLabels = Object.keys(ps); var psValues = Object.values(ps); if (psLabels.length) { var psColorMap = { 'discovered': '#38bdf8', 'loaded': '#a855f7', 'activated': '#22c55e', 'deactivated': '#a1a1aa', 'error': '#ef4444', }; var psColors = psLabels.map(function(l) { return psColorMap[l] || '#6b7280'; }); new Chart(document.getElementById('chart-plugin-states'), { type: 'doughnut', data: { labels: psLabels, datasets: [{ data: psValues, backgroundColor: psColors, borderWidth: 0, hoverOffset: 6 }], }, options: { responsive: true, maintainAspectRatio: false, cutout: '68%', plugins: { legend: { position: 'bottom', labels: { padding: 10 } } }, }, }); } // ── Rollout Phases ─────────────────────────────────────────── var rp = charts.rollout_phases || {}; var rpLabels = Object.keys(rp); var rpValues = Object.values(rp); if (rpLabels.length) { var rpColorMap = { 'pending': '#a1a1aa', 'in_progress': '#3b82f6', 'completed': '#22c55e', 'rolled_back': '#fbbf24', 'failed': '#ef4444', }; var rpColors = rpLabels.map(function(l) { return rpColorMap[l] || '#6b7280'; }); new Chart(document.getElementById('chart-rollout-phases'), { type: 'bar', data: { labels: rpLabels, datasets: [{ label: 'Rollouts', data: rpValues, backgroundColor: rpColors, borderWidth: 0, borderRadius: 4, }], }, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', scales: { x: { beginAtZero: true, grid: { color: gridColor }, ticks: { stepSize: 1 } }, y: { grid: { display: false } }, }, plugins: { legend: { display: false } }, }, }); } // ── Experiment Statuses ────────────────────────────────────── var es = charts.experiment_statuses || {}; var esLabels = Object.keys(es); var esValues = Object.values(es); if (esLabels.length) { var esColorMap = { 'active': '#22c55e', 'inactive': '#a1a1aa', 'paused': '#fbbf24', 'concluded': '#3b82f6', 'completed': '#22c55e', }; var esColors = esLabels.map(function(l) { return esColorMap[l] || '#6b7280'; }); new Chart(document.getElementById('chart-experiment-statuses'), { type: 'doughnut', data: { labels: esLabels, datasets: [{ data: esValues, backgroundColor: esColors, borderWidth: 0, hoverOffset: 6 }], }, options: { responsive: true, maintainAspectRatio: false, cutout: '68%', plugins: { legend: { position: 'bottom', labels: { padding: 10 } } }, }, }); } // ── Memory Allocations by Model ───────────────────────────── var ma = charts.memory_allocations || {}; var maLabels = Object.keys(ma); var maValues = Object.values(ma); if (maLabels.length) { var maPalette = ['#22c55e','#3b82f6','#a855f7','#f97316','#ef4444','#fbbf24','#0ea5e9','#ec4899','#14b8a6','#6366f1']; var maColors = maLabels.map(function(_, i) { return maPalette[i % maPalette.length]; }); new Chart(document.getElementById('chart-memory-allocations'), { type: 'bar', data: { labels: maLabels, datasets: [{ label: 'Memory (MB)', data: maValues, backgroundColor: maColors, borderWidth: 0, borderRadius: 4, }], }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { display: false } }, y: { beginAtZero: true, grid: { color: gridColor }, ticks: { callback: function(v) { return v + ' MB'; } } }, }, plugins: { legend: { display: false } }, }, }); } })(); // ── Count-up animation ────────────────────────────────────────── document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('[data-count]').forEach(function(el) { var target = parseInt(el.getAttribute('data-count'), 10); if (isNaN(target) || target === 0) { el.textContent = '0'; return; } var duration = 600, start = performance.now(); function step(now) { var p = Math.min((now - start) / duration, 1); var ease = 1 - Math.pow(1 - p, 3); el.textContent = Math.round(target * ease).toLocaleString(); if (p < 1) requestAnimationFrame(step); else el.classList.add('count-done'); } requestAnimationFrame(step); }); // ── Model search filter (grid + table) ── var searchInput = document.getElementById('modelSearch'); var gridView = document.getElementById('modelGridView'); var tableView = document.getElementById('modelTableView'); var noResults = document.getElementById('modelNoResults'); if (searchInput) { searchInput.addEventListener('input', function() { var query = searchInput.value.toLowerCase(); var visible = 0; // Filter grid cards if (gridView) { gridView.querySelectorAll('.model-card').forEach(function(card) { var name = (card.getAttribute('data-name') || '').toLowerCase(); var fw = (card.getAttribute('data-framework') || '').toLowerCase(); var state = (card.getAttribute('data-state') || '').toLowerCase(); if (!query || name.indexOf(query) !== -1 || fw.indexOf(query) !== -1 || state.indexOf(query) !== -1) { card.style.display = ''; visible++; } else { card.style.display = 'none'; } }); } // Filter table rows var table = document.getElementById('modelsTable'); if (table) { table.querySelectorAll('tbody tr').forEach(function(row) { var name = (row.getAttribute('data-name') || '').toLowerCase(); var fw = (row.getAttribute('data-framework') || '').toLowerCase(); var state = (row.getAttribute('data-state') || '').toLowerCase(); if (!query || name.indexOf(query) !== -1 || fw.indexOf(query) !== -1 || state.indexOf(query) !== -1) { row.style.display = ''; } else { row.style.display = 'none'; } }); } if (noResults) { noResults.style.display = visible === 0 ? 'block' : 'none'; } }); } // ── Model view toggle (cards / table) ── window.switchModelView = function(view) { var grid = document.getElementById('modelGridView'); var table = document.getElementById('modelTableView'); if (!grid || !table) return; if (view === 'grid') { grid.style.display = ''; table.style.display = 'none'; } else { grid.style.display = 'none'; table.style.display = ''; } document.querySelectorAll('.model-view-btn').forEach(function(btn) { btn.classList.toggle('active', btn.getAttribute('data-view') === view); }); }; // ── Capability tabs ── document.querySelectorAll('.cap-tab').forEach(function(tab) { tab.addEventListener('click', function() { document.querySelectorAll('.cap-tab').forEach(function(t) { t.classList.remove('active'); }); document.querySelectorAll('.cap-panel').forEach(function(p) { p.classList.remove('active'); }); tab.classList.add('active'); var target = document.getElementById(tab.getAttribute('data-cap')); if (target) target.classList.add('active'); }); }); // ── Snippet tabs ── document.querySelectorAll('.snippet-tab').forEach(function(tab) { tab.addEventListener('click', function() { var panel = tab.closest('.snippet-panel'); panel.querySelectorAll('.snippet-tab').forEach(function(t) { t.classList.remove('active'); }); panel.querySelectorAll('.snippet-content').forEach(function(c) { c.classList.remove('active'); }); tab.classList.add('active'); var target = document.getElementById(tab.getAttribute('data-tab')); if (target) target.classList.add('active'); }); }); }); // ── Copy snippet helper ── function copySnippet(id) { var el = document.getElementById(id); if (!el) return; var pre = el.querySelector('pre'); if (!pre) return; var tmp = document.createElement('div'); tmp.innerHTML = pre.innerHTML; tmp.querySelectorAll('.code-line-num').forEach(function(n) { n.remove(); }); var raw = tmp.textContent || tmp.innerText; navigator.clipboard.writeText(raw.trim()).then(function() { var btn = el.querySelector('.copy-btn'); if (btn) { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 1500); } }); } // ── Copy prometheus text helper ── function copyPrometheusText() { if (typeof _promRaw !== 'undefined') { navigator.clipboard.writeText(_promRaw.trim()).then(function() { var pre = document.getElementById('prometheus-text'); if (!pre) return; var btn = pre.closest('.card').querySelector('.copy-btn'); if (btn) { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 1500); } }); return; } var pre = document.getElementById('prometheus-text'); if (!pre) return; navigator.clipboard.writeText(pre.textContent.trim()).then(function() { var btn = pre.closest('.card').querySelector('.copy-btn'); if (btn) { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 1500); } }); } // ── Prometheus Text Format syntax highlighter ── (function() { if (typeof _promRaw === 'undefined') return; var pre = document.getElementById('prometheus-text'); var lineNumEl = document.getElementById('prom-line-nums'); var lineCountEl = document.getElementById('prom-line-count'); if (!pre) return; function esc(s) { return s.replace(/&/g,'&').replace(//g,'>'); } function highlightPromLine(line) { if (!line.trim()) return ''; // # HELP metric_name description var helpMatch = line.match(/^(# HELP)\s+(\S+)\s*(.*)/); if (helpMatch) { return '' + esc(helpMatch[1]) + ' ' + '' + esc(helpMatch[2]) + '' + (helpMatch[3] ? ' ' + esc(helpMatch[3]) + '' : ''); } // # TYPE metric_name type var typeMatch = line.match(/^(# TYPE)\s+(\S+)\s+(\S+)/); if (typeMatch) { return '' + esc(typeMatch[1]) + ' ' + '' + esc(typeMatch[2]) + ' ' + '' + esc(typeMatch[3]) + ''; } // # comment if (line.match(/^#/)) { return '' + esc(line) + ''; } // metric_name{labels} value [timestamp] // or metric_name value [timestamp] var metricMatch = line.match(/^([a-zA-Z_:][a-zA-Z0-9_:]*)((?:\{[^}]*\})?)\s+([\d.eE+\-]+|NaN|Inf|\+Inf|-Inf)(.*)$/); if (metricMatch) { var out = '' + esc(metricMatch[1]) + ''; // Parse labels var labelStr = metricMatch[2]; if (labelStr && labelStr.length > 2) { out += '{'; var inner = labelStr.slice(1, -1); // Parse label pairs: key="value" var labelRe = /([a-zA-Z_][a-zA-Z0-9_]*)=(\"[^\"]*\"|[^\",}]+)/g; var parts = []; var lm; while ((lm = labelRe.exec(inner)) !== null) { parts.push( '' + esc(lm[1]) + '' + '=' + '' + esc(lm[2]) + '' ); } out += parts.join(','); out += '}'; } out += ' ' + esc(metricMatch[3]) + ''; // Timestamp var rest = metricMatch[4].trim(); if (rest) { out += ' ' + esc(rest) + ''; } return out; } // Fallback return esc(line); } var lines = _promRaw.split('\n'); var highlighted = []; var nums = []; for (var i = 0; i < lines.length; i++) { highlighted.push(highlightPromLine(lines[i])); nums.push('
' + (i + 1) + '
'); } pre.innerHTML = highlighted.join('\n'); if (lineNumEl) lineNumEl.innerHTML = nums.join(''); if (lineCountEl) lineCountEl.textContent = lines.length + ' lines'; })(); // ── Client-side Python syntax highlighter ── (function() { var KW = ['def','class','import','from','return','if','elif','else','for','while','with','as', 'try','except','finally','raise','pass','break','continue','yield','async','await', 'not','and','or','in','is','None','True','False','self','lambda','assert']; var KW_RE = new RegExp('\\b(' + KW.join('|') + ')\\b', 'g'); function escHtml(s) { return s.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); } function highlightLine(line) { var tokens = []; var re3 = /"""[\s\S]*?"""|'''[\s\S]*?'''/g, m; while ((m = re3.exec(line)) !== null) tokens.push([m.index, re3.lastIndex, 'str']); var reS = /"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'/g; while ((m = reS.exec(line)) !== null) tokens.push([m.index, reS.lastIndex, 'str']); var hashIdx = line.indexOf('#'); if (hashIdx !== -1) { var inside = tokens.some(function(t) { return t[0] <= hashIdx && hashIdx < t[1]; }); if (!inside) tokens.push([hashIdx, line.length, 'cmt']); } var reD = /^(\s*)(@\w+)/; m = reD.exec(line); if (m) tokens.push([m.index + m[1].length, m.index + m[1].length + m[2].length, 'dec']); KW_RE.lastIndex = 0; while ((m = KW_RE.exec(line)) !== null) tokens.push([m.index, KW_RE.lastIndex, 'kw']); var reN = /\b\d+\.?\d*\b/g; while ((m = reN.exec(line)) !== null) tokens.push([m.index, reN.lastIndex, 'num']); var reF = /\b(\w+)\(/g; while ((m = reF.exec(line)) !== null) tokens.push([m.index, m.index + m[1].length, 'fn']); tokens.sort(function(a, b) { return a[0] - b[0] || (b[1] - b[0]) - (a[1] - a[0]); }); var merged = [], usedEnd = 0; tokens.forEach(function(t) { if (t[0] < usedEnd) return; merged.push(t); usedEnd = t[1]; }); var parts = [], pos = 0; merged.forEach(function(t) { if (t[0] > pos) parts.push(escHtml(line.substring(pos, t[0]))); parts.push('' + escHtml(line.substring(t[0], t[1])) + ''); pos = t[1]; }); if (pos < line.length) parts.push(escHtml(line.substring(pos))); return parts.join(''); } document.querySelectorAll('pre[data-aq-highlight="python"]').forEach(function(pre) { var raw = pre.textContent; var lines = raw.split('\n'); var html = lines.map(function(line, i) { var num = '' + (i + 1) + ''; return num + highlightLine(line); }).join('\n'); pre.innerHTML = html; }); })(); // ── Drawer helpers ────────────────────────────────────────────── (function() { 'use strict'; var overlay = document.getElementById('aq-drawer-overlay'); var drawer = document.getElementById('aq-drawer'); var title = document.getElementById('drawer-title'); var subtitle = document.getElementById('drawer-subtitle'); var icon = document.getElementById('drawer-icon'); var body = document.getElementById('drawer-body'); if (!overlay || !drawer) return; function esc(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } function makeField(label, value, opts) { opts = opts || {}; var cls = opts.mono ? ' mono' : ''; var full = opts.full ? ' style="grid-column:1/-1;"' : ''; return '
' + '
' + esc(label) + '
' + '
' + (value || '—') + '
'; } function makeBadge(text, cssClass) { return '' + esc(text) + ''; } function openDrawer() { overlay.classList.add('open'); drawer.classList.add('open'); document.body.style.overflow = 'hidden'; } window.closeDrawer = function() { overlay.classList.remove('open'); drawer.classList.remove('open'); document.body.style.overflow = ''; }; document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && drawer.classList.contains('open')) { window.closeDrawer(); } }); // ── Open Model Drawer ──────────────────────────────────────── window.openModelDrawer = function(row) { var d = row.dataset; title.textContent = d.name; subtitle.textContent = 'Model v' + d.version; var isLoaded = d.state === 'loaded'; var isFailed = d.state === 'failed'; icon.style.background = isFailed ? 'rgba(239,68,68,0.12)' : (isLoaded ? 'rgba(34,197,94,0.12)' : 'rgba(59,130,246,0.12)'); icon.style.color = isFailed ? 'var(--danger)' : (isLoaded ? 'var(--accent)' : 'var(--accent)'); icon.innerHTML = isFailed ? '' : ''; var html = ''; // ── Identity ── html += '
'; html += '
Model Identity
'; html += '
'; html += makeField('Name', '' + esc(d.name) + '', {full: true}); html += makeField('Version', esc(d.version)); html += makeField('State', makeBadge(d.state, 'badge-' + d.state)); html += '
'; // ── Runtime ── html += '
'; html += '
Runtime Configuration
'; html += '
'; html += makeField('Framework', esc(d.framework)); html += makeField('Model Type', esc(d.modelType)); html += makeField('Device', esc(d.device)); html += makeField('Runtime', esc(d.runtime)); html += makeField('Active Version', d.active === 'True' ? '✓ Yes' : '✗ No'); html += '
'; // ── Quick Predict ── html += '
'; html += '
Quick Predict
'; html += '
'; html += '# Via orchestrator\n'; html += 'await orchestrator.predict("' + esc(d.name) + '", {"input": data})\n\n'; html += '# HTTP endpoint\n'; html += 'POST /predict/' + esc(d.name) + '\n'; html += '# Stream endpoint\n'; html += 'POST /stream/' + esc(d.name) + ''; html += '
'; body.innerHTML = html; openDrawer(); }; // ── Open Rollout Drawer ────────────────────────────────────── window.openRolloutDrawer = function(row) { var d = row.dataset; title.textContent = 'Rollout ' + d.id.substring(0, 12); subtitle.textContent = d.from + ' → ' + d.to; var isActive = d.phase === 'in_progress'; var isFailed = d.phase === 'failed' || d.phase === 'rolled_back'; icon.style.background = isFailed ? 'rgba(239,68,68,0.12)' : (isActive ? 'rgba(59,130,246,0.12)' : 'rgba(34,197,94,0.12)'); icon.style.color = isFailed ? 'var(--danger)' : (isActive ? '#3b82f6' : 'var(--accent)'); icon.innerHTML = ''; var html = ''; // ── Identity ── html += '
'; html += '
Rollout Identity
'; html += '
'; html += makeField('ID', esc(d.id), {full: true, mono: true}); html += makeField('From Version', '' + esc(d.from) + ''); html += makeField('To Version', '' + esc(d.to) + ''); html += makeField('Strategy', makeBadge(d.strategy, 'badge-' + d.strategy)); html += makeField('Phase', makeBadge(d.phase, 'badge-' + d.phase)); html += '
'; // ── Progress ── html += '
'; html += '
Progress
'; var pct = parseInt(d.percentage, 10) || 0; var pctColor = isFailed ? 'var(--danger)' : (pct >= 100 ? 'var(--success)' : 'var(--accent)'); html += '
'; html += '
'; html += '
'; html += '
'; html += '' + pct + '%'; html += '
'; html += '
'; html += makeField('Steps Completed', esc(d.steps)); html += makeField('Traffic Split', pct + '% new / ' + (100 - pct) + '% old'); html += '
'; // ── Error (if any) ── if (d.error) { html += '
'; html += '
Error
'; html += '
'; html += esc(d.error); html += '
'; } body.innerHTML = html; openDrawer(); }; })(); // ── MLOps API Base URL ────────────────────────────────────────── var MLOPS_BASE = window.location.pathname.replace(/\/$/, ''); function _mlCsrf(){var el=document.querySelector('meta[name="csrf-token"]');return el?el.content:'';} function _mlHeaders(){var h={'Content-Type':'application/json'};var t=_mlCsrf();if(t)h['X-CSRF-Token']=t;return h;} // ── Inference History Data (live — pre-seeded from server render) ── var _inferenceHistory = {{ inference_history|default([])|tojson }}; // ── Record a new entry and refresh the history table ───────────── function recordToHistory(entry) { _inferenceHistory.unshift(entry); if (_inferenceHistory.length > 100) _inferenceHistory.length = 100; renderHistoryTable(); } function renderHistoryTable() { var container = document.getElementById('inference-history-container'); if (!container) return; if (_inferenceHistory.length === 0) { container.innerHTML = '
' + '' + '

No inference history yet

' + '

Use the Live Inference Playground above to test models and build history.

' + '
'; return; } var rows = _inferenceHistory.map(function(h, idx) { var statusClass = h.status === 'ok' ? 'ok' : (h.status === 'compare' ? 'compare' : 'error'); return '' + '' + (h.timestamp || '—') + '' + '
' + '' + '' + (h.model || '—') + '' + '' + (h.version || '?') + '' + '
' + '' + (h.status || 'error') + '' + '' + (h.latency_ms || 0) + 'ms' + '' + (h.user || '—') + '' + '' + ''; }); container.innerHTML = '' + '' + '' + '' + '' + '' + rows.join('') + '' + '
TimestampModelStatusLatencyUserDetails
'; } // ── JSON Syntax Highlighter ───────────────────────────────────── function highlightJSON(raw) { // Escape HTML entities first var s = raw.replace(/&/g, '&').replace(//g, '>'); // Use a token-based approach for robust highlighting var result = ''; var i = 0; while (i < s.length) { var ch = s[i]; // String detection if (ch === '"' || (ch === '&' && s.substr(i, 6) === '"')) { // raw " — find closing quote var start = i; i++; // skip opening " while (i < s.length) { if (s[i] === '\\') { i += 2; continue; } if (s[i] === '"') { i++; break; } i++; } var strToken = s.substring(start, i); // Check if this is a key (followed by optional whitespace + colon) var rest = s.substring(i); var colonMatch = rest.match(/^(\s*):(\s*)/); if (colonMatch) { result += '' + strToken + ''; result += colonMatch[1] + ':' + colonMatch[2]; i += colonMatch[0].length; } else { result += '' + strToken + ''; } } // Number else if (ch === '-' || (ch >= '0' && ch <= '9')) { var numStart = i; if (ch === '-') i++; while (i < s.length && ((s[i] >= '0' && s[i] <= '9') || s[i] === '.' || s[i] === 'e' || s[i] === 'E' || s[i] === '+' || s[i] === '-')) { if ((s[i] === '+' || s[i] === '-') && s[i-1] !== 'e' && s[i-1] !== 'E') break; i++; } result += '' + s.substring(numStart, i) + ''; } // true / false / null else if (s.substr(i, 4) === 'true') { result += 'true'; i += 4; } else if (s.substr(i, 5) === 'false') { result += 'false'; i += 5; } else if (s.substr(i, 4) === 'null') { result += 'null'; i += 4; } // Braces else if (ch === '{' || ch === '}') { result += '' + ch + ''; i++; } // Brackets else if (ch === '[' || ch === ']') { result += '' + ch + ''; i++; } // Comma else if (ch === ',') { result += ','; i++; } // Colon (standalone, not after key — shouldn't happen in valid JSON but just in case) else if (ch === ':') { result += ':'; i++; } // Whitespace and everything else else { result += ch; i++; } } return result; } // Render highlighted JSON into an element (for output panels) function renderJSONHighlighted(el, data) { var text = (typeof data === 'string') ? data : JSON.stringify(data, null, 2); el.innerHTML = highlightJSON(text); } // ── JSON Editor Sync (live syntax highlighting on input) ──────── function syncEditorHighlight(textarea) { var hlId = textarea.getAttribute('data-highlight'); if (!hlId) return; var hl = document.getElementById(hlId); if (!hl) return; var val = textarea.value; // Append a trailing newline so the highlight div matches scrollbar height hl.innerHTML = highlightJSON(val) + '\n'; } function initJSONEditors() { var editors = document.querySelectorAll('.json-editor-textarea'); editors.forEach(function(ta) { // Initial highlight syncEditorHighlight(ta); // Sync on input ta.addEventListener('input', function() { syncEditorHighlight(ta); }); // Sync scroll ta.addEventListener('scroll', function() { var hlId = ta.getAttribute('data-highlight'); var hl = document.getElementById(hlId); if (hl) { hl.scrollTop = ta.scrollTop; hl.scrollLeft = ta.scrollLeft; } }); // Tab key inserts 2 spaces instead of changing focus ta.addEventListener('keydown', function(e) { if (e.key === 'Tab') { e.preventDefault(); var start = ta.selectionStart, end = ta.selectionEnd; ta.value = ta.value.substring(0, start) + ' ' + ta.value.substring(end); ta.selectionStart = ta.selectionEnd = start + 2; syncEditorHighlight(ta); } }); }); } // ── Custom Select Menu ────────────────────────────────────────── function initCustomSelects() { document.querySelectorAll('.aq-sel').forEach(function(sel) { var trigger = sel.querySelector('.aq-sel-trigger'); var menu = sel.querySelector('.aq-sel-menu'); var label = sel.querySelector('.aq-sel-label'); var targetId = sel.getAttribute('data-target'); var hiddenInput = targetId ? document.getElementById(targetId) : null; var searchInput = menu ? menu.querySelector('.aq-sel-search input') : null; // Toggle open/close trigger.addEventListener('click', function(e) { e.stopPropagation(); // Close all other selects first document.querySelectorAll('.aq-sel.open').forEach(function(other) { if (other !== sel) other.classList.remove('open'); }); sel.classList.toggle('open'); if (sel.classList.contains('open') && searchInput) { searchInput.value = ''; searchInput.focus(); // Show all options menu.querySelectorAll('.aq-sel-opt').forEach(function(o) { o.style.display = ''; }); } }); // Keyboard: Enter/Space to toggle, Escape to close trigger.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); trigger.click(); } if (e.key === 'Escape') sel.classList.remove('open'); }); // Option click if (menu) { menu.querySelectorAll('.aq-sel-opt').forEach(function(opt) { opt.addEventListener('click', function(e) { e.stopPropagation(); var val = opt.getAttribute('data-value'); // Update label var optLabel = opt.querySelector('.aq-sel-opt-label'); if (label && optLabel) label.textContent = optLabel.textContent.trim(); // Update hidden input if (hiddenInput) hiddenInput.value = val; // Toggle selected class menu.querySelectorAll('.aq-sel-opt').forEach(function(o) { o.classList.remove('selected'); }); opt.classList.add('selected'); // Close menu sel.classList.remove('open'); }); }); } // Search filtering if (searchInput) { searchInput.addEventListener('input', function() { var q = searchInput.value.toLowerCase(); menu.querySelectorAll('.aq-sel-opt').forEach(function(opt) { var text = (opt.textContent || '').toLowerCase(); opt.style.display = text.indexOf(q) >= 0 ? '' : 'none'; }); }); searchInput.addEventListener('click', function(e) { e.stopPropagation(); }); } }); // Close all selects on outside click document.addEventListener('click', function() { document.querySelectorAll('.aq-sel.open').forEach(function(s) { s.classList.remove('open'); }); }); } // ── Init on load ──────────────────────────────────────────────── (function() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { initCustomSelects(); initJSONEditors(); }); } else { initCustomSelects(); initJSONEditors(); } })(); // ── Live Inference Playground ─────────────────────────────────── function runPlayground() { var btn = document.getElementById('pg-run-btn'); var modelSel = document.getElementById('pg-model-select'); var inputEl = document.getElementById('pg-input'); var outputEl = document.getElementById('pg-output'); var latencyEl = document.getElementById('pg-latency'); if (!btn || !modelSel || !inputEl || !outputEl) return; var model = modelSel.value; var inputText = inputEl.value.trim(); var inputData; try { inputData = JSON.parse(inputText); } catch(e) { outputEl.className = 'playground-output error'; outputEl.textContent = 'Invalid JSON input: ' + e.message; return; } btn.disabled = true; btn.innerHTML = ' Running...'; outputEl.className = 'playground-output'; outputEl.textContent = 'Processing...'; outputEl.style.fontStyle = ''; outputEl.style.color = ''; latencyEl.style.display = 'none'; fetch(MLOPS_BASE + '/api/predict/', { method: 'POST', headers: _mlHeaders(), body: JSON.stringify({model: model, input: inputData}) }) .then(function(r) { return r.json(); }) .then(function(data) { btn.disabled = false; btn.innerHTML = ' Run Inference'; var ts = new Date().toISOString(); if (data.status === 'ok') { outputEl.className = 'playground-output success'; renderJSONHighlighted(outputEl, data.output); latencyEl.style.display = ''; var ms = data.latency_ms || 0; latencyEl.textContent = ms.toFixed(2) + 'ms'; latencyEl.className = 'playground-latency ' + (ms < 50 ? 'fast' : (ms < 200 ? 'medium' : 'slow')); recordToHistory({ model: model, version: data.version || '?', input: inputData, output: data.output, latency_ms: ms, timestamp: ts, status: 'ok', user: 'admin' }); } else { outputEl.className = 'playground-output error'; outputEl.textContent = 'Error: ' + (data.error || 'Unknown error'); recordToHistory({ model: model, version: data.version || '?', input: inputData, output: null, error: data.error || 'Unknown error', latency_ms: 0, timestamp: ts, status: 'error', user: 'admin' }); } }) .catch(function(err) { btn.disabled = false; btn.innerHTML = ' Run Inference'; outputEl.className = 'playground-output error'; outputEl.textContent = 'Network error: ' + err.message; recordToHistory({ model: model, version: '?', input: inputData, output: null, error: 'Network error: ' + err.message, latency_ms: 0, timestamp: new Date().toISOString(), status: 'error', user: 'admin' }); }); } function clearPlayground() { var inputEl = document.getElementById('pg-input'); var outputEl = document.getElementById('pg-output'); var latencyEl = document.getElementById('pg-latency'); var defaultVal = '{\n "text": "sample input"\n}'; if (inputEl) { inputEl.value = defaultVal; syncEditorHighlight(inputEl); } if (outputEl) { outputEl.className = 'playground-output'; outputEl.innerHTML = ''; outputEl.textContent = 'Run inference to see output...'; outputEl.style.fontStyle = 'italic'; outputEl.style.color = 'var(--text-faint)'; } if (latencyEl) latencyEl.style.display = 'none'; } // ── Model Comparison ──────────────────────────────────────────── function runComparison() { var btn = document.getElementById('cmp-run-btn'); var modelA = document.getElementById('cmp-model-a'); var modelB = document.getElementById('cmp-model-b'); var inputEl = document.getElementById('cmp-input'); var resultsEl = document.getElementById('cmp-results'); if (!btn || !modelA || !modelB || !inputEl || !resultsEl) return; var inputData; try { inputData = JSON.parse(inputEl.value.trim()); } catch(e) { resultsEl.style.display = 'block'; resultsEl.innerHTML = '
Invalid JSON: ' + e.message + '
'; return; } var models = [modelA.value, modelB.value]; btn.disabled = true; btn.innerHTML = ' Comparing...'; fetch(MLOPS_BASE + '/api/compare/', { method: 'POST', headers: _mlHeaders(), body: JSON.stringify({models: models, input: inputData}) }) .then(function(r) { return r.json(); }) .then(function(data) { btn.disabled = false; btn.innerHTML = ' Compare'; resultsEl.style.display = 'grid'; var html = ''; var ts = new Date().toISOString(); (data.results || []).forEach(function(r) { var isOk = r.status === 'ok'; var latencyClass = !isOk ? '' : (r.latency_ms < 50 ? 'fast' : (r.latency_ms < 200 ? 'medium' : 'slow')); html += '
'; html += '
' + (r.model || '?'); if (isOk) html += ' ' + r.latency_ms.toFixed(2) + 'ms'; html += '
'; if (isOk) { html += '
' + highlightJSON(JSON.stringify(r.output, null, 2)) + '
'; } else { html += '
Error: ' + (r.error || 'unknown') + '
'; } html += '
'; // Record each model comparison result to history recordToHistory({ model: r.model || '?', version: r.version || '?', input: inputData, output: isOk ? r.output : null, error: isOk ? undefined : (r.error || 'unknown'), latency_ms: r.latency_ms || 0, timestamp: ts, status: isOk ? 'compare' : 'error', user: 'admin' }); }); resultsEl.innerHTML = html; }) .catch(function(err) { btn.disabled = false; btn.innerHTML = ' Compare'; resultsEl.style.display = 'block'; resultsEl.innerHTML = '
Network error: ' + err.message + '
'; }); } // ── Batch Inference ───────────────────────────────────────────── function runBatch() { var btn = document.getElementById('batch-run-btn'); var modelSel = document.getElementById('batch-model-select'); var inputEl = document.getElementById('batch-input'); var progressEl = document.getElementById('batch-progress'); var progressFill = document.getElementById('batch-progress-fill'); var resultsEl = document.getElementById('batch-results'); var resultsBody = document.getElementById('batch-results-body'); var countEl = document.getElementById('batch-count'); var errorsEl = document.getElementById('batch-errors'); var totalLatencyEl = document.getElementById('batch-total-latency'); var avgLatencyEl = document.getElementById('batch-avg-latency'); if (!btn || !modelSel || !inputEl) return; var inputs; try { inputs = JSON.parse(inputEl.value.trim()); if (!Array.isArray(inputs)) throw new Error('Input must be a JSON array'); } catch(e) { if (resultsEl) { resultsEl.style.display = 'block'; } if (resultsBody) { resultsBody.className = 'playground-output error'; resultsBody.textContent = 'Invalid JSON: ' + e.message; } return; } btn.disabled = true; btn.innerHTML = ' Processing...'; if (progressEl) { progressEl.style.display = 'block'; progressFill.style.width = '10%'; } fetch(MLOPS_BASE + '/api/batch-predict/', { method: 'POST', headers: _mlHeaders(), body: JSON.stringify({model: modelSel.value, inputs: inputs}) }) .then(function(r) { return r.json(); }) .then(function(data) { btn.disabled = false; btn.innerHTML = ' Run Batch'; if (data.error) { if (progressEl) progressEl.style.display = 'none'; if (resultsEl) resultsEl.style.display = 'block'; if (resultsBody) { resultsBody.className = 'playground-output error'; resultsBody.textContent = 'Error: ' + data.error; } recordToHistory({ model: modelSel.value, version: '?', input: {batch_size: inputs.length}, output: null, error: data.error, latency_ms: 0, timestamp: new Date().toISOString(), status: 'error', user: 'admin' }); return; } if (progressFill) progressFill.style.width = '100%'; if (countEl) countEl.textContent = data.count || 0; if (errorsEl) errorsEl.textContent = data.errors || 0; if (totalLatencyEl) totalLatencyEl.textContent = (data.total_latency_ms || 0).toFixed(1); if (avgLatencyEl) avgLatencyEl.textContent = (data.avg_latency_ms || 0).toFixed(1); if (resultsEl) resultsEl.style.display = 'block'; if (resultsBody) { resultsBody.className = 'playground-output success'; renderJSONHighlighted(resultsBody, data.results); } recordToHistory({ model: modelSel.value, version: '?', input: {batch_size: data.count || inputs.length}, output: {count: data.count, errors: data.errors, avg_latency_ms: data.avg_latency_ms}, latency_ms: data.total_latency_ms || 0, timestamp: new Date().toISOString(), status: (data.errors || 0) === 0 ? 'ok' : 'error', user: 'admin' }); }) .catch(function(err) { btn.disabled = false; btn.innerHTML = ' Run Batch'; if (progressEl) progressEl.style.display = 'none'; if (resultsEl) resultsEl.style.display = 'block'; if (resultsBody) { resultsBody.className = 'playground-output error'; resultsBody.textContent = 'Network error: ' + err.message; } recordToHistory({ model: modelSel.value, version: '?', input: {batch_size: inputs.length}, output: null, error: 'Network error: ' + err.message, latency_ms: 0, timestamp: new Date().toISOString(), status: 'error', user: 'admin' }); }); } // ── Health Check ──────────────────────────────────────────────── function runHealthCheck() { var btn = document.getElementById('health-check-btn'); if (!btn) return; btn.disabled = true; btn.innerHTML = ' Checking...'; fetch(MLOPS_BASE + '/api/health-check/', { method: 'POST', headers: _mlHeaders(), body: '{}' }) .then(function(r) { return r.json(); }) .then(function(data) { btn.disabled = false; btn.innerHTML = ' Run All Checks'; (data.models || []).forEach(function(m, i) { var card = document.getElementById('health-' + i); var latencyEl = document.getElementById('health-latency-' + i); if (!card) return; var dot = card.querySelector('.health-dot'); var isOk = m.status === 'ok' || m.status === 'healthy' || m.status === 'loaded'; var isErr = m.status === 'error' || m.status === 'failed'; var cls = isOk ? 'healthy' : (isErr ? 'unhealthy' : 'unknown'); card.className = 'health-card ' + cls; if (dot) dot.className = 'health-dot ' + cls; if (latencyEl) latencyEl.textContent = m.latency_ms > 0 ? m.latency_ms.toFixed(2) + 'ms' : m.status; }); }) .catch(function(err) { btn.disabled = false; btn.innerHTML = ' Run All Checks'; }); } // ── Alert Rules ───────────────────────────────────────────────── function _makeAlertSel(uniqueId, targetClass, options, defaultVal, minWidth) { var defLabel = options.filter(function(o){ return o[0] === defaultVal; })[0]; defLabel = defLabel ? defLabel[1] : options[0][1]; var arrowSvg = ''; var checkSvg = ''; var opts = options.map(function(o) { return '
' + '' + o[1] + '' + '' + checkSvg + '
'; }).join(''); return '
' + '
' + '' + defLabel + '' + '' + arrowSvg + '' + '
' + '
' + opts + '
' + '' + '
'; } function _initAlertRowSels(row) { row.querySelectorAll('.aq-sel').forEach(function(sel) { var trigger = sel.querySelector('.aq-sel-trigger'); var menu = sel.querySelector('.aq-sel-menu'); var label = sel.querySelector('.aq-sel-label'); var targetId = sel.getAttribute('data-target'); var hiddenInput = targetId ? document.getElementById(targetId) : null; trigger.addEventListener('click', function(e) { e.stopPropagation(); document.querySelectorAll('.aq-sel.open').forEach(function(other) { if (other !== sel) other.classList.remove('open'); }); sel.classList.toggle('open'); }); trigger.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); trigger.click(); } if (e.key === 'Escape') sel.classList.remove('open'); }); if (menu) { menu.querySelectorAll('.aq-sel-opt').forEach(function(opt) { opt.addEventListener('click', function(e) { e.stopPropagation(); var val = opt.getAttribute('data-value'); var optLabel = opt.querySelector('.aq-sel-opt-label'); if (label && optLabel) label.textContent = optLabel.textContent.trim(); if (hiddenInput) hiddenInput.value = val; menu.querySelectorAll('.aq-sel-opt').forEach(function(o) { o.classList.remove('selected'); }); opt.classList.add('selected'); sel.classList.remove('open'); }); }); } }); } function addAlertRule() { var container = document.getElementById('alert-rules-container'); if (!container) return; // Remove the "no rules" placeholder if present var placeholder = container.querySelector('[style*="text-align:center"]'); if (placeholder && placeholder.querySelector('.icon-bell-off')) placeholder.remove(); var uid = 'ar' + Date.now(); var metricOpts = [ ['error_rate','Error Rate'],['p50_latency','P50 Latency'],['p95_latency','P95 Latency'], ['p99_latency','P99 Latency'],['total_errors','Total Errors'],['drift_score','Drift Score'], ['memory_usage_pct','Memory Usage %'] ]; var operatorOpts = [['>','>'],['>=','≥'],['<','<'],['==','=']]; var severityOpts = [['info','Info'],['warning','Warning'],['critical','Critical']]; var row = document.createElement('div'); row.className = 'alert-rule-row'; row.innerHTML = _makeAlertSel(uid + '-metric', 'alert-metric', metricOpts, 'error_rate', '140px') + _makeAlertSel(uid + '-operator', 'alert-operator', operatorOpts, '>', '70px') + '' + _makeAlertSel(uid + '-severity', 'alert-severity', severityOpts, 'warning', '100px') + ''; container.appendChild(row); _initAlertRowSels(row); } function saveAlertRules() { var container = document.getElementById('alert-rules-container'); if (!container) return; var rows = container.querySelectorAll('.alert-rule-row'); var rules = []; rows.forEach(function(row) { var metricEl = row.querySelector('.alert-metric'); var operatorEl = row.querySelector('.alert-operator'); var severityEl = row.querySelector('.alert-severity'); rules.push({ metric: metricEl ? metricEl.value : 'error_rate', operator: operatorEl ? operatorEl.value : '>', threshold: parseFloat(row.querySelector('.alert-threshold').value) || 0, severity: severityEl ? severityEl.value : 'warning', enabled: true, }); }); fetch(MLOPS_BASE + '/api/alerts/', { method: 'POST', headers: _mlHeaders(), body: JSON.stringify({rules: rules}) }) .then(function(r) { return r.json(); }) .then(function(data) { var triggered = data.triggered || []; if (triggered.length > 0) { alert('⚠️ ' + triggered.length + ' alert(s) triggered!\n\n' + triggered.map(function(a) { return a.metric + ' ' + a.operator + ' ' + a.threshold + ' (current: ' + a.current_value + ')'; }).join('\n')); } else { // Brief visual feedback var btn = container.closest('.card').querySelector('.playground-btn:last-of-type'); if (btn) { var orig = btn.innerHTML; btn.innerHTML = ' Saved'; setTimeout(function() { btn.innerHTML = orig; }, 1500); } } }) .catch(function(err) { alert('Error saving alert rules: ' + err.message); }); } // ── Inference History Detail View ─────────────────────────────── function viewHistoryDetail(idx) { var item = _inferenceHistory[idx]; if (!item) return; var title = document.getElementById('drawer-title'); var subtitle = document.getElementById('drawer-subtitle'); var icon = document.getElementById('drawer-icon'); var body = document.getElementById('drawer-body'); var overlay = document.getElementById('aq-drawer-overlay'); var drawer = document.getElementById('aq-drawer'); if (!title || !body || !overlay || !drawer) return; title.textContent = 'Inference Detail'; subtitle.textContent = item.model + ' · ' + (item.timestamp || ''); icon.style.background = item.status === 'ok' ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)'; icon.style.color = item.status === 'ok' ? 'var(--accent)' : 'var(--danger)'; icon.innerHTML = item.status === 'ok' ? '' : ''; function esc(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } var html = '
'; html += '
Request Metadata
'; html += '
'; html += '
Model
' + esc(item.model) + '
'; html += '
Version
' + esc(item.version || '?') + '
'; html += '
Status
' + esc(item.status || 'error') + '
'; html += '
Latency
' + (item.latency_ms || 0) + 'ms
'; html += '
User
' + esc(item.user || '—') + '
'; html += '
Timestamp
' + esc(item.timestamp || '—') + '
'; html += '
'; html += '
'; html += '
Input
'; html += '
'; html += highlightJSON(JSON.stringify(item.input, null, 2)); html += '
'; html += '
'; html += '
' + (item.status === 'ok' ? 'Output' : 'Error') + '
'; if (item.status === 'ok') { html += '
'; html += highlightJSON(JSON.stringify(item.output, null, 2)); } else { html += '
'; html += esc(item.error || 'Unknown error'); } html += '
'; body.innerHTML = html; overlay.classList.add('open'); drawer.classList.add('open'); document.body.style.overflow = 'hidden'; } // ── Export Snapshot ────────────────────────────────────────────── function exportSnapshot() { fetch(MLOPS_BASE + '/api/export-snapshot/', { method: 'POST', headers: _mlHeaders(), body: '{}' }) .then(function(r) { return r.blob(); }) .then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'aquilia-mlops-snapshot-' + new Date().toISOString().split('T')[0] + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }) .catch(function(err) { alert('Export failed: ' + err.message); }); } function copyMetricsClipboard() { fetch(MLOPS_BASE + '/api/') .then(function(r) { return r.json(); }) .then(function(data) { var text = JSON.stringify(data, null, 2); navigator.clipboard.writeText(text).then(function() { alert('✅ MLOps metrics copied to clipboard'); }); }) .catch(function(err) { alert('Copy failed: ' + err.message); }); } // ── Auto-refresh polling ──────────────────────────────────────── (function() { var POLL_INTERVAL = 15000; var API_URL = window.location.pathname.replace(/\/$/, '') + '/api/'; function refreshMetrics() { fetch(API_URL).then(function(r) { return r.json(); }).then(function(data) { // Update stat cards var cards = document.querySelectorAll('[data-count]'); var countMap = { 0: data.total_models, 1: data.total_inferences, 3: data.total_tokens, 4: data.total_stream_requests, }; cards.forEach(function(el, i) { if (countMap[i] !== undefined) { el.textContent = (countMap[i] || 0).toLocaleString(); } }); }).catch(function() { /* silent */ }); } setInterval(refreshMetrics, POLL_INTERVAL); })(); {% endblock %}