Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,10 @@ not exist. `tests/test_templates.py` now asserts every rendered template resolve
7. **The remaining facade.** `reports/executive_dashboard.py` generates its revenue trends
and geographic breakdown. Those payloads now carry a `simulated` flag so the UI can
label them, but generated figures in an executive dashboard should be built or removed.
Seven admin, Azure and project-template pages still render templates that do not exist.
It is now the only one left: the nine missing pages are built, the eleven analytics
helpers are implemented, and `admin/system_status.html` no longer reports a hardcoded
"245ms average response time, 127 requests per minute, 0.2% error rate" — it measures
what it can and omits request rate rather than inventing it.
8. **Licensing — resolved.** The repository is now [MIT licensed](LICENSE), copyright
Matthew M. Emma. It previously described itself as "proprietary software developed for
Balfour Beatty US. All rights reserved." while carrying no `LICENSE` file, so under
Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![CI](https://github.com/ibuilder/AIHackScheduler/actions/workflows/ci.yml/badge.svg)](https://github.com/ibuilder/AIHackScheduler/actions/workflows/ci.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-284%20passing-brightgreen)](tests/)
[![Tests](https://img.shields.io/badge/tests-327%20passing-brightgreen)](tests/)

Most schedule tools assume the schedule they are given is sound. Most are not. BBSchedule
computes the critical path properly, then grades the schedule against the
Expand Down Expand Up @@ -399,9 +399,19 @@ flag so the UI can label them, but they should be built or removed.
no webhook reconciliation, no PCI scope. The README previously advertised "Stripe
integration in progress" against no implementation of any kind.

**Known gaps, tested as gaps** — seven admin, Azure and project-template pages render
templates that were never written, so those routes return 500. `tests/test_templates.py`
holds the list and fails if it grows.
**Every page renders** — the nine admin, Azure, project-template and reporting pages that
rendered templates nobody had written are built, and the two analytics endpoints whose
helpers did not exist are implemented. `tests/test_all_routes.py` walks the URL map and
requests all 90 GET routes signed in; 83 return 200, none return a server error. It walks
the map rather than a list, so a route added tomorrow is covered the day it appears.

**Resource optimisation and portfolio insight** — `/api/ai/resource-optimization/<id>`
reports utilisation per resource, names what is over-allocated and by how many units,
prices the excess at each resource's own unit cost, and ranks the moves.
`/api/ai/company-insights` reports completion rate, spend against approved budget,
throughput trend across six periods, and DCMA health per project. Both are deterministic:
the same data gives the same answer, which is the property a schedule review needs.
Azure OpenAI is not required for either.

The near-term priorities, in order:

Expand Down
120 changes: 101 additions & 19 deletions admin/user_management.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import logging

from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
from datetime import datetime, timezone

from flask import (
Blueprint,
current_app,
flash,
jsonify,
redirect,
render_template,
request,
url_for,
)
from flask_login import current_user, login_required
from werkzeug.security import generate_password_hash

from audit.audit_logger import audit_logger
from extensions import db
from models import AuditLog, Company, User, UserRole
from models import AuditLog, Company, Project, User, UserRole

admin_bp = Blueprint("user_management", __name__)

Expand Down Expand Up @@ -257,21 +267,93 @@ def system_status():
flash("Access denied. Admin privileges required.", "error")
return redirect(url_for("main.dashboard"))

# Get system health information
status_data = {
"database": "healthy",
"cache": "healthy",
"background_jobs": "healthy",
"integrations": {
"power_bi": "pending_setup",
"azure_ai": "not_configured",
"fabric": "not_configured",
},
"performance": {
"avg_response_time": "245ms",
"requests_per_minute": 127,
"error_rate": "0.2%",
},
return render_template("admin/system_status.html", status=collect_system_status())


def collect_system_status() -> dict:
"""Measure what the platform can actually observe about itself.

This used to return a literal dict: database "healthy", cache "healthy",
average response time "245ms", 127 requests per minute, error rate "0.2%".
None of it was measured. An administrator opening the page to decide
whether the system was in trouble was reading numbers that never changed,
which is worse than showing nothing.

Everything below is either measured now or reported as unknown. Request
rate and error rate are deliberately absent rather than invented: nothing
in the application records them, and the place to read them is the
``/health/metrics`` endpoint that Prometheus scrapes.
"""
import os
import time

import psutil

from monitoring.health_checks import _database_is_reachable

status = {"checked_at": datetime.now(timezone.utc).isoformat()}

try:
status["database"] = {
"status": "healthy",
"response_time_ms": _database_is_reachable(),
}
except Exception as exc:
logging.error("System status: database unreachable: %s", exc, exc_info=True)
status["database"] = {"status": "unhealthy"}

# The cache is configured at startup; report the backend actually in use
# rather than asserting health of something that may be a no-op.
try:
cache_type = current_app.config.get("CACHE_TYPE", "unknown")
status["cache"] = {
"status": "healthy" if cache_type else "not_configured",
"backend": str(cache_type),
}
except Exception as exc:
logging.error("System status: cache check failed: %s", exc, exc_info=True)
status["cache"] = {"status": "unknown"}

# Background jobs need a broker. Without one, Celery is not running, and
# saying so is more useful than a green tick.
broker = os.environ.get("CELERY_BROKER_URL") or os.environ.get("REDIS_URL")
status["background_jobs"] = {
"status": "configured" if broker else "not_configured",
"broker": "redis" if broker else None,
}

# An integration is configured when its credentials are present. This is
# the same test services/optional.py applies before enabling a feature.
status["integrations"] = {
"azure_ai": "configured"
if os.environ.get("AZURE_OPENAI_ENDPOINT") and os.environ.get("AZURE_OPENAI_KEY")
else "not_configured",
"fabric": "configured" if os.environ.get("AZURE_FABRIC_ENDPOINT") else "not_configured",
"power_bi": "configured"
if all(
os.environ.get(name)
for name in ("POWERBI_CLIENT_ID", "POWERBI_CLIENT_SECRET", "POWERBI_TENANT_ID")
)
else "not_configured",
"stripe": "configured" if os.environ.get("STRIPE_SECRET_KEY") else "not_configured",
}

try:
process = psutil.Process()
status["process"] = {
"pid": process.pid,
"uptime_seconds": round(time.time() - process.create_time()),
"memory_mb": round(process.memory_info().rss / (1024 * 1024), 1),
"cpu_percent": psutil.cpu_percent(interval=None),
"system_memory_percent": psutil.virtual_memory().percent,
}
except Exception as exc:
logging.error("System status: process metrics failed: %s", exc, exc_info=True)
status["process"] = {}

status["records"] = {
"users": User.query.filter_by(company_id=current_user.company_id).count(),
"projects": Project.query.filter_by(company_id=current_user.company_id).count(),
}

return render_template("admin/system_status.html", status=status_data)
return status
Loading