Base design
This commit is contained in:
0
.dockerignore
Normal file
0
.dockerignore
Normal file
227
.gitignore
vendored
227
.gitignore
vendored
@@ -1,216 +1,23 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
# Python
|
||||||
|
*.pyc
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[codz]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py.cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
# Pipfile.lock
|
|
||||||
|
|
||||||
# UV
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# uv.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
# poetry.lock
|
|
||||||
# poetry.toml
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
||||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
||||||
# pdm.lock
|
|
||||||
# pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# pixi
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
||||||
# pixi.lock
|
|
||||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
||||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
||||||
.pixi
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
*.rdb
|
|
||||||
*.aof
|
|
||||||
*.pid
|
|
||||||
|
|
||||||
# RabbitMQ
|
|
||||||
mnesia/
|
|
||||||
rabbitmq/
|
|
||||||
rabbitmq-data/
|
|
||||||
|
|
||||||
# ActiveMQ
|
|
||||||
activemq-data/
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.envrc
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
.env
|
||||||
env.bak/
|
db.sqlite3
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
# Django
|
||||||
.spyderproject
|
media/
|
||||||
.spyproject
|
staticfiles/
|
||||||
|
|
||||||
# Rope project settings
|
# Compiled CSS
|
||||||
.ropeproject
|
watcher_visio/static/css/output.css
|
||||||
|
watcher_visio/static/css/tailwindcss
|
||||||
|
watcher_visio/static/css/tailwindcss.exe
|
||||||
|
|
||||||
# mkdocs documentation
|
# IDE
|
||||||
/site
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
# mypy
|
# DaisyUI
|
||||||
.mypy_cache/
|
static/css/output.css
|
||||||
.dmypy.json
|
static/css/tailwindcss
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
# .idea/
|
|
||||||
|
|
||||||
# Abstra
|
|
||||||
# Abstra is an AI-powered process automation framework.
|
|
||||||
# Ignore directories containing user credentials, local state, and settings.
|
|
||||||
# Learn more at https://abstra.io/docs
|
|
||||||
.abstra/
|
|
||||||
|
|
||||||
# Visual Studio Code
|
|
||||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
||||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
||||||
# you could uncomment the following to ignore the entire vscode folder
|
|
||||||
# .vscode/
|
|
||||||
|
|
||||||
# Ruff stuff:
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# PyPI configuration file
|
|
||||||
.pypirc
|
|
||||||
|
|
||||||
# Marimo
|
|
||||||
marimo/_static/
|
|
||||||
marimo/_lsp/
|
|
||||||
__marimo__/
|
|
||||||
|
|
||||||
# Streamlit
|
|
||||||
.streamlit/secrets.toml
|
|
||||||
14
Dockerfile
14
Dockerfile
@@ -1,4 +1,12 @@
|
|||||||
FROM alpine:3 AS build
|
FROM node:25-alpine AS node-build-stage
|
||||||
|
|
||||||
|
COPY ./watcher_visio/static ./
|
||||||
|
RUN npx tailwindcss \
|
||||||
|
-i input.css \
|
||||||
|
-o ./tailwind.css --minify
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:3 AS venv-build-stage
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --virtual .build-deps \
|
apk add --no-cache --virtual .build-deps \
|
||||||
@@ -25,7 +33,7 @@ RUN apk add --no-cache --update python3
|
|||||||
COPY --from=build /venv /venv
|
COPY --from=build /venv /venv
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
COPY ./watcher_visio/ /app
|
COPY --from=build /app /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD [ "python", "manage.py", "runserver", "0.0.0.0:5000" ]
|
CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]
|
||||||
@@ -2,6 +2,6 @@ services:
|
|||||||
watcher-visio:
|
watcher-visio:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./watcher_visio:/app
|
- ./watcher_visio:/app
|
||||||
12
docker-entrypoint.sh
Normal file
12
docker-entrypoint.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Applying database migrations..."
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
|
||||||
|
echo "Collecting static files..."
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
echo "Starting Django application..."
|
||||||
|
exec "$@"
|
||||||
0
watcher_visio/dashboard/templatetags/__init__.py
Normal file
0
watcher_visio/dashboard/templatetags/__init__.py
Normal file
17
watcher_visio/dashboard/templatetags/mathfilters.py
Normal file
17
watcher_visio/dashboard/templatetags/mathfilters.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def div(a, b):
|
||||||
|
try:
|
||||||
|
return float(a) / float(b)
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def mul(a, b):
|
||||||
|
try:
|
||||||
|
return float(a) * float(b)
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
6
watcher_visio/dashboard/urls.py
Normal file
6
watcher_visio/dashboard/urls.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.index, name='index'),
|
||||||
|
]
|
||||||
174
watcher_visio/dashboard/views.py
Normal file
174
watcher_visio/dashboard/views.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import json
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
_BASE = {
|
||||||
|
"region_name": "ct3k1ldt"
|
||||||
|
}
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
context = {**_BASE,
|
||||||
|
# CPU and RAM utilization data
|
||||||
|
'cpu_used': 42,
|
||||||
|
'cpu_free': 58,
|
||||||
|
'cpu_used_percentage': 42.0,
|
||||||
|
'ram_used': 128,
|
||||||
|
'ram_free': 256,
|
||||||
|
'ram_used_percentage': 33.3,
|
||||||
|
|
||||||
|
# Instance summary data
|
||||||
|
'vm_count': 47,
|
||||||
|
'vm_active': 42,
|
||||||
|
'vm_stopped': 5,
|
||||||
|
'vm_error': 0,
|
||||||
|
'common_flavor': 'm1.medium',
|
||||||
|
'common_flavor_count': 18,
|
||||||
|
'second_common_flavor': {
|
||||||
|
'name': 'm1.small',
|
||||||
|
'count': 12
|
||||||
|
},
|
||||||
|
'third_common_flavor': {
|
||||||
|
'name': 'm1.large',
|
||||||
|
'count': 8
|
||||||
|
},
|
||||||
|
|
||||||
|
# Resource allocation data
|
||||||
|
'cpu_allocated': 94,
|
||||||
|
'cpu_total': 160,
|
||||||
|
'cpu_overcommit_ratio': 1.5,
|
||||||
|
'ram_allocated': 384,
|
||||||
|
'ram_total': 512,
|
||||||
|
'ram_overcommit_ratio': 1.2,
|
||||||
|
|
||||||
|
# Quick stats
|
||||||
|
'avg_cpu_per_vm': 2.0,
|
||||||
|
'avg_ram_per_vm': 8.2,
|
||||||
|
'vm_density': 9.4,
|
||||||
|
|
||||||
|
# Audit data
|
||||||
|
'audits': [
|
||||||
|
{
|
||||||
|
'id': 'audit_001',
|
||||||
|
'name': 'Weekly Optimization',
|
||||||
|
'created_at': '2024-01-15',
|
||||||
|
'cpu_weight': 1.2,
|
||||||
|
'ram_weight': 0.8,
|
||||||
|
'scope': 'Full Cluster',
|
||||||
|
'strategy': 'Load Balancing',
|
||||||
|
'goal': 'Optimize CPU distribution across all hosts',
|
||||||
|
'migrations': [
|
||||||
|
{
|
||||||
|
'instanceName': 'web-server-01',
|
||||||
|
'source': 'compute-02',
|
||||||
|
'destination': 'compute-05',
|
||||||
|
'flavor': 'm1.medium',
|
||||||
|
'impact': 'Low'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'instanceName': 'db-replica-03',
|
||||||
|
'source': 'compute-01',
|
||||||
|
'destination': 'compute-04',
|
||||||
|
'flavor': 'm1.large',
|
||||||
|
'impact': 'Medium'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'instanceName': 'api-gateway',
|
||||||
|
'source': 'compute-03',
|
||||||
|
'destination': 'compute-06',
|
||||||
|
'flavor': 'm1.small',
|
||||||
|
'impact': 'Low'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'instanceName': 'cache-node-02',
|
||||||
|
'source': 'compute-01',
|
||||||
|
'destination': 'compute-07',
|
||||||
|
'flavor': 'm1.small',
|
||||||
|
'impact': 'Low'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'instanceName': 'monitoring-server',
|
||||||
|
'source': 'compute-04',
|
||||||
|
'destination': 'compute-02',
|
||||||
|
'flavor': 'm1.medium',
|
||||||
|
'impact': 'Low'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'host_labels': ['compute-01', 'compute-02', 'compute-03', 'compute-04', 'compute-05', 'compute-06', 'compute-07'],
|
||||||
|
'cpu_current': [78, 65, 42, 89, 34, 56, 71],
|
||||||
|
'cpu_projected': [65, 58, 45, 72, 48, 61, 68]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'audit_002',
|
||||||
|
'name': 'Emergency Rebalance',
|
||||||
|
'created_at': '2024-01-14',
|
||||||
|
'cpu_weight': 1.0,
|
||||||
|
'ram_weight': 1.0,
|
||||||
|
'scope': 'Overloaded Hosts',
|
||||||
|
'strategy': 'Hotspot Reduction',
|
||||||
|
'goal': 'Reduce load on compute-01 and compute-04',
|
||||||
|
'migrations': [
|
||||||
|
{
|
||||||
|
'instanceName': 'app-server-02',
|
||||||
|
'source': 'compute-01',
|
||||||
|
'destination': 'compute-06',
|
||||||
|
'flavor': 'm1.medium',
|
||||||
|
'impact': 'Medium'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'instanceName': 'file-server-01',
|
||||||
|
'source': 'compute-04',
|
||||||
|
'destination': 'compute-07',
|
||||||
|
'flavor': 'm1.large',
|
||||||
|
'impact': 'High'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'host_labels': ['compute-01', 'compute-02', 'compute-03', 'compute-04', 'compute-05', 'compute-06', 'compute-07'],
|
||||||
|
'cpu_current': [92, 65, 42, 85, 34, 56, 71],
|
||||||
|
'cpu_projected': [72, 65, 42, 65, 34, 66, 81]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'audit_003',
|
||||||
|
'name': 'Pre-Maintenance Planning',
|
||||||
|
'created_at': '2024-01-10',
|
||||||
|
'cpu_weight': 0.8,
|
||||||
|
'ram_weight': 1.5,
|
||||||
|
'scope': 'Maintenance Zone',
|
||||||
|
'strategy': 'Evacuation',
|
||||||
|
'goal': 'Empty compute-03 for maintenance',
|
||||||
|
'migrations': [
|
||||||
|
{
|
||||||
|
'instanceName': 'test-vm-01',
|
||||||
|
'source': 'compute-03',
|
||||||
|
'destination': 'compute-02',
|
||||||
|
'flavor': 'm1.small',
|
||||||
|
'impact': 'Low'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'instanceName': 'dev-server',
|
||||||
|
'source': 'compute-03',
|
||||||
|
'destination': 'compute-05',
|
||||||
|
'flavor': 'm1.medium',
|
||||||
|
'impact': 'Low'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'instanceName': 'staging-db',
|
||||||
|
'source': 'compute-03',
|
||||||
|
'destination': 'compute-07',
|
||||||
|
'flavor': 'm1.large',
|
||||||
|
'impact': 'High'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'host_labels': ['compute-01', 'compute-02', 'compute-03', 'compute-04', 'compute-05', 'compute-06', 'compute-07'],
|
||||||
|
'cpu_current': [78, 65, 56, 89, 34, 56, 71],
|
||||||
|
'cpu_projected': [78, 75, 0, 89, 54, 56, 81]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serialize lists for JavaScript
|
||||||
|
for audit in context['audits']:
|
||||||
|
audit['migrations'] = json.dumps(audit['migrations'])
|
||||||
|
audit['host_labels'] = json.dumps(audit['host_labels'])
|
||||||
|
audit['cpu_current'] = json.dumps(audit['cpu_current'])
|
||||||
|
audit['cpu_projected'] = json.dumps(audit['cpu_projected'])
|
||||||
|
|
||||||
|
return render(request, 'index.html', context)
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title>Prometheus dashboard</title>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Prometheus dashboard</h1>
|
|
||||||
<div>
|
|
||||||
<label>Metric: <input id="metric" value="{{ default_metric }}"/></label>
|
|
||||||
<button id="reload">Reload</button>
|
|
||||||
<a id="pdfLink" href="/report/pdf/?metric={{ default_metric }}" target="_blank">Download PDF</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<canvas id="chart" width="900" height="400"></canvas>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const ctx = document.getElementById('chart').getContext('2d');
|
|
||||||
let chart = null;
|
|
||||||
async function loadData() {
|
|
||||||
const metric = document.getElementById('metric').value;
|
|
||||||
const res = await fetch(`/api/metrics/?metric=${encodeURIComponent(metric)}`);
|
|
||||||
const payload = await res.json();
|
|
||||||
const labels = (payload.labels || []).map(ts => new Date(ts));
|
|
||||||
const datasets = (payload.datasets || []).map((d, i) => ({
|
|
||||||
label: d.label,
|
|
||||||
data: d.data.map((v, idx) => ({x: labels[idx], y: v})),
|
|
||||||
fill: false,
|
|
||||||
// Chart.js will auto pick colors
|
|
||||||
}));
|
|
||||||
if (chart) chart.destroy();
|
|
||||||
chart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: { datasets },
|
|
||||||
options: {
|
|
||||||
parsing: false,
|
|
||||||
scales: {
|
|
||||||
x: { type: 'time', time: { unit: 'minute' } },
|
|
||||||
y: { beginAtZero: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// update PDF link
|
|
||||||
document.getElementById('pdfLink').href = `/report/pdf/?metric=${encodeURIComponent(metric)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('reload').addEventListener('click', loadData);
|
|
||||||
loadData();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("", views.dashboard, name="dashboard"),
|
|
||||||
path("api/metrics/", views.metrics_api, name="metrics_api"),
|
|
||||||
path("report/pdf/", views.report_pdf, name="report_pdf"),
|
|
||||||
]
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from django.conf import settings
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.http import JsonResponse, HttpResponse
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
|
|
||||||
# Helper: query Prometheus HTTP API (query_range)
|
|
||||||
def query_prometheus_range(query, start, end, step="60s"):
|
|
||||||
url = settings.PROMETHEUS_URL.rstrip("/") + "/api/v1/query_range"
|
|
||||||
params = {"query": query, "start": start, "end": end, "step": step}
|
|
||||||
r = requests.get(url, params=params, timeout=10)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
# API endpoint used by Chart.js frontend
|
|
||||||
def metrics_api(request):
|
|
||||||
# get parameters or default (last 1 hour)
|
|
||||||
metric = request.GET.get("metric", settings.PROMETHEUS_DEFAULT_METRIC)
|
|
||||||
now = int(time.time())
|
|
||||||
start = request.GET.get("start", str(now - 3600)) # unix epoch seconds
|
|
||||||
end = request.GET.get("end", str(now))
|
|
||||||
step = request.GET.get("step", "60s")
|
|
||||||
|
|
||||||
# Example: if the metric is a gauge giving bytes, we may want to convert ... keep raw for now
|
|
||||||
q = metric
|
|
||||||
data = query_prometheus_range(q, start, end, step)
|
|
||||||
# Prometheus returns JSON; keep it minimal for Chart.js: {labels: [...], datasets: [{label,...,data:[...]}]}
|
|
||||||
series = []
|
|
||||||
labels = []
|
|
||||||
datasets = []
|
|
||||||
|
|
||||||
if data.get("status") != "success":
|
|
||||||
return JsonResponse({"error": "prometheus error", "detail": data})
|
|
||||||
|
|
||||||
result = data["data"]["result"] # list of time series
|
|
||||||
# if no series, return empty
|
|
||||||
if not result:
|
|
||||||
return JsonResponse({"labels": [], "datasets": []})
|
|
||||||
|
|
||||||
# Build labels from first series timestamps
|
|
||||||
# Prometheus returns values as [[ts, value], ...]
|
|
||||||
first_values = result[0]["values"]
|
|
||||||
labels = [int(float(t[0])) * 1000 for t in first_values] # JS prefers ms
|
|
||||||
for s in result:
|
|
||||||
# create dataset for each timeseries (label from metric labels)
|
|
||||||
metric_labels = s.get("metric", {})
|
|
||||||
label = metric_labels.get("instance") or metric_labels.get("domain") or json.dumps(metric_labels)
|
|
||||||
values = [float(v[1]) if v[1] != "NaN" else None for v in s["values"]]
|
|
||||||
datasets.append({
|
|
||||||
"label": label,
|
|
||||||
"data": values,
|
|
||||||
})
|
|
||||||
|
|
||||||
return JsonResponse({"labels": labels, "datasets": datasets})
|
|
||||||
|
|
||||||
# Dashboard page (Jinja template)
|
|
||||||
def dashboard(request):
|
|
||||||
# let template ask API for data with JS.
|
|
||||||
return render(request, "dashboard.html", {
|
|
||||||
"default_metric": settings.PROMETHEUS_DEFAULT_METRIC,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Render page to PDF using WeasyPrint
|
|
||||||
def report_pdf(request):
|
|
||||||
# optionally accept ?metric=...&start=...&end=...
|
|
||||||
metric = request.GET.get("metric", settings.PROMETHEUS_DEFAULT_METRIC)
|
|
||||||
now = int(time.time())
|
|
||||||
start = int(request.GET.get("start", now - 3600))
|
|
||||||
end = int(request.GET.get("end", now))
|
|
||||||
|
|
||||||
# fetch data server-side to include in report
|
|
||||||
try:
|
|
||||||
resp = query_prometheus_range(metric, start, end, step="60s")
|
|
||||||
except Exception as e:
|
|
||||||
return HttpResponse(f"Error fetching metrics: {e}", status=500)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"metric": metric,
|
|
||||||
"prom_data": resp.get("data", {}),
|
|
||||||
"generated_at": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()),
|
|
||||||
}
|
|
||||||
html = render_to_string("report.html", context)
|
|
||||||
|
|
||||||
# Generate PDF via WeasyPrint
|
|
||||||
from weasyprint import HTML
|
|
||||||
pdf = HTML(string=html, base_url=request.build_absolute_uri("/")).write_pdf()
|
|
||||||
return HttpResponse(pdf, content_type="application/pdf")
|
|
||||||
@@ -1,41 +1,34 @@
|
|||||||
asgiref==3.11.0
|
asgiref==3.11.0
|
||||||
brotli==1.2.0
|
|
||||||
certifi==2025.11.12
|
certifi==2025.11.12
|
||||||
cffi==2.0.0
|
cffi==2.0.0
|
||||||
charset-normalizer==3.4.4
|
charset-normalizer==3.4.4
|
||||||
cryptography==46.0.3
|
cryptography==46.0.3
|
||||||
cssselect2==0.8.0
|
|
||||||
decorator==5.2.1
|
decorator==5.2.1
|
||||||
Django==5.2.8
|
Django==5.2.8
|
||||||
dogpile.cache==1.5.0
|
dogpile.cache==1.5.0
|
||||||
fonttools==4.60.1
|
|
||||||
idna==3.11
|
idna==3.11
|
||||||
iso8601==2.1.0
|
iso8601==2.1.0
|
||||||
Jinja2==3.1.6
|
|
||||||
jmespath==1.0.1
|
jmespath==1.0.1
|
||||||
jsonpatch==1.33
|
jsonpatch==1.33
|
||||||
jsonpointer==3.0.0
|
jsonpointer==3.0.0
|
||||||
keystoneauth1==5.12.0
|
keystoneauth1==5.12.0
|
||||||
MarkupSafe==3.0.3
|
numpy==2.3.5
|
||||||
openstacksdk==4.8.0
|
openstacksdk==4.8.0
|
||||||
os-service-types==1.8.2
|
os-service-types==1.8.2
|
||||||
|
pandas==2.3.3
|
||||||
pbr==7.0.3
|
pbr==7.0.3
|
||||||
pillow==12.0.0
|
|
||||||
platformdirs==4.5.0
|
platformdirs==4.5.0
|
||||||
psutil==7.1.3
|
psutil==7.1.3
|
||||||
pycparser==2.23
|
pycparser==2.23
|
||||||
pydyf==0.11.0
|
python-dateutil==2.9.0.post0
|
||||||
pyphen==0.17.2
|
pytz==2025.2
|
||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
requestsexceptions==1.4.0
|
requestsexceptions==1.4.0
|
||||||
sqlparse==0.5.3
|
setuptools==80.9.0
|
||||||
|
six==1.17.0
|
||||||
|
sqlparse==0.5.4
|
||||||
stevedore==5.6.0
|
stevedore==5.6.0
|
||||||
tinycss2==1.5.1
|
|
||||||
tinyhtml5==2.0.0
|
|
||||||
typing_extensions==4.15.0
|
typing_extensions==4.15.0
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
weasyprint==66.0
|
|
||||||
webencodings==0.5.1
|
|
||||||
zopfli==0.4.0
|
|
||||||
|
|||||||
124
watcher_visio/static/css/daisyui-theme.js
Normal file
124
watcher_visio/static/css/daisyui-theme.js
Normal file
File diff suppressed because one or more lines are too long
1057
watcher_visio/static/css/daisyui.js
Normal file
1057
watcher_visio/static/css/daisyui.js
Normal file
File diff suppressed because one or more lines are too long
3
watcher_visio/static/css/input.css
Normal file
3
watcher_visio/static/css/input.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@import "tailwindcss" source(none);
|
||||||
|
@plugin "./daisyui.js";
|
||||||
|
@source "../../templates";
|
||||||
9
watcher_visio/static/js/analytics/audit.js
Normal file
9
watcher_visio/static/js/analytics/audit.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Audit-related JavaScript functions
|
||||||
|
function initializeAuditSelector() {
|
||||||
|
const selector = document.getElementById('auditSelector');
|
||||||
|
if (selector && selector.options.length > 0) {
|
||||||
|
selector.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export other audit-related functions...
|
||||||
55
watcher_visio/static/js/analytics/charts.js
Normal file
55
watcher_visio/static/js/analytics/charts.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Chart configuration and initialization
|
||||||
|
const chartConfig = {
|
||||||
|
cutout: "60%",
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return `${context.label}: ${context.parsed} vCPU`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
animateScale: true,
|
||||||
|
animateRotate: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function initializeUtilizationCharts(cpuMean, ramMean) {
|
||||||
|
const cpuCtx = document.getElementById("cpuChart").getContext('2d');
|
||||||
|
const ramCtx = document.getElementById("ramChart").getContext('2d');
|
||||||
|
|
||||||
|
new Chart(cpuCtx, {
|
||||||
|
type: "doughnut",
|
||||||
|
data: {
|
||||||
|
labels: ["Used", "Free"],
|
||||||
|
datasets: [{
|
||||||
|
data: [{{ cpu_used }}, {{ cpu_free }}],
|
||||||
|
backgroundColor: ["#3b82f6", "#e5e7eb"],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#ffffff"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: chartConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(ramCtx, {
|
||||||
|
type: "doughnut",
|
||||||
|
data: {
|
||||||
|
labels: ["Used", "Free"],
|
||||||
|
datasets: [{
|
||||||
|
data: [{{ ram_used }}, {{ ram_free }}],
|
||||||
|
backgroundColor: ["#f97316", "#e5e7eb"],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#ffffff"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: chartConfig
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export other chart-related functions...
|
||||||
12
watcher_visio/static/js/analytics/utils.js
Normal file
12
watcher_visio/static/js/analytics/utils.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Utility functions
|
||||||
|
function calculateStats(data) {
|
||||||
|
if (!data || data.length === 0) return { mean: 0, std: 0 };
|
||||||
|
|
||||||
|
const mean = data.reduce((a, b) => a + b, 0) / data.length;
|
||||||
|
const variance = data.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / data.length;
|
||||||
|
const std = Math.sqrt(variance);
|
||||||
|
|
||||||
|
return { mean, std };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export other utility functions...
|
||||||
14
watcher_visio/static/js/chart.js
Normal file
14
watcher_visio/static/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
7
watcher_visio/static/js/chartjs-plugin-annotation.min.js
vendored
Normal file
7
watcher_visio/static/js/chartjs-plugin-annotation.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
0
watcher_visio/static/js/chartjs-plugin-datalabels.min.js
vendored
Normal file
0
watcher_visio/static/js/chartjs-plugin-datalabels.min.js
vendored
Normal file
64
watcher_visio/templates/base.html
Normal file
64
watcher_visio/templates/base.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Watcher Visio{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/output.css' %}">
|
||||||
|
{% block imports %}
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Navbar -->
|
||||||
|
<div class="navbar bg-base-100 shadow-lg">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="btn btn-ghost text-xl" href="{% url 'index' %}">Watcher Visio</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-center hidden lg:flex">
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
<div class="px-1 flex gap-3 pr-10">
|
||||||
|
<span class="badge badge-primary badge-lg">{{ region_name }}</span>
|
||||||
|
<label class="swap swap-rotate">
|
||||||
|
<input type="checkbox" class="theme-controller" value="dark" />
|
||||||
|
<svg class="swap-off fill-current w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="swap-on fill-current w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"/>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="container mx-auto px-4 py-8 min-h-screen">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Function to apply theme
|
||||||
|
function applyTheme(theme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
const checkbox = document.querySelector('.theme-controller');
|
||||||
|
checkbox.checked = (theme === 'dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved theme from localStorage
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
applyTheme(savedTheme);
|
||||||
|
|
||||||
|
// Listen for toggle changes
|
||||||
|
document.querySelector('.theme-controller').addEventListener('change', function() {
|
||||||
|
const newTheme = this.checked ? 'dark' : 'light';
|
||||||
|
applyTheme(newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% block script %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
866
watcher_visio/templates/index.html
Normal file
866
watcher_visio/templates/index.html
Normal file
@@ -0,0 +1,866 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static mathfilters %}
|
||||||
|
|
||||||
|
{% block title %}System Analytics - Django + DaisyUI{% endblock %}
|
||||||
|
|
||||||
|
{% block imports %}
|
||||||
|
<script src="{% static 'js/chart.js' %}"></script>
|
||||||
|
<script src="{% static 'js/chartjs-plugin-datalabels.min.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- MAIN GRID -->
|
||||||
|
<div class="p-4 grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<!-- CPU Utilization Card -->
|
||||||
|
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h2 class="card-title text-sm font-semibold">CPU Utilization</h2>
|
||||||
|
<div class="text-xs badge badge-ghost">{{ cpu_used }} / {{ cpu_used|add:cpu_free }} vCPU</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative h-40 flex items-center justify-center">
|
||||||
|
<canvas id="cpuChart"></canvas>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold text-primary">{{ cpu_used_percentage|floatformat:1 }}%</div>
|
||||||
|
<div class="text-xs text-base-content/60">Used</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs mt-2">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-primary"></span>
|
||||||
|
Used: {{ cpu_used }} vCPU
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-base-300"></span>
|
||||||
|
Free: {{ cpu_free }} vCPU
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RAM Utilization Card -->
|
||||||
|
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h2 class="card-title text-sm font-semibold">RAM Utilization</h2>
|
||||||
|
<div class="text-xs badge badge-ghost">{{ ram_used }} / {{ ram_used|add:ram_free }} GB</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative h-40 flex items-center justify-center">
|
||||||
|
<canvas id="ramChart"></canvas>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold text-secondary">{{ ram_used_percentage|floatformat:1 }}%</div>
|
||||||
|
<div class="text-xs text-base-content/60">Used</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs mt-2">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-secondary"></span>
|
||||||
|
Used: {{ ram_used }} GB
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-base-300"></span>
|
||||||
|
Free: {{ ram_free }} GB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instance Summary Card - IMPROVED -->
|
||||||
|
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow col-span-1 lg:col-span-2">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="card-title text-lg font-semibold flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
|
</svg>
|
||||||
|
Instance Summary
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<!-- VM Statistics -->
|
||||||
|
<div class="bg-base-200 rounded-xl p-4 space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-base-content/70">Total Instances</div>
|
||||||
|
<div class="text-3xl font-bold text-primary mt-1">{{ vm_count }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-primary opacity-80">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-xs text-base-content/60">Active</span>
|
||||||
|
<span class="font-semibold">{{ vm_active|default:"N/A" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-xs text-base-content/60">Stopped</span>
|
||||||
|
<span class="font-semibold">{{ vm_stopped|default:"N/A" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-xs text-base-content/60">Error</span>
|
||||||
|
<span class="font-semibold">{{ vm_error|default:"0" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Flavor -->
|
||||||
|
<div class="bg-base-200 rounded-xl p-4 space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-base-content/70">Most Used Flavor</div>
|
||||||
|
<div class="text-2xl font-bold text-secondary mt-1">{{ common_flavor }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-secondary opacity-80">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between bg-base-300/50 p-2 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-base-content/60">Instances</div>
|
||||||
|
<div class="font-bold">{{ common_flavor_count }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-xs text-base-content/60">Share</div>
|
||||||
|
<div class="font-bold">
|
||||||
|
{{ common_flavor_count|div:vm_count|mul:100|floatformat:0 }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if second_common_flavor %}
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-base-content/60">2nd:</span>
|
||||||
|
<span class="font-medium">{{ second_common_flavor.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs badge badge-outline">{{ second_common_flavor.count }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if third_common_flavor %}
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-base-content/60">3rd:</span>
|
||||||
|
<span class="font-medium">{{ third_common_flavor.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs badge badge-outline">{{ third_common_flavor.count }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resource Allocation -->
|
||||||
|
<div class="bg-base-200 rounded-xl p-4 space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-base-content/70">Resource Allocation</div>
|
||||||
|
<div class="text-xs text-base-content/60">Provisioned vs Total</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-warning opacity-80">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<!-- CPU Allocation -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-primary"></span>
|
||||||
|
<span>CPU</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold">
|
||||||
|
{{ cpu_allocated }} / {{ cpu_total }} vCPU
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% with pct=cpu_allocated|div:cpu_total|mul:100 %}
|
||||||
|
<div class="relative pt-1">
|
||||||
|
<div class="flex mb-2 items-center justify-between">
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-xs font-semibold inline-block">
|
||||||
|
{{ pct|floatformat:1 }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-base-300">
|
||||||
|
<div style="width: {{ pct }}%" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-primary"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span>Allocated</span>
|
||||||
|
<span class="text-warning">OC: x{{ cpu_overcommit_ratio }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RAM Allocation -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-secondary"></span>
|
||||||
|
<span>RAM</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold">
|
||||||
|
{{ ram_allocated }} / {{ ram_total }} GB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% with pct=ram_allocated|div:ram_total|mul:100 %}
|
||||||
|
<div class="relative pt-1">
|
||||||
|
<div class="flex mb-2 items-center justify-between">
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-xs font-semibold inline-block">
|
||||||
|
{{ pct|floatformat:1 }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-base-300">
|
||||||
|
<div style="width: {{ pct }}%" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-secondary"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span>Allocated</span>
|
||||||
|
<span class="text-warning">OC: x{{ ram_overcommit_ratio }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats Footer -->
|
||||||
|
<div class="mt-4 pt-4 border-t border-base-300">
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-success"></div>
|
||||||
|
<span class="text-xs">Avg. CPU per VM:</span>
|
||||||
|
<span class="text-xs font-semibold">{{ avg_cpu_per_vm|floatformat:1 }} vCPU</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-info"></div>
|
||||||
|
<span class="text-xs">Avg. RAM per VM:</span>
|
||||||
|
<span class="text-xs font-semibold">{{ avg_ram_per_vm|floatformat:1 }} GB</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-warning"></div>
|
||||||
|
<span class="text-xs">Density:</span>
|
||||||
|
<span class="text-xs font-semibold">{{ vm_density|floatformat:1 }} VMs/host</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AUDIT ANALYSIS SECTION -->
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="card bg-base-100 shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Selector Row -->
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="label pb-1">
|
||||||
|
<span class="label-text font-medium">Available Audits</span>
|
||||||
|
<span class="label-text-alt text-xs">{{ audits|length }} available</span>
|
||||||
|
</label>
|
||||||
|
<select id="auditSelector" class="select select-bordered w-full">
|
||||||
|
{% for audit in audits %}
|
||||||
|
<option value="{{ audit.id }}"
|
||||||
|
data-cpu="{{ audit.cpu_weight }}"
|
||||||
|
data-ram="{{ audit.ram_weight }}"
|
||||||
|
data-scope="{{ audit.scope }}"
|
||||||
|
data-strategy="{{ audit.strategy }}"
|
||||||
|
data-goal="{{ audit.goal }}">
|
||||||
|
{{ audit.name }} ({{ audit.created_at|date:"m/d/y" }})
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Full Parameters (Initially hidden, can be toggled) -->
|
||||||
|
<div id="auditDetails" class="mt-4 space-y-3 hidden">
|
||||||
|
<div class="divider text-xs">Audit Configuration</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium mb-2">Resource Weights</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm">CPU Priority:</span>
|
||||||
|
<span class="font-bold text-primary" id="detailCpu">1.0</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm">RAM Priority:</span>
|
||||||
|
<span class="font-bold text-secondary" id="detailRam">1.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium mb-2">Configuration</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm">Strategy:</span>
|
||||||
|
<span class="font-medium" id="detailStrategy">Balanced</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm">Scope:</span>
|
||||||
|
<span class="font-medium" id="detailScope">Full Cluster</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium mb-2">Goal</div>
|
||||||
|
<div class="bg-base-200 rounded p-3 text-sm" id="detailGoal">
|
||||||
|
Optimize resource distribution
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Simple Footer -->
|
||||||
|
<div class="flex justify-between items-center mt-4 pt-4 border-t border-base-300">
|
||||||
|
<div class="text-xs text-base-content/60" id="selectedAuditInfo">
|
||||||
|
Selected: {{ audits.0.name|default:"No audit selected" }}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn btn-xs btn-ghost" onclick="toggleAuditDetails()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-xs btn-primary" onclick="loadSelectedAudit()">
|
||||||
|
Load Audit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<!-- Charts Grid -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<div class="card bg-base-100 shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-semibold">Current CPU Utilization</h3>
|
||||||
|
<div class="badge badge-primary badge-outline">Live</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-64">
|
||||||
|
<canvas id="cpuHostChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="text-center text-xs text-base-content/60 mt-2">
|
||||||
|
Real-time CPU usage across compute hosts
|
||||||
|
</div>
|
||||||
|
<!-- Mean baseline indicator -->
|
||||||
|
<div class="flex items-center justify-center gap-2 mt-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-0.5 bg-success"></div>
|
||||||
|
<span class="text-xs text-success">Mean: <span id="currentCpuMean">0</span>%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-semibold">Projected CPU Utilization</h3>
|
||||||
|
<div class="badge badge-warning badge-outline">Projection</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-64">
|
||||||
|
<canvas id="cpuProjectedChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="text-center text-xs text-base-content/60 mt-2">
|
||||||
|
Estimated CPU usage after migrations
|
||||||
|
</div>
|
||||||
|
<!-- Mean and std deviation indicators -->
|
||||||
|
<div class="flex items-center justify-center gap-4 mt-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-0.5 bg-success"></div>
|
||||||
|
<span class="text-xs text-success">Mean: <span id="projectedCpuMean">0</span>%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-0.5 bg-error/60"></div>
|
||||||
|
<span class="text-xs text-error/60">±1σ: <span id="projectedCpuStd">0</span>%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Migration Table -->
|
||||||
|
<div class="card bg-base-100 shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="font-semibold">Migration Actions</h3>
|
||||||
|
<div class="badge badge-neutral" id="migrationCount">0 migrations</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-base-200">
|
||||||
|
<th class="text-xs font-semibold">Instance</th>
|
||||||
|
<th class="text-xs font-semibold">Source Host</th>
|
||||||
|
<th class="text-xs font-semibold">Destination Host</th>
|
||||||
|
<th class="text-xs font-semibold">Flavor</th>
|
||||||
|
<th class="text-xs font-semibold">Impact</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="migrationTableBody" class="text-sm">
|
||||||
|
<!-- Will be populated by JavaScript -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-8 text-base-content/40">
|
||||||
|
Select an audit to view migration actions
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
// Update preview when selection changes
|
||||||
|
document.getElementById('auditSelector').addEventListener('change', function(e) {
|
||||||
|
const option = this.options[this.selectedIndex];
|
||||||
|
|
||||||
|
// Update details section if visible
|
||||||
|
document.getElementById('detailCpu').textContent = option.dataset.cpu || '1.0';
|
||||||
|
document.getElementById('detailRam').textContent = option.dataset.ram || '1.0';
|
||||||
|
document.getElementById('detailScope').textContent = option.dataset.scope || 'Full Cluster';
|
||||||
|
document.getElementById('detailStrategy').textContent = option.dataset.strategy || 'Balanced';
|
||||||
|
document.getElementById('detailGoal').textContent = option.dataset.goal || 'Optimize resource distribution';
|
||||||
|
document.getElementById('selectedAuditInfo').textContent = `Selected: ${option.text}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleAuditDetails() {
|
||||||
|
const details = document.getElementById('auditDetails');
|
||||||
|
details.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSelectedAudit() {
|
||||||
|
const auditId = document.getElementById('auditSelector').value;
|
||||||
|
// Your existing load audit logic here
|
||||||
|
updateMigrationTable(auditId);
|
||||||
|
updateCPUCharts(auditId);
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast toast-top toast-end';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<span>Audit loaded successfully</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
setTimeout(() => toast.remove(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const selector = document.getElementById('auditSelector');
|
||||||
|
if (selector.options.length > 0) {
|
||||||
|
selector.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility function to calculate mean and standard deviation
|
||||||
|
function calculateStats(data) {
|
||||||
|
if (!data || data.length === 0) return { mean: 0, std: 0 };
|
||||||
|
|
||||||
|
const mean = data.reduce((a, b) => a + b, 0) / data.length;
|
||||||
|
const variance = data.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / data.length;
|
||||||
|
const std = Math.sqrt(variance);
|
||||||
|
|
||||||
|
return { mean, std };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart configuration
|
||||||
|
const chartConfig = {
|
||||||
|
cutout: "60%",
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return `${context.label}: ${context.parsed} vCPU`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
animateScale: true,
|
||||||
|
animateRotate: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize CPU and RAM donut charts with mean baselines
|
||||||
|
const cpuCtx = document.getElementById("cpuChart").getContext('2d');
|
||||||
|
const ramCtx = document.getElementById("ramChart").getContext('2d');
|
||||||
|
|
||||||
|
// Calculate CPU mean for baseline
|
||||||
|
const cpuMean = {{ cpu_used_percentage }};
|
||||||
|
const cpuChart = new Chart(cpuCtx, {
|
||||||
|
type: "doughnut",
|
||||||
|
data: {
|
||||||
|
labels: ["Used", "Free"],
|
||||||
|
datasets: [{
|
||||||
|
data: [{{ cpu_used }}, {{ cpu_free }}],
|
||||||
|
backgroundColor: ["#3b82f6", "#e5e7eb"],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#ffffff"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...chartConfig,
|
||||||
|
plugins: {
|
||||||
|
...chartConfig.plugins,
|
||||||
|
annotation: {
|
||||||
|
annotations: {
|
||||||
|
meanLine: {
|
||||||
|
type: 'line',
|
||||||
|
yMin: cpuMean,
|
||||||
|
yMax: cpuMean,
|
||||||
|
borderColor: '#10b981',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [5, 5],
|
||||||
|
label: {
|
||||||
|
enabled: true,
|
||||||
|
content: `Mean: ${cpuMean.toFixed(1)}%`,
|
||||||
|
position: 'end',
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
color: 'white',
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate RAM mean for baseline
|
||||||
|
const ramMean = {{ ram_used_percentage }};
|
||||||
|
const ramChart = new Chart(ramCtx, {
|
||||||
|
type: "doughnut",
|
||||||
|
data: {
|
||||||
|
labels: ["Used", "Free"],
|
||||||
|
datasets: [{
|
||||||
|
data: [{{ ram_used }}, {{ ram_free }}],
|
||||||
|
backgroundColor: ["#f97316", "#e5e7eb"],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#ffffff"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...chartConfig,
|
||||||
|
plugins: {
|
||||||
|
...chartConfig.plugins,
|
||||||
|
annotation: {
|
||||||
|
annotations: {
|
||||||
|
meanLine: {
|
||||||
|
type: 'line',
|
||||||
|
yMin: ramMean,
|
||||||
|
yMax: ramMean,
|
||||||
|
borderColor: '#10b981',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [5, 5],
|
||||||
|
label: {
|
||||||
|
enabled: true,
|
||||||
|
content: `Mean: ${ramMean.toFixed(1)}%`,
|
||||||
|
position: 'end',
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
color: 'white',
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Audit data (in production, this should come from Django context)
|
||||||
|
const auditData = {
|
||||||
|
{% for audit in audits %}
|
||||||
|
"{{ audit.id }}": {
|
||||||
|
name: "{{ audit.name }}",
|
||||||
|
migrations: {{ audit.migrations|safe }},
|
||||||
|
hostData: {
|
||||||
|
labels: {{ audit.host_labels|safe }},
|
||||||
|
current: {{ audit.cpu_current|safe }},
|
||||||
|
projected: {{ audit.cpu_projected|safe }}
|
||||||
|
}
|
||||||
|
}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chart instances
|
||||||
|
let cpuHostChart = null;
|
||||||
|
let cpuProjectedChart = null;
|
||||||
|
|
||||||
|
// Update migration table
|
||||||
|
function updateMigrationTable(auditId) {
|
||||||
|
const tbody = document.getElementById('migrationTableBody');
|
||||||
|
const migrationCount = document.getElementById('migrationCount');
|
||||||
|
const data = auditData[auditId];
|
||||||
|
|
||||||
|
if (!data || data.migrations.length === 0) {
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-8 text-base-content/40">
|
||||||
|
No migration actions for this audit
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
migrationCount.textContent = '0 migrations';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
data.migrations.forEach(migration => {
|
||||||
|
const impact = migration.impact || 'Low';
|
||||||
|
const impactClass = {
|
||||||
|
'Low': 'badge-success',
|
||||||
|
'Medium': 'badge-warning',
|
||||||
|
'High': 'badge-error'
|
||||||
|
}[impact];
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td class="font-medium">${migration.instanceName}</td>
|
||||||
|
<td><span class="badge badge-outline">${migration.source}</span></td>
|
||||||
|
<td><span class="badge badge-primary badge-outline">${migration.destination}</span></td>
|
||||||
|
<td><code class="text-xs">${migration.flavor}</code></td>
|
||||||
|
<td><span class="badge ${impactClass}">${impact}</span></td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
migrationCount.textContent = `${data.migrations.length} migration${data.migrations.length !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update CPU charts
|
||||||
|
function updateCPUCharts(auditId) {
|
||||||
|
const data = auditData[auditId];
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const hostCtx = document.getElementById('cpuHostChart').getContext('2d');
|
||||||
|
const projCtx = document.getElementById('cpuProjectedChart').getContext('2d');
|
||||||
|
|
||||||
|
// Calculate statistics
|
||||||
|
const currentStats = calculateStats(data.hostData.current);
|
||||||
|
const projectedStats = calculateStats(data.hostData.projected);
|
||||||
|
|
||||||
|
// Update stats displays
|
||||||
|
document.getElementById('currentCpuMean').textContent = currentStats.mean.toFixed(1);
|
||||||
|
document.getElementById('projectedCpuMean').textContent = projectedStats.mean.toFixed(1);
|
||||||
|
document.getElementById('projectedCpuStd').textContent = projectedStats.std.toFixed(1);
|
||||||
|
|
||||||
|
// Create mean and std deviation arrays for projected chart
|
||||||
|
const meanLine = Array(data.hostData.labels.length).fill(projectedStats.mean);
|
||||||
|
const stdPlusOne = Array(data.hostData.labels.length).fill(projectedStats.mean + projectedStats.std);
|
||||||
|
const stdMinusOne = Array(data.hostData.labels.length).fill(projectedStats.mean - projectedStats.std);
|
||||||
|
|
||||||
|
// Destroy existing charts
|
||||||
|
if (cpuHostChart) cpuHostChart.destroy();
|
||||||
|
if (cpuProjectedChart) cpuProjectedChart.destroy();
|
||||||
|
|
||||||
|
// Create current CPU chart with mean baseline
|
||||||
|
cpuHostChart = new Chart(hostCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.hostData.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'CPU Utilization (%)',
|
||||||
|
data: data.hostData.current,
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderSkipped: false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Percentage (%)'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => `${ctx.parsed.y}% CPU`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
annotations: {
|
||||||
|
meanLine: {
|
||||||
|
type: 'line',
|
||||||
|
yMin: currentStats.mean,
|
||||||
|
yMax: currentStats.mean,
|
||||||
|
borderColor: '#10b981',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [5, 5],
|
||||||
|
label: {
|
||||||
|
enabled: true,
|
||||||
|
content: `Mean: ${currentStats.mean.toFixed(1)}%`,
|
||||||
|
position: 'end',
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
color: 'white',
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create projected CPU chart with mean and std deviation baselines
|
||||||
|
cpuProjectedChart = new Chart(projCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.hostData.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Projected CPU (%)',
|
||||||
|
data: data.hostData.projected,
|
||||||
|
backgroundColor: '#f97316',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderSkipped: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mean',
|
||||||
|
data: meanLine,
|
||||||
|
type: 'line',
|
||||||
|
borderColor: '#10b981',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [5, 5],
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '+1σ',
|
||||||
|
data: stdPlusOne,
|
||||||
|
type: 'line',
|
||||||
|
borderColor: '#ef4444',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderDash: [3, 3],
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '-1σ',
|
||||||
|
data: stdMinusOne,
|
||||||
|
type: 'line',
|
||||||
|
borderColor: '#ef4444',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderDash: [3, 3],
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Percentage (%)'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawBorder: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => {
|
||||||
|
if (ctx.datasetIndex === 0) {
|
||||||
|
return `Projected: ${ctx.parsed.y}%`;
|
||||||
|
} else if (ctx.datasetIndex === 1) {
|
||||||
|
return `Mean: ${ctx.parsed.y.toFixed(1)}%`;
|
||||||
|
} else {
|
||||||
|
return `${ctx.datasetIndex === 2 ? '+1σ' : '-1σ'}: ${ctx.parsed.y.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const initialAudit = "{{ audits.0.id }}";
|
||||||
|
updateMigrationTable(initialAudit);
|
||||||
|
updateCPUCharts(initialAudit);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -37,7 +37,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'metrics',
|
'dashboard',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Prometheus settings (environment override recommended)
|
# Prometheus settings (environment override recommended)
|
||||||
@@ -59,7 +59,7 @@ ROOT_URLCONF = 'watcher_visio.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@@ -119,7 +119,13 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR / "static",
|
||||||
|
]
|
||||||
|
|
||||||
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|||||||
@@ -19,5 +19,5 @@ from django.urls import path, include
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path("", include("metrics.urls")),
|
path('', include('dashboard.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user