From 9e1e4ea39b4109ef00dfe5a331d8c5fd56cc38aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:35:43 +0000 Subject: [PATCH 1/4] Initial plan From b48b68a56ea6163be7e0b350c186c87a87ddacc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 00:41:04 +0000 Subject: [PATCH 2/4] docs: create comprehensive project-level README.md Replace single-line stub with full 666-line README covering: - Project overview with status note (in-memory / framework stage) - Five Pillars of Resilience diagram and table - ASCII architecture diagram showing all layers - Event-driven communication topics table - Key features list and tech stack table - Four quick-start options (Docker, local dev, demo, CLI) - Installation instructions - Configuration reference with key env vars - Microservices table (ports, responsibilities) - Dashboard & REST API route group summary - CLI reference with all commands - Chaos engineering experiments table + API usage - CRS formula and risk-profile weight table - Maturity model (5 levels) - Runbook structure example + recovery tier table - Testing (commands, suite summary, lint/type-check) - Full annotated project structure tree - Contributing guidelines with development notes - License Co-authored-by: dnoice <94666757+dnoice@users.noreply.github.com> --- README.md | 667 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 666 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b35e5c..6e7493b 100644 --- a/README.md +++ b/README.md @@ -1 +1,666 @@ -# AREF - Adaptive Resilience Engineering Framework +
+ +# AREF — Adaptive Resilience Engineering Framework + +**A comprehensive systems-level platform for infrastructure resilience, failure recovery, and operational continuity** + +[![Version](https://img.shields.io/badge/version-2.0.0-blue.svg)](https://github.com/dnoice/AREF) +[![Python](https://img.shields.io/badge/python-3.11%2B-brightgreen.svg)](https://www.python.org/downloads/) +[![License](https://img.shields.io/badge/license-Apache%202.0-orange.svg)](LICENSE) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.110%2B-009688.svg)](https://fastapi.tiangolo.com) +[![Docker](https://img.shields.io/badge/docker-compose-2496ED.svg)](docker-compose.yml) +[![Tests](https://img.shields.io/badge/tests-pytest-brightgreen.svg)](tests/) + +*Author: Dennis 'dnoice' Smaltz — digiSpace Technical Studio* + +
+ +--- + +## Table of Contents + +- [Overview](#overview) +- [The Five Pillars of Resilience](#the-five-pillars-of-resilience) +- [Architecture](#architecture) +- [Key Features](#key-features) +- [Tech Stack](#tech-stack) +- [Quick Start](#quick-start) + - [Option 1 — Full Stack (Docker)](#option-1--full-stack-docker) + - [Option 2 — Dashboard Only (Local)](#option-2--dashboard-only-local) + - [Option 3 — Interactive Demo](#option-3--interactive-demo) + - [Option 4 — CLI](#option-4--cli) +- [Installation](#installation) +- [Configuration](#configuration) +- [Microservices](#microservices) +- [Dashboard & API](#dashboard--api) +- [CLI Reference](#cli-reference) +- [Chaos Engineering](#chaos-engineering) +- [Composite Resilience Score (CRS)](#composite-resilience-score-crs) +- [Maturity Model](#maturity-model) +- [Runbooks](#runbooks) +- [Testing](#testing) +- [Project Structure](#project-structure) +- [Contributing](#contributing) +- [License](#license) + +--- + +## Overview + +**AREF** (Adaptive Resilience Engineering Framework) is a production-grade reference implementation of a **self-healing distributed systems platform**. It operationalises the five engineering disciplines required to keep complex services reliable under failure conditions: + +1. **Detect** anomalies early, before they cascade +2. **Absorb** the blast radius and contain the damage +3. **Adapt** dynamically by reconfiguring in real-time +4. **Recover** in a tiered, runbook-driven fashion +5. **Evolve** continuously through post-incident learning + +AREF ships with a live control-plane dashboard, a rich CLI, five instrumented microservices, a chaos-injection engine, Prometheus/Grafana observability, and a complete test suite — everything needed to study, demo, and extend resilience engineering patterns. + +> **Status:** Reference / Framework stage. All state is in-memory. PostgreSQL and Redis are provisioned in Docker but not yet wired to the application layer (planned future phase). + +--- + +## The Five Pillars of Resilience + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Pillar I │ │ Pillar II │ │ Pillar III │ │ Pillar IV │ │ Pillar V │ +│ DETECTION │→ │ ABSORPTION │→ │ ADAPTATION │→ │ RECOVERY │→ │ EVOLUTION │ +│ │ │ │ │ │ │ │ │ │ +│ Early │ │ Impact │ │ Real-time │ │ Tiered │ │ Post- │ +│ warning & │ │ containment │ │ reconfig- │ │ service │ │ incident │ +│ anomaly │ │ & blast │ │ uration │ │ restoration │ │ learning │ +│ detection │ │ radius ctrl │ │ │ │ (T0 – T4) │ │ │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ +``` + +| Pillar | Purpose | Key Components | +|--------|---------|----------------| +| **I — Detection** | Identify failures before they become outages | Threshold detector, statistical anomaly detector (Z-score), synthetic probing, SLI/SLO tracking | +| **II — Absorption** | Contain blast radius and prevent cascades | Circuit breaker (3-state FSM), bulkhead isolation, token-bucket rate limiter, 4-tier graceful degradation, blast-radius graph | +| **III — Adaptation** | Reconfigure the system in real-time | Feature flags, weighted traffic shifting, horizontal auto-scaler, 6-step decision tree | +| **IV — Recovery** | Restore service in a structured, time-boxed way | Tiered runbook executor (T0–T4), YAML runbooks, incident commander workflow | +| **V — Evolution** | Turn every incident into a system improvement | Automated post-incident reviews, action-item tracker, pattern matcher, knowledge base | + +--- + +## Architecture + +``` + ┌─────────────────────────────────┐ + │ AREF Dashboard (port 8080) │ + │ FastAPI + SPA Web UI │ + │ REST API / Control Plane │ + └────────────┬────────────────────┘ + │ events / polling + ┌────────────────────────▼────────────────────────────┐ + │ AREF Engine Core │ + │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ + │ │Detection │ │Absorption│ │Adaptation│ │ + │ │ Engine │ │ Engine │ │ Engine │ │ + │ └──────────┘ └──────────┘ └──────────┘ │ + │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ + │ │Recovery │ │Evolution │ │ Maturity │ │ + │ │ Engine │ │ Engine │ │ Assessor │ │ + │ └──────────┘ └──────────┘ └──────────┘ │ + │ Event Bus (pub/sub) │ + └───────────────────┬─────────────────────────────────┘ + │ HTTP / health checks + ┌─────────────────────────▼──────────────────────────────────┐ + │ Microservice Layer │ + │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ + │ │ Gateway │ │ Orders │ │ Payments │ │ Inventory │ │ + │ │ :8000 │ │ :8001 │ │ :8002 │ │ :8003 │ │ + │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ + │ ┌──────────────────┐ │ + │ │ Notifications │ │ + │ │ :8004 │ │ + │ └──────────────────┘ │ + └────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────────────▼──────────────────────────────────┐ + │ Observability Layer │ + │ Prometheus :9090 · Grafana :3000 │ + └────────────────────────────────────────────────────────────┘ +``` + +### Event-Driven Communication + +All engines communicate through an **async in-process event bus** using a `"category.event_type"` topic format: + +| Topic | Published by | +|-------|-------------| +| `detection.alert_fired` | Detection Engine | +| `absorption.circuit_breaker_opened` | Absorption Engine | +| `adaptation.adaptation_executed` | Adaptation Engine | +| `recovery.recovery_started` | Recovery Engine | +| `recovery.recovery_resolved` | Recovery Engine | +| `evolution.post_incident_review_generated` | Evolution Engine | + +Engines can subscribe using wildcards: `"detection.*"` or `"*"` for all events. + +--- + +## Key Features + +- **Five-Pillar Resilience Framework** — A complete, end-to-end incident lifecycle implemented in code +- **Composite Resilience Score (CRS)** — A single weighted metric (0–5) reflecting overall system resilience across configurable risk profiles +- **Five-Level Maturity Model** — Per-pillar gap analysis from *Reactive* to *Optimizing* +- **Live Dashboard** — Single-page control plane with real-time CRS, pillar health, incident timeline, and chaos controls +- **Rich CLI** — Full `aref` command-line interface for status, maturity, chaos, and timeline +- **Chaos Engineering Engine** — Five pre-defined fault-injection experiments with automatic rollback +- **Runbook-Driven Recovery** — YAML-defined, version-controlled runbooks with T0–T4 tiering +- **Prometheus + Grafana** — Out-of-the-box metrics, scrape configs, and Grafana provisioning +- **Five Instrumented Microservices** — Gateway + Orders + Payments + Inventory + Notifications with shared factory +- **Structured Logging** — structlog throughout, with correlation IDs +- **Interactive Demo** — End-to-end Rich terminal demo with scenario walkthroughs +- **Full Test Suite** — Unit and integration tests covering the complete incident lifecycle + +--- + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Language | Python 3.11+ | +| Web Framework | FastAPI 0.110+ | +| ASGI Server | Uvicorn | +| Data Validation | Pydantic v2, pydantic-settings | +| CLI | Click + Rich | +| Metrics | Prometheus Client, prometheus-fastapi-instrumentator | +| Anomaly Detection | NumPy (Z-score, statistical baseline) | +| Logging | structlog | +| Runbooks | PyYAML | +| HTTP Client | HTTPX | +| Containerisation | Docker, Docker Compose | +| Observability | Prometheus, Grafana | +| Database (provisioned) | PostgreSQL 16 | +| Cache (provisioned) | Redis 7 | +| Build | Hatchling | +| Test | pytest, pytest-asyncio, pytest-cov | +| Lint / Format | Ruff | +| Type Check | mypy (strict) | + +--- + +## Quick Start + +### Option 1 — Full Stack (Docker) + +The fastest way to run everything — five microservices, the AREF control plane, Prometheus, and Grafana: + +```bash +git clone https://github.com/dnoice/AREF.git +cd AREF + +# (Optional) copy and customise environment config +cp .env.example .env + +docker compose up --build +``` + +| Service | URL | +|---------|-----| +| AREF Dashboard | http://localhost:8080 | +| API Gateway | http://localhost:8000 | +| Orders Service | http://localhost:8001 | +| Payments Service | http://localhost:8002 | +| Inventory Service | http://localhost:8003 | +| Notifications Service | http://localhost:8004 | +| Prometheus | http://localhost:9090 | +| Grafana | http://localhost:3000 (admin / aref) | + +### Option 2 — Dashboard Only (Local) + +Run just the AREF control plane locally without Docker: + +```bash +# Install with dev dependencies +pip install -e ".[dev]" + +# Start the dashboard +uvicorn aref.dashboard.app:app --port 8080 --reload +``` + +Open http://localhost:8080 in your browser. + +### Option 3 — Interactive Demo + +An end-to-end Rich terminal walkthrough of the full incident lifecycle: + +```bash +pip install -e ".[dev]" + +# Run the full interactive demo +python -m scripts.demo + +# Run a specific experiment directly +python -m scripts.demo --experiment payment_provider_failure +``` + +### Option 4 — CLI + +```bash +pip install -e ".[dev]" + +aref status # Platform overview & CRS +aref pillars # Per-pillar health & scores +aref maturity # Maturity assessment & gap analysis +aref timeline # Recent event history +aref chaos list # Available chaos experiments +aref chaos run payment_provider_failure # Inject a fault +aref serve # Start dashboard from CLI +``` + +--- + +## Installation + +**Prerequisites:** Python 3.11+, pip + +```bash +# Clone the repository +git clone https://github.com/dnoice/AREF.git +cd AREF + +# Install runtime dependencies only +pip install -e . + +# Install with developer tooling (tests, lint, type-check) +pip install -e ".[dev]" +``` + +--- + +## Configuration + +All configuration is driven by environment variables (or a `.env` file). Copy the template to get started: + +```bash +cp .env.example .env +``` + +### General Settings + +| Variable | Default | Description | +|----------|---------|-------------| +| `AREF_ENVIRONMENT` | `development` | Set to `docker` for container hostnames | +| `AREF_DEBUG` | `true` | Enable debug mode | +| `AREF_LOG_LEVEL` | `INFO` | Log verbosity | +| `AREF_RISK_PROFILE` | `balanced` | CRS weighting profile (see [CRS section](#composite-resilience-score-crs)) | +| `AREF_API_HOST` | `0.0.0.0` | API bind address | +| `AREF_API_PORT` | `8000` | API listen port | +| `AREF_DASHBOARD_PORT` | `8080` | Dashboard listen port | + +### Pillar-Specific Settings (selected) + +| Variable | Default | Description | +|----------|---------|-------------| +| `AREF_DETECTION_MTTD_TARGET_SECONDS` | `300` | MTTD target (< 5 min) | +| `AREF_ABSORPTION_CIRCUIT_BREAKER_FAILURE_THRESHOLD` | `5` | Failures before circuit opens | +| `AREF_ABSORPTION_CIRCUIT_BREAKER_RECOVERY_TIMEOUT` | `30` | Seconds before half-open | +| `AREF_ABSORPTION_RATE_LIMIT_REQUESTS_PER_SECOND` | `100` | Token bucket rate limit | +| `AREF_ADAPTATION_ADAPTATION_WINDOW_SECONDS` | `120` | Window before escalating to recovery | +| `AREF_RECOVERY_MTTR_TARGET_SECONDS` | `900` | MTTR target (< 15 min) | +| `AREF_EVOLUTION_ACTION_COMPLETION_RATE_TARGET` | `85` | Target action completion % | + +See [`.env.example`](.env.example) for the full list of 60+ variables covering all five pillars, database, Redis, and Grafana. + +--- + +## Microservices + +All five services are built through the shared `services/base.py` factory (`create_service()`), which automatically provides: + +- Prometheus instrumentation (`/metrics`) +- Health and readiness probes (`/health`, `/readyz`) +- Service info endpoint (`/info`) +- Correlation ID propagation +- Structured logging +- CORS middleware + +| Service | Port | Responsibility | +|---------|------|---------------| +| **Gateway** | 8000 | Request routing, retry logic, circuit-breaker awareness, order pipeline orchestration | +| **Orders** | 8001 | Order lifecycle state machine, payment callbacks, inventory coordination, audit trail | +| **Payments** | 8002 | Payment provider integration, failure simulation, queued payments | +| **Inventory** | 8003 | Stock management, reservation, degradation scenarios | +| **Notifications** | 8004 | Queue-based notification dispatch | + +--- + +## Dashboard & API + +The AREF Dashboard is a **FastAPI** application (`aref/dashboard/app.py`) that serves: + +- A **single-page web application** (`/`) with six tabs: Overview, Pillars, Services, Metrics, Chaos, and Timeline +- A **REST API** under `/api/v1/` with four route groups: + +| Route Group | Path Prefix | Description | +|-------------|------------|-------------| +| Status | `/api/v1/status/` | Platform status, service health, incident list | +| Pillars | `/api/v1/pillars/` | Per-pillar status, scores, and active alerts | +| Metrics | `/api/v1/metrics/` | CRS, MTTD, MTTR, pillar scores | +| Chaos | `/api/v1/chaos/` | List experiments, inject faults, rollback | + +The dashboard polls all five microservices every 10 seconds and updates the UI in real-time. + +--- + +## CLI Reference + +The `aref` CLI is built with Click and Rich: + +``` +Usage: aref [OPTIONS] COMMAND [ARGS]... + +Commands: + status Display platform status and overall CRS score + pillars Show per-pillar health, scores, and active alerts + maturity Run maturity assessment and gap analysis + timeline Display recent event history from the event bus + chaos Manage and run chaos experiments + serve Start the AREF dashboard server +``` + +```bash +# View platform status with CRS +aref status + +# Detailed pillar breakdown +aref pillars + +# Maturity assessment across all five pillars +aref maturity + +# Recent event timeline (last 20 events) +aref timeline --limit 20 + +# List available chaos experiments +aref chaos list + +# Run a specific experiment +aref chaos run cascading_failure + +# Start the dashboard (alternative to uvicorn) +aref serve --port 8080 +``` + +--- + +## Chaos Engineering + +AREF ships with a **FaultInjector** (`chaos/injector.py`) and five pre-defined experiments (`chaos/experiments.py`): + +| Experiment | Target | Fault Type | Description | +|------------|--------|-----------|-------------| +| `payment_provider_failure` | Payments | Error injection | Simulates a payment provider outage | +| `order_service_latency` | Orders | Latency injection | Adds artificial latency to order processing | +| `inventory_degradation` | Inventory | Error rate + degradation | Triggers graceful degradation mode | +| `notification_overload` | Notifications | Load spike | Floods the notification queue | +| `cascading_failure` | Multiple | Multi-service | Simulates a cascading cross-service failure | + +All experiments include **automatic rollback** — the injector restores the original service behaviour when the experiment concludes or times out. + +### Running Chaos via the API + +```bash +# List experiments +GET /api/v1/chaos/experiments + +# Start an experiment +POST /api/v1/chaos/experiments/{experiment_id}/start + +# Rollback (stop experiment) +POST /api/v1/chaos/experiments/{experiment_id}/stop +``` + +--- + +## Composite Resilience Score (CRS) + +The **CRS** is a single weighted metric in the range **0.0 – 5.0** that reflects overall system resilience. The weight of each pillar is determined by the active **risk profile**: + +| Pillar | Availability Critical | Data Integrity Critical | Balanced | Innovation Heavy | +|--------|----------------------|------------------------|---------|----------------| +| Detection | 30% | 20% | 20% | 15% | +| Absorption | 25% | 20% | 20% | 15% | +| Adaptation | 20% | 15% | 20% | 25% | +| Recovery | 15% | 30% | 20% | 15% | +| Evolution | 10% | 15% | 20% | 30% | + +Set the risk profile via `AREF_RISK_PROFILE` environment variable (options: `balanced`, `availability_critical`, `data_integrity_critical`, `innovation_heavy`). + +**Formula:** +``` +CRS = Σ (pillar_score × weight) for each pillar in {I, II, III, IV, V} +``` + +--- + +## Maturity Model + +Each pillar is assessed independently on a **five-level maturity scale**: + +| Level | Name | Characteristics | +|-------|------|----------------| +| **1** | Reactive | Ad-hoc responses, no documented processes | +| **2** | Managed | Repeatable processes, basic monitoring in place | +| **3** | Defined | Documented procedures, standard tooling adopted | +| **4** | Measured | Quantified metrics, targets tracked and met | +| **5** | Optimizing | Continuous improvement, full automation, feedback loops | + +The **MaturityAssessor** (`aref/maturity/model.py`) calculates a score for each pillar, identifies gaps, and generates prioritised improvement recommendations. Access via `aref maturity` or the Dashboard *Maturity* tab. + +--- + +## Runbooks + +Recovery runbooks are **YAML-defined** files stored in `runbooks/` and executed by the `RunbookExecutor` (`aref/recovery/runbooks.py`). + +### Runbook Structure + +```yaml +runbooks: + - name: payment_t0_stabilize + service: payments + tier: 0 # T0 = fully automated, 0–5 minutes + version: "1.0.0" + description: "Emergency stabilization for payment provider outage" + steps: + - order: 1 + action: detect_payment_failures + automated: true + timeout_seconds: 10 + - order: 2 + action: open_circuit_breaker + automated: true + timeout_seconds: 5 + - order: 3 + action: switch_provider + automated: true + timeout_seconds: 10 + escalation: "Page on-call if backup provider also fails" +``` + +### Recovery Tiers + +| Tier | Time Window | Automation | Owner | +|------|-------------|-----------|-------| +| **T0** | 0 – 5 min | Fully automated | System | +| **T1** | 5 – 15 min | Mostly automated | Incident Commander | +| **T2** | 15 – 60 min | Semi-automated | Engineering team | +| **T3** | 1 – 4 hours | Manual with tooling | Senior engineers | +| **T4** | 1 – 2 weeks | Process-driven | Leadership + Engineering | + +--- + +## Testing + +```bash +# Install dev dependencies +pip install -e ".[dev]" + +# Run all tests +pytest tests/ -v + +# Run with coverage report +pytest tests/ -v --cov=aref --cov=services + +# Run only unit tests +pytest tests/unit/ -v + +# Run only integration tests +pytest tests/integration/ -v +``` + +### Test Coverage + +| Suite | File | What's Tested | +|-------|------|--------------| +| Unit | `tests/unit/test_core.py` | Config, event bus, metrics, models | +| Unit | `tests/unit/test_pillars.py` | Circuit breaker FSM, feature flags, recovery tiers | +| Integration | `tests/integration/test_full_pipeline.py` | Complete incident lifecycle: detection → absorption → adaptation → recovery → evolution | + +### Linting & Type Checking + +```bash +# Lint with Ruff +ruff check . + +# Type check with mypy (strict mode) +mypy aref/ +``` + +--- + +## Project Structure + +``` +AREF/ +├── aref/ # Core AREF framework package +│ ├── core/ # Shared infrastructure +│ │ ├── config.py # Pydantic-settings configuration (env-driven) +│ │ ├── events.py # Async event bus (pub/sub + history) +│ │ ├── metrics.py # Prometheus metrics + CRS formula engine +│ │ ├── models.py # Domain models (Incident, ActionItem, …) +│ │ └── logging.py # structlog setup +│ ├── detection/ # Pillar I — Early warning & anomaly detection +│ │ ├── engine.py # DetectionEngine orchestrator +│ │ ├── threshold.py # Metric threshold detection +│ │ ├── anomaly.py # Statistical anomaly detection (Z-score) +│ │ ├── synthetic.py # Active HTTP synthetic probing +│ │ └── sli_tracker.py # SLI/SLO tracking + error budget +│ ├── absorption/ # Pillar II — Impact containment +│ │ ├── circuit_breaker.py # 3-state circuit breaker + registry +│ │ ├── bulkhead.py # Semaphore-based concurrency isolation +│ │ ├── rate_limiter.py # Token bucket rate limiting +│ │ ├── blast_radius.py # Dependency graph + blast radius analysis +│ │ └── degradation.py # 4-tier graceful degradation +│ ├── adaptation/ # Pillar III — Real-time reconfiguration +│ │ ├── engine.py # AdaptationEngine orchestrator +│ │ ├── decision_tree.py # 6-step adaptation decision tree +│ │ ├── feature_flags.py # Feature flag manager +│ │ ├── traffic_shifter.py # Weighted route redistribution +│ │ └── scaler.py # Simulated horizontal auto-scaler +│ ├── recovery/ # Pillar IV — Tiered service restoration +│ │ ├── engine.py # RecoveryEngine (T0–T4 orchestration) +│ │ └── runbooks.py # YAML runbook executor +│ ├── evolution/ # Pillar V — Post-incident learning +│ │ ├── engine.py # EvolutionEngine orchestrator +│ │ ├── post_incident.py # Automated PIR generator +│ │ ├── tracker.py # Action item tracker +│ │ ├── patterns.py # Incident pattern matcher +│ │ └── knowledge_base.py # Lessons-learned repository +│ ├── maturity/ # Maturity assessment & CRS scoring +│ │ └── model.py # MaturityAssessor (L1–L5, gap analysis) +│ ├── dashboard/ # Control plane — FastAPI + SPA +│ │ ├── app.py # Main FastAPI application +│ │ ├── routes/ # API route handlers (status, pillars, metrics, chaos) +│ │ ├── templates/index.html # Single-page application (6 tabs) +│ │ └── static/ # CSS, JS, SVG assets +│ └── cli/ +│ └── main.py # Click CLI (status, pillars, maturity, chaos, timeline, serve) +│ +├── services/ # Five FastAPI microservices +│ ├── base.py # Shared service factory (health, metrics, CORS, logging) +│ ├── gateway/gateway_app.py # API Gateway (port 8000) +│ ├── orders/orders_app.py # Orders service (port 8001) +│ ├── payments/payments_app.py # Payments service (port 8002) +│ ├── inventory/inventory_app.py # Inventory service (port 8003) +│ └── notifications/notifications_app.py # Notifications service (port 8004) +│ +├── chaos/ # Fault injection & experiments +│ ├── injector.py # FaultInjector with auto-rollback +│ └── experiments.py # 5 pre-defined chaos experiments +│ +├── scripts/ +│ └── demo.py # Interactive end-to-end demo (Rich UI) +│ +├── tests/ +│ ├── unit/ +│ │ ├── test_core.py # Core infrastructure tests +│ │ └── test_pillars.py # Pillar unit tests +│ └── integration/ +│ └── test_full_pipeline.py # Full incident lifecycle integration test +│ +├── runbooks/ +│ └── payment_failure.yml # T0 & T1 runbooks for payment outage +│ +├── config/ +│ ├── prometheus.yml # Prometheus scrape config +│ └── grafana/provisioning/ # Grafana dashboard provisioning +│ +├── docs/ +│ ├── assets/ # Diagrams and framework documents +│ ├── blueprint.pdf # Architectural blueprint +│ └── standards/ +│ └── DOCSTRING_STANDARDS.md # Mandatory file header format +│ +├── docker-compose.yml # Full stack orchestration (8 services) +├── Dockerfile # Python 3.11-slim container image +├── pyproject.toml # Project metadata, dependencies, tool config +├── .env.example # Environment variable template +└── LICENSE # Apache 2.0 +``` + +--- + +## Contributing + +1. Fork the repository and create a feature branch +2. Follow the [Docstring Standards](docs/standards/DOCSTRING_STANDARDS.md) — every file requires the mandatory metadata header +3. Ensure your code passes linting and type checks: + ```bash + ruff check . + mypy aref/ + ``` +4. Add or update tests to cover your changes: + ```bash + pytest tests/ -v --cov=aref --cov=services + ``` +5. Open a pull request with a clear description of your changes + +### Development Notes + +- **Singletons** are accessed via `get_X()` / `reset_X()` functions (e.g. `get_config()`, `get_event_bus()`, `get_metrics_engine()`) +- **Engine pattern** — all pillar engines implement `async start()`, `async stop()`, and `get_status() -> dict` +- **Service files** follow the `{service}_app.py` naming convention +- **Event bus topics** use the `"category.event_type"` format + +--- + +## License + +Copyright © 2025 Dennis 'dnoice' Smaltz — digiSpace Technical Studio + +Licensed under the [Apache License, Version 2.0](LICENSE). + +--- + +
+ +*Built with care for the craft of resilience engineering.* + +
From 10d3f442b227193dd36fabca516f8c421eed7b9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:46:27 +0000 Subject: [PATCH 3/4] feat(dashboard): dual-theme system + interactive widget management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS v3.0.0: add [data-theme='light'] token overrides (bg/text/border/ shadow), glassmorphism backdrop-filter on .card, unified --transition at 350ms cubic-bezier, widget container styles (.widget-wrapper, .widget-drag-handle, .widget-controls, .widget-collapsed), icon-btn base, #theme-toggle / #open-customizer buttons, and full customizer panel styles (.customizer-overlay, .customizer-panel, .customizer-item, toggle-switch) - widgets.js (new): IIFE-wrapped WidgetManager — initTheme/toggleTheme with localStorage + data-theme attr, widget registry Map with localStorage persistence, HTML5 drag-and-drop reordering, toggleCollapse, toggleVisibility, openCustomizer/closeCustomizer/renderCustomizer with safe DOM construction (no innerHTML interpolation), init() wired on DOMContentLoaded; exposed as window.WidgetManager - index.html v3.0.0: #theme-toggle + #open-customizer buttons in header, pillar-cards / crs-gauge / maturity-radar wrapped in .widget-wrapper divs with drag handles + controls, #widget-customizer panel added before , widgets.js script tag before dashboard.js Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aref/dashboard/static/css/dashboard.css | 337 +++++++++++++++++++- aref/dashboard/static/js/widgets.js | 393 ++++++++++++++++++++++++ aref/dashboard/templates/index.html | 75 ++++- 3 files changed, 787 insertions(+), 18 deletions(-) create mode 100644 aref/dashboard/static/js/widgets.js diff --git a/aref/dashboard/static/css/dashboard.css b/aref/dashboard/static/css/dashboard.css index 5f15221..b68cf76 100644 --- a/aref/dashboard/static/css/dashboard.css +++ b/aref/dashboard/static/css/dashboard.css @@ -1,13 +1,13 @@ /* * ============================================================================ * ✒ Metadata -* - Title: AREF Dashboard Stylesheet (AREF Edition - v2.0) +* - Title: AREF Dashboard Stylesheet (AREF Edition - v3.0) * - File Name: dashboard.css * - Relative Path: aref/dashboard/static/css/dashboard.css * - Artifact Type: config -* - Version: 2.0.0 -* - Date: 2026-03-13 -* - Update: Thursday, March 13, 2026 +* - Version: 3.0.0 +* - Date: 2026-03-14 +* - Update: Friday, March 14, 2026 * - Author: Dennis 'dnoice' Smaltz * - A.I. Acknowledgement: Anthropic - Claude Opus 4 * - Signature: ︻デ═─── ✦ ✦ ✦ | Aim Twice, Shoot Once! @@ -138,9 +138,10 @@ --shadow-glow: 0 0 20px rgba(56, 189, 248, 0.15); /* Transitions */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1); + --transition: 350ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-fast: 350ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 350ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); } /* ---------- Reset & Base ---------- */ @@ -2147,3 +2148,325 @@ body { .stagger > *:nth-child(3) { animation-delay: 0.15s; } .stagger > *:nth-child(4) { animation-delay: 0.20s; } .stagger > *:nth-child(5) { animation-delay: 0.25s; } + +/* ============================================================ + * Light Theme Overrides + * Applied when is set by WidgetManager + * ============================================================ */ +[data-theme="light"] { + --bg-primary: #f0f4f8; + --bg-secondary: #ffffff; + --bg-card: rgba(255, 255, 255, 0.75); + --bg-card-hover: rgba(255, 255, 255, 0.92); + --bg-surface: #e8edf2; + + --text-primary: #1e293b; + --text-secondary: #475569; + --text-muted: #94a3b8; + --text-accent: #0284c7; + + --border-default: #cbd5e1; + --border-active: #94a3b8; + + --shadow-sm: 0 1px 3px rgba(71, 85, 105, 0.12), 0 1px 2px rgba(71, 85, 105, 0.08); + --shadow-md: 0 4px 8px -1px rgba(71, 85, 105, 0.15), 0 2px 4px rgba(71, 85, 105, 0.1); + --shadow-lg: 0 10px 20px -3px rgba(71, 85, 105, 0.18), 0 4px 8px rgba(71, 85, 105, 0.12); + --shadow-glow: 0 0 20px rgba(2, 132, 199, 0.12); +} + +[data-theme="light"] body { + background: var(--bg-primary); + color: var(--text-primary); +} + +[data-theme="light"] .app-sidebar { + background: #ffffff; + border-right: 1px solid var(--border-default); +} + +[data-theme="light"] .app-header { + background: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border-default); +} + +[data-theme="light"] .nav-link:hover, +[data-theme="light"] .nav-link.active { + background: rgba(2, 132, 199, 0.1); + color: var(--text-accent); +} + +/* ============================================================ + * Glassmorphism Card Surfaces (both themes) + * ============================================================ */ +.card { + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +[data-theme="light"] .card { + background: rgba(255, 255, 255, 0.75); + border-color: rgba(203, 213, 225, 0.6); + box-shadow: var(--shadow-md); +} + +[data-theme="light"] .card:hover { + background: rgba(255, 255, 255, 0.92); + box-shadow: var(--shadow-lg); +} + +[data-theme="light"] .card--glow:hover { + box-shadow: var(--shadow-glow), var(--shadow-lg); +} + +/* ============================================================ + * Widget Container System + * ============================================================ */ +.widget-wrapper { + position: relative; + transition: opacity var(--transition), transform var(--transition); +} + +.widget-wrapper[draggable="true"] { cursor: default; } + +.widget-wrapper.drag-over { + outline: 2px dashed var(--text-accent); + outline-offset: 4px; + border-radius: var(--radius-md); +} + +.widget-header { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-xs) 0 var(--space-sm); + margin-bottom: var(--space-xs); +} + +.widget-drag-handle { + cursor: grab; + font-size: 1.1rem; + color: var(--text-muted); + opacity: 0; + transition: opacity var(--transition); + user-select: none; + padding: 2px 4px; + border-radius: var(--radius-sm); +} + +.widget-wrapper:hover .widget-drag-handle { opacity: 1; } +.widget-drag-handle:active { cursor: grabbing; } + +.widget-title { + flex: 1; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); +} + +.widget-controls { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity var(--transition); + position: absolute; + top: 0; + right: 0; +} + +.widget-wrapper:hover .widget-controls { opacity: 1; } + +.widget-collapse-btn, +.widget-visibility-btn { + width: 22px; + height: 22px; + font-size: 0.85rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.widget-body { + transition: all var(--transition); + overflow: hidden; +} + +.widget-collapsed .widget-body { + display: none; +} + +.widget-collapsed .widget-collapse-btn::before { + content: '+'; +} + +body.dragging-widget, +body.dragging-widget * { cursor: grabbing !important; } + +/* ============================================================ + * Icon Button Base Style + * ============================================================ */ +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + background: transparent; + color: var(--text-secondary); + cursor: pointer; + transition: background var(--transition), color var(--transition), border-color var(--transition); + font-size: 1rem; + line-height: 1; +} + +.icon-btn:hover { + background: var(--bg-card-hover); + color: var(--text-primary); + border-color: var(--border-active); +} + +/* ============================================================ + * Theme Toggle Button (#theme-toggle) + * ============================================================ */ +#theme-toggle { + width: 36px; + height: 36px; + font-size: 1.1rem; + border-radius: var(--radius-lg); +} + +#open-customizer { + width: 36px; + height: 36px; + font-size: 1.1rem; + border-radius: var(--radius-lg); +} + +.theme-icon { line-height: 1; } + +/* ============================================================ + * Widget Customizer Panel (#widget-customizer) + * ============================================================ */ +.customizer-overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + display: flex; + justify-content: flex-end; + transition: opacity var(--transition); +} + +.customizer-overlay[hidden] { + display: none !important; +} + +.customizer-panel { + width: 300px; + height: 100%; + background: var(--bg-secondary); + border-left: 1px solid var(--border-default); + box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + overflow: hidden; + animation: slideInRight 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +.customizer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-lg); + border-bottom: 1px solid var(--border-default); + flex-shrink: 0; +} + +.customizer-header h3 { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.customizer-list { + flex: 1; + overflow-y: auto; + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.customizer-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-sm) var(--space-md); + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + gap: var(--space-sm); + transition: background var(--transition); +} + +.customizer-item:hover { background: var(--bg-card-hover); } + +.customizer-item-title { + flex: 1; + font-size: 0.875rem; + color: var(--text-primary); + font-weight: 500; +} + +.customizer-item-actions { + display: flex; + gap: 4px; + align-items: center; +} + +/* Toggle switch within customizer items */ +.toggle-switch { + position: relative; + display: inline-block; + width: 36px; + height: 20px; +} + +.toggle-switch input { opacity: 0; width: 0; height: 0; } + +.toggle-track { + position: absolute; + inset: 0; + background: var(--border-active); + border-radius: 20px; + cursor: pointer; + transition: background var(--transition); +} + +.toggle-track::before { + content: ''; + position: absolute; + width: 14px; + height: 14px; + left: 3px; + top: 3px; + background: #fff; + border-radius: 50%; + transition: transform var(--transition); +} + +.toggle-switch input:checked + .toggle-track { background: var(--text-accent); } +.toggle-switch input:checked + .toggle-track::before { transform: translateX(16px); } + +[data-theme="light"] .customizer-panel { + background: #ffffff; + box-shadow: var(--shadow-lg); +} diff --git a/aref/dashboard/static/js/widgets.js b/aref/dashboard/static/js/widgets.js new file mode 100644 index 0000000..8782686 --- /dev/null +++ b/aref/dashboard/static/js/widgets.js @@ -0,0 +1,393 @@ +/* +* ============================================================================ +* ✒ Metadata +* - Title: AREF Dashboard Widget Manager (AREF Edition - v1.0) +* - File Name: widgets.js +* - Relative Path: aref/dashboard/static/js/widgets.js +* - Artifact Type: script +* - Version: 1.0.0 +* - Date: 2026-03-14 +* - Update: Friday, March 14, 2026 +* - Author: Dennis 'dnoice' Smaltz +* - A.I. Acknowledgement: Anthropic - Claude Opus 4 +* - Signature: ︻デ═─── ✦ ✦ ✦ | Aim Twice, Shoot Once! +* +* ✒ Description: +* Widget management system for the AREF Dashboard. Provides dual-theme +* switching (dark/light), drag-and-drop widget reordering, per-widget +* collapse/expand and visibility toggling, a slide-in customizer panel, +* and full localStorage persistence of all user preferences. +* +* ✒ Key Features: +* - Feature 1: Theme management — reads/writes localStorage 'aref-theme', +* applies data-theme attribute to , updates toggle icon +* - Feature 2: Widget registry — keyed by data-widget-id, stores visible, +* collapsed, and order state; initialized from localStorage +* - Feature 3: Drag-and-drop reordering — native HTML5 drag events on +* .widget-wrapper[draggable] elements; persists new order +* - Feature 4: Collapse/expand — toggles .widget-collapsed class, persists +* - Feature 5: Visibility toggle — shows/hides widget wrappers, persists +* - Feature 6: Customizer panel — slide-in panel listing all widgets with +* visibility toggles and collapse buttons +* - Feature 7: Automatic DOM scan on DOMContentLoaded; wires all controls +* +* ✒ Usage Instructions: +* Loaded by index.html before dashboard.js: +* +* +* Widget HTML structure: +*
+*
...
+*
...
+*
+* +* WidgetManager is exposed globally: +* WidgetManager.toggleCollapse('my-widget') +* WidgetManager.toggleVisibility('my-widget') +* +* ✒ Other Important Information: +* - Storage keys: 'aref-theme' (string), 'aref-widgets' (JSON) +* - No external dependencies; pure vanilla JS +* - Compatible platforms: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ +* ---------------------------------------------------------------------------- + */ + +(function () { + 'use strict'; + + /* ------------------------------------------------------------------ */ + /* Default widget definitions (order determines initial DOM order) */ + /* ------------------------------------------------------------------ */ + const DEFAULT_WIDGETS = [ + { id: 'pillar-cards', title: 'Pillar Overview', visible: true, collapsed: false, order: 0 }, + { id: 'crs-gauge', title: 'CRS Score', visible: true, collapsed: false, order: 1 }, + { id: 'maturity-radar', title: 'Maturity Radar', visible: true, collapsed: false, order: 2 }, + ]; + + /* ------------------------------------------------------------------ */ + /* WidgetManager */ + /* ------------------------------------------------------------------ */ + const WidgetManager = { + /** @type {Map} */ + widgets: new Map(), + + /* ---- Theme ---- */ + + initTheme() { + const saved = localStorage.getItem('aref-theme') || 'dark'; + document.documentElement.setAttribute('data-theme', saved); + this._updateThemeToggleIcon(saved); + }, + + toggleTheme() { + const current = document.documentElement.getAttribute('data-theme') || 'dark'; + const next = current === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('aref-theme', next); + this._updateThemeToggleIcon(next); + }, + + _updateThemeToggleIcon(theme) { + const btn = document.getElementById('theme-toggle'); + if (!btn) return; + const icon = btn.querySelector('.theme-icon'); + if (icon) icon.textContent = theme === 'dark' ? '☀' : '🌙'; + btn.setAttribute('title', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'); + btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'); + }, + + /* ---- Widget Registry ---- */ + + _loadState() { + try { + const raw = localStorage.getItem('aref-widgets'); + if (raw) { + const saved = JSON.parse(raw); + if (Array.isArray(saved)) { + saved.forEach(w => { + if (w && w.id) this.widgets.set(w.id, w); + }); + return; + } + } + } catch (err) { /* ignore corrupt data */ } + /* Fall back to defaults */ + DEFAULT_WIDGETS.forEach(w => this.widgets.set(w.id, { ...w })); + }, + + saveState() { + const arr = Array.from(this.widgets.values()); + localStorage.setItem('aref-widgets', JSON.stringify(arr)); + }, + + /* ---- Apply saved state to DOM ---- */ + + _applyStates() { + this.widgets.forEach((state, id) => { + const el = document.querySelector(`.widget-wrapper[data-widget-id="${id}"]`); + if (!el) return; + + if (!state.visible) { + el.style.display = 'none'; + } else { + el.style.display = ''; + } + + if (state.collapsed) { + el.classList.add('widget-collapsed'); + } else { + el.classList.remove('widget-collapsed'); + } + }); + }, + + /* ---- Collapse / Expand ---- */ + + toggleCollapse(id) { + const el = document.querySelector(`.widget-wrapper[data-widget-id="${id}"]`); + if (!el) return; + + const state = this.widgets.get(id); + const nowCollapsed = !el.classList.contains('widget-collapsed'); + + el.classList.toggle('widget-collapsed', nowCollapsed); + + if (state) { + state.collapsed = nowCollapsed; + } else { + this.widgets.set(id, { id, title: id, visible: true, collapsed: nowCollapsed, order: 999 }); + } + + this.saveState(); + this.renderCustomizer(); + }, + + /* ---- Visibility Toggle ---- */ + + toggleVisibility(id) { + const el = document.querySelector(`.widget-wrapper[data-widget-id="${id}"]`); + const state = this.widgets.get(id); + const currentlyVisible = el ? el.style.display !== 'none' : (state ? state.visible : true); + const nextVisible = !currentlyVisible; + + if (el) el.style.display = nextVisible ? '' : 'none'; + + if (state) { + state.visible = nextVisible; + } else { + this.widgets.set(id, { id, title: id, visible: nextVisible, collapsed: false, order: 999 }); + } + + this.saveState(); + this.renderCustomizer(); + }, + + /* ---- Drag and Drop ---- */ + + initDragAndDrop() { + const wrappers = () => Array.from(document.querySelectorAll('.widget-wrapper[draggable="true"]')); + let dragSrc = null; + + document.addEventListener('dragstart', (e) => { + const wrapper = e.target.closest('.widget-wrapper[draggable="true"]'); + if (!wrapper) return; + dragSrc = wrapper; + document.body.classList.add('dragging-widget'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', wrapper.dataset.widgetId || ''); + setTimeout(() => wrapper.style.opacity = '0.4', 0); + }); + + document.addEventListener('dragover', (e) => { + const wrapper = e.target.closest('.widget-wrapper[draggable="true"]'); + if (!wrapper || wrapper === dragSrc) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + /* Highlight drop target */ + wrappers().forEach(w => w.classList.remove('drag-over')); + wrapper.classList.add('drag-over'); + }); + + document.addEventListener('dragleave', (e) => { + const wrapper = e.target.closest('.widget-wrapper[draggable="true"]'); + if (wrapper) wrapper.classList.remove('drag-over'); + }); + + document.addEventListener('drop', (e) => { + const target = e.target.closest('.widget-wrapper[draggable="true"]'); + if (!target || !dragSrc || target === dragSrc) return; + e.preventDefault(); + target.classList.remove('drag-over'); + + const parent = target.parentNode; + if (!parent) return; + + const allWidgets = wrappers().filter(w => w.parentNode === parent); + const srcIdx = allWidgets.indexOf(dragSrc); + const tgtIdx = allWidgets.indexOf(target); + + if (srcIdx < tgtIdx) { + parent.insertBefore(dragSrc, target.nextSibling); + } else { + parent.insertBefore(dragSrc, target); + } + + /* Update order state */ + Array.from(parent.querySelectorAll('.widget-wrapper[data-widget-id]')).forEach((el, i) => { + const id = el.dataset.widgetId; + const s = this.widgets.get(id); + if (s) s.order = i; + }); + this.saveState(); + }); + + document.addEventListener('dragend', (e) => { + document.body.classList.remove('dragging-widget'); + wrappers().forEach(w => { + w.style.opacity = ''; + w.classList.remove('drag-over'); + }); + dragSrc = null; + }); + }, + + /* ---- Customizer Panel ---- */ + + openCustomizer() { + const panel = document.getElementById('widget-customizer'); + if (panel) panel.removeAttribute('hidden'); + this.renderCustomizer(); + }, + + closeCustomizer() { + const panel = document.getElementById('widget-customizer'); + if (panel) panel.setAttribute('hidden', ''); + }, + + renderCustomizer() { + const list = document.getElementById('customizer-list'); + if (!list) return; + + list.innerHTML = ''; + + /* Merge DOM widgets not yet in registry */ + document.querySelectorAll('.widget-wrapper[data-widget-id]').forEach((el, i) => { + const id = el.dataset.widgetId; + if (!this.widgets.has(id)) { + const titleEl = el.querySelector('.widget-title'); + this.widgets.set(id, { + id, + title: titleEl ? titleEl.textContent.trim() : id, + visible: el.style.display !== 'none', + collapsed: el.classList.contains('widget-collapsed'), + order: i, + }); + } + }); + + const sorted = Array.from(this.widgets.values()).sort((a, b) => a.order - b.order); + + sorted.forEach(state => { + const item = document.createElement('div'); + item.className = 'customizer-item'; + + const titleSpan = document.createElement('span'); + titleSpan.className = 'customizer-item-title'; + titleSpan.textContent = state.title; + + const actions = document.createElement('div'); + actions.className = 'customizer-item-actions'; + + /* Visibility toggle switch */ + const label = document.createElement('label'); + label.className = 'toggle-switch'; + label.title = 'Toggle visibility'; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = state.visible; + checkbox.addEventListener('change', () => this.toggleVisibility(state.id)); + const track = document.createElement('span'); + track.className = 'toggle-track'; + label.appendChild(checkbox); + label.appendChild(track); + + /* Collapse button */ + const collapseBtn = document.createElement('button'); + collapseBtn.className = 'icon-btn widget-collapse-btn'; + collapseBtn.title = state.collapsed ? 'Expand' : 'Collapse'; + collapseBtn.textContent = state.collapsed ? '+' : '−'; + collapseBtn.addEventListener('click', () => this.toggleCollapse(state.id)); + + actions.appendChild(label); + actions.appendChild(collapseBtn); + item.appendChild(titleSpan); + item.appendChild(actions); + list.appendChild(item); + }); + }, + + /* ---- Scan DOM for widget wrappers ---- */ + + _scanWidgets() { + document.querySelectorAll('.widget-wrapper[data-widget-id]').forEach((el, i) => { + const id = el.dataset.widgetId; + if (!this.widgets.has(id)) { + const titleEl = el.querySelector('.widget-title'); + this.widgets.set(id, { + id, + title: titleEl ? titleEl.textContent.trim() : id, + visible: true, + collapsed: false, + order: i, + }); + } + }); + }, + + /* ---- Init ---- */ + + init() { + this._loadState(); + this.initTheme(); + this._scanWidgets(); + this._applyStates(); + this.initDragAndDrop(); + + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', () => this.toggleTheme()); + } + + const openBtn = document.getElementById('open-customizer'); + if (openBtn) { + openBtn.addEventListener('click', () => this.openCustomizer()); + } + + const closeBtn = document.getElementById('close-customizer'); + if (closeBtn) { + closeBtn.addEventListener('click', () => this.closeCustomizer()); + } + + /* Close overlay when clicking the backdrop */ + const overlay = document.getElementById('widget-customizer'); + if (overlay) { + overlay.addEventListener('click', (e) => { + if (e.target === overlay) this.closeCustomizer(); + }); + } + }, + }; + + /* ------------------------------------------------------------------ */ + /* Bootstrap on DOMContentLoaded */ + /* ------------------------------------------------------------------ */ + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => WidgetManager.init()); + } else { + WidgetManager.init(); + } + + /* Expose globally */ + window.WidgetManager = WidgetManager; + +}()); diff --git a/aref/dashboard/templates/index.html b/aref/dashboard/templates/index.html index a776c0d..2b59a5e 100644 --- a/aref/dashboard/templates/index.html +++ b/aref/dashboard/templates/index.html @@ -1,12 +1,12 @@ -
+
+
+ + Pillar Overview +
+ + +
+
+
+
+
+
-
-
Composite Resilience Score
-
+
+
+ + CRS Score +
+ + +
+
+
+
+
Composite Resilience Score
+
+
+
-
-
Maturity Radar
-
+
+
+ + Maturity Radar +
+ + +
+
+
+
+
Maturity Radar
+
+
+
@@ -1224,6 +1266,17 @@

Event Timeline

+ + + From b4b76c96774c648ae7350b961f8c81f55954876d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:47:46 +0000 Subject: [PATCH 4/4] =?UTF-8?q?revert:=20undo=20dashboard=20redesign=20?= =?UTF-8?q?=E2=80=94=20README=20already=20reflects=20current=20codebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dnoice <94666757+dnoice@users.noreply.github.com> --- aref/dashboard/static/css/dashboard.css | 337 +------------------- aref/dashboard/static/js/widgets.js | 393 ------------------------ aref/dashboard/templates/index.html | 75 +---- 3 files changed, 18 insertions(+), 787 deletions(-) delete mode 100644 aref/dashboard/static/js/widgets.js diff --git a/aref/dashboard/static/css/dashboard.css b/aref/dashboard/static/css/dashboard.css index b68cf76..5f15221 100644 --- a/aref/dashboard/static/css/dashboard.css +++ b/aref/dashboard/static/css/dashboard.css @@ -1,13 +1,13 @@ /* * ============================================================================ * ✒ Metadata -* - Title: AREF Dashboard Stylesheet (AREF Edition - v3.0) +* - Title: AREF Dashboard Stylesheet (AREF Edition - v2.0) * - File Name: dashboard.css * - Relative Path: aref/dashboard/static/css/dashboard.css * - Artifact Type: config -* - Version: 3.0.0 -* - Date: 2026-03-14 -* - Update: Friday, March 14, 2026 +* - Version: 2.0.0 +* - Date: 2026-03-13 +* - Update: Thursday, March 13, 2026 * - Author: Dennis 'dnoice' Smaltz * - A.I. Acknowledgement: Anthropic - Claude Opus 4 * - Signature: ︻デ═─── ✦ ✦ ✦ | Aim Twice, Shoot Once! @@ -138,10 +138,9 @@ --shadow-glow: 0 0 20px rgba(56, 189, 248, 0.15); /* Transitions */ - --transition: 350ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-fast: 350ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-normal: 350ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1); } /* ---------- Reset & Base ---------- */ @@ -2148,325 +2147,3 @@ body { .stagger > *:nth-child(3) { animation-delay: 0.15s; } .stagger > *:nth-child(4) { animation-delay: 0.20s; } .stagger > *:nth-child(5) { animation-delay: 0.25s; } - -/* ============================================================ - * Light Theme Overrides - * Applied when is set by WidgetManager - * ============================================================ */ -[data-theme="light"] { - --bg-primary: #f0f4f8; - --bg-secondary: #ffffff; - --bg-card: rgba(255, 255, 255, 0.75); - --bg-card-hover: rgba(255, 255, 255, 0.92); - --bg-surface: #e8edf2; - - --text-primary: #1e293b; - --text-secondary: #475569; - --text-muted: #94a3b8; - --text-accent: #0284c7; - - --border-default: #cbd5e1; - --border-active: #94a3b8; - - --shadow-sm: 0 1px 3px rgba(71, 85, 105, 0.12), 0 1px 2px rgba(71, 85, 105, 0.08); - --shadow-md: 0 4px 8px -1px rgba(71, 85, 105, 0.15), 0 2px 4px rgba(71, 85, 105, 0.1); - --shadow-lg: 0 10px 20px -3px rgba(71, 85, 105, 0.18), 0 4px 8px rgba(71, 85, 105, 0.12); - --shadow-glow: 0 0 20px rgba(2, 132, 199, 0.12); -} - -[data-theme="light"] body { - background: var(--bg-primary); - color: var(--text-primary); -} - -[data-theme="light"] .app-sidebar { - background: #ffffff; - border-right: 1px solid var(--border-default); -} - -[data-theme="light"] .app-header { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border-default); -} - -[data-theme="light"] .nav-link:hover, -[data-theme="light"] .nav-link.active { - background: rgba(2, 132, 199, 0.1); - color: var(--text-accent); -} - -/* ============================================================ - * Glassmorphism Card Surfaces (both themes) - * ============================================================ */ -.card { - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); -} - -[data-theme="light"] .card { - background: rgba(255, 255, 255, 0.75); - border-color: rgba(203, 213, 225, 0.6); - box-shadow: var(--shadow-md); -} - -[data-theme="light"] .card:hover { - background: rgba(255, 255, 255, 0.92); - box-shadow: var(--shadow-lg); -} - -[data-theme="light"] .card--glow:hover { - box-shadow: var(--shadow-glow), var(--shadow-lg); -} - -/* ============================================================ - * Widget Container System - * ============================================================ */ -.widget-wrapper { - position: relative; - transition: opacity var(--transition), transform var(--transition); -} - -.widget-wrapper[draggable="true"] { cursor: default; } - -.widget-wrapper.drag-over { - outline: 2px dashed var(--text-accent); - outline-offset: 4px; - border-radius: var(--radius-md); -} - -.widget-header { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-xs) 0 var(--space-sm); - margin-bottom: var(--space-xs); -} - -.widget-drag-handle { - cursor: grab; - font-size: 1.1rem; - color: var(--text-muted); - opacity: 0; - transition: opacity var(--transition); - user-select: none; - padding: 2px 4px; - border-radius: var(--radius-sm); -} - -.widget-wrapper:hover .widget-drag-handle { opacity: 1; } -.widget-drag-handle:active { cursor: grabbing; } - -.widget-title { - flex: 1; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -.widget-controls { - display: flex; - gap: 4px; - opacity: 0; - transition: opacity var(--transition); - position: absolute; - top: 0; - right: 0; -} - -.widget-wrapper:hover .widget-controls { opacity: 1; } - -.widget-collapse-btn, -.widget-visibility-btn { - width: 22px; - height: 22px; - font-size: 0.85rem; - line-height: 1; - display: flex; - align-items: center; - justify-content: center; -} - -.widget-body { - transition: all var(--transition); - overflow: hidden; -} - -.widget-collapsed .widget-body { - display: none; -} - -.widget-collapsed .widget-collapse-btn::before { - content: '+'; -} - -body.dragging-widget, -body.dragging-widget * { cursor: grabbing !important; } - -/* ============================================================ - * Icon Button Base Style - * ============================================================ */ -.icon-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - padding: 0; - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - background: transparent; - color: var(--text-secondary); - cursor: pointer; - transition: background var(--transition), color var(--transition), border-color var(--transition); - font-size: 1rem; - line-height: 1; -} - -.icon-btn:hover { - background: var(--bg-card-hover); - color: var(--text-primary); - border-color: var(--border-active); -} - -/* ============================================================ - * Theme Toggle Button (#theme-toggle) - * ============================================================ */ -#theme-toggle { - width: 36px; - height: 36px; - font-size: 1.1rem; - border-radius: var(--radius-lg); -} - -#open-customizer { - width: 36px; - height: 36px; - font-size: 1.1rem; - border-radius: var(--radius-lg); -} - -.theme-icon { line-height: 1; } - -/* ============================================================ - * Widget Customizer Panel (#widget-customizer) - * ============================================================ */ -.customizer-overlay { - position: fixed; - inset: 0; - z-index: 1000; - background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(4px); - display: flex; - justify-content: flex-end; - transition: opacity var(--transition); -} - -.customizer-overlay[hidden] { - display: none !important; -} - -.customizer-panel { - width: 300px; - height: 100%; - background: var(--bg-secondary); - border-left: 1px solid var(--border-default); - box-shadow: var(--shadow-lg); - display: flex; - flex-direction: column; - overflow: hidden; - animation: slideInRight 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards; -} - -.customizer-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-lg); - border-bottom: 1px solid var(--border-default); - flex-shrink: 0; -} - -.customizer-header h3 { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin: 0; -} - -.customizer-list { - flex: 1; - overflow-y: auto; - padding: var(--space-md); - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.customizer-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-sm) var(--space-md); - background: var(--bg-card); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - gap: var(--space-sm); - transition: background var(--transition); -} - -.customizer-item:hover { background: var(--bg-card-hover); } - -.customizer-item-title { - flex: 1; - font-size: 0.875rem; - color: var(--text-primary); - font-weight: 500; -} - -.customizer-item-actions { - display: flex; - gap: 4px; - align-items: center; -} - -/* Toggle switch within customizer items */ -.toggle-switch { - position: relative; - display: inline-block; - width: 36px; - height: 20px; -} - -.toggle-switch input { opacity: 0; width: 0; height: 0; } - -.toggle-track { - position: absolute; - inset: 0; - background: var(--border-active); - border-radius: 20px; - cursor: pointer; - transition: background var(--transition); -} - -.toggle-track::before { - content: ''; - position: absolute; - width: 14px; - height: 14px; - left: 3px; - top: 3px; - background: #fff; - border-radius: 50%; - transition: transform var(--transition); -} - -.toggle-switch input:checked + .toggle-track { background: var(--text-accent); } -.toggle-switch input:checked + .toggle-track::before { transform: translateX(16px); } - -[data-theme="light"] .customizer-panel { - background: #ffffff; - box-shadow: var(--shadow-lg); -} diff --git a/aref/dashboard/static/js/widgets.js b/aref/dashboard/static/js/widgets.js deleted file mode 100644 index 8782686..0000000 --- a/aref/dashboard/static/js/widgets.js +++ /dev/null @@ -1,393 +0,0 @@ -/* -* ============================================================================ -* ✒ Metadata -* - Title: AREF Dashboard Widget Manager (AREF Edition - v1.0) -* - File Name: widgets.js -* - Relative Path: aref/dashboard/static/js/widgets.js -* - Artifact Type: script -* - Version: 1.0.0 -* - Date: 2026-03-14 -* - Update: Friday, March 14, 2026 -* - Author: Dennis 'dnoice' Smaltz -* - A.I. Acknowledgement: Anthropic - Claude Opus 4 -* - Signature: ︻デ═─── ✦ ✦ ✦ | Aim Twice, Shoot Once! -* -* ✒ Description: -* Widget management system for the AREF Dashboard. Provides dual-theme -* switching (dark/light), drag-and-drop widget reordering, per-widget -* collapse/expand and visibility toggling, a slide-in customizer panel, -* and full localStorage persistence of all user preferences. -* -* ✒ Key Features: -* - Feature 1: Theme management — reads/writes localStorage 'aref-theme', -* applies data-theme attribute to , updates toggle icon -* - Feature 2: Widget registry — keyed by data-widget-id, stores visible, -* collapsed, and order state; initialized from localStorage -* - Feature 3: Drag-and-drop reordering — native HTML5 drag events on -* .widget-wrapper[draggable] elements; persists new order -* - Feature 4: Collapse/expand — toggles .widget-collapsed class, persists -* - Feature 5: Visibility toggle — shows/hides widget wrappers, persists -* - Feature 6: Customizer panel — slide-in panel listing all widgets with -* visibility toggles and collapse buttons -* - Feature 7: Automatic DOM scan on DOMContentLoaded; wires all controls -* -* ✒ Usage Instructions: -* Loaded by index.html before dashboard.js: -* -* -* Widget HTML structure: -*
-*
...
-*
...
-*
-* -* WidgetManager is exposed globally: -* WidgetManager.toggleCollapse('my-widget') -* WidgetManager.toggleVisibility('my-widget') -* -* ✒ Other Important Information: -* - Storage keys: 'aref-theme' (string), 'aref-widgets' (JSON) -* - No external dependencies; pure vanilla JS -* - Compatible platforms: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ -* ---------------------------------------------------------------------------- - */ - -(function () { - 'use strict'; - - /* ------------------------------------------------------------------ */ - /* Default widget definitions (order determines initial DOM order) */ - /* ------------------------------------------------------------------ */ - const DEFAULT_WIDGETS = [ - { id: 'pillar-cards', title: 'Pillar Overview', visible: true, collapsed: false, order: 0 }, - { id: 'crs-gauge', title: 'CRS Score', visible: true, collapsed: false, order: 1 }, - { id: 'maturity-radar', title: 'Maturity Radar', visible: true, collapsed: false, order: 2 }, - ]; - - /* ------------------------------------------------------------------ */ - /* WidgetManager */ - /* ------------------------------------------------------------------ */ - const WidgetManager = { - /** @type {Map} */ - widgets: new Map(), - - /* ---- Theme ---- */ - - initTheme() { - const saved = localStorage.getItem('aref-theme') || 'dark'; - document.documentElement.setAttribute('data-theme', saved); - this._updateThemeToggleIcon(saved); - }, - - toggleTheme() { - const current = document.documentElement.getAttribute('data-theme') || 'dark'; - const next = current === 'dark' ? 'light' : 'dark'; - document.documentElement.setAttribute('data-theme', next); - localStorage.setItem('aref-theme', next); - this._updateThemeToggleIcon(next); - }, - - _updateThemeToggleIcon(theme) { - const btn = document.getElementById('theme-toggle'); - if (!btn) return; - const icon = btn.querySelector('.theme-icon'); - if (icon) icon.textContent = theme === 'dark' ? '☀' : '🌙'; - btn.setAttribute('title', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'); - btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'); - }, - - /* ---- Widget Registry ---- */ - - _loadState() { - try { - const raw = localStorage.getItem('aref-widgets'); - if (raw) { - const saved = JSON.parse(raw); - if (Array.isArray(saved)) { - saved.forEach(w => { - if (w && w.id) this.widgets.set(w.id, w); - }); - return; - } - } - } catch (err) { /* ignore corrupt data */ } - /* Fall back to defaults */ - DEFAULT_WIDGETS.forEach(w => this.widgets.set(w.id, { ...w })); - }, - - saveState() { - const arr = Array.from(this.widgets.values()); - localStorage.setItem('aref-widgets', JSON.stringify(arr)); - }, - - /* ---- Apply saved state to DOM ---- */ - - _applyStates() { - this.widgets.forEach((state, id) => { - const el = document.querySelector(`.widget-wrapper[data-widget-id="${id}"]`); - if (!el) return; - - if (!state.visible) { - el.style.display = 'none'; - } else { - el.style.display = ''; - } - - if (state.collapsed) { - el.classList.add('widget-collapsed'); - } else { - el.classList.remove('widget-collapsed'); - } - }); - }, - - /* ---- Collapse / Expand ---- */ - - toggleCollapse(id) { - const el = document.querySelector(`.widget-wrapper[data-widget-id="${id}"]`); - if (!el) return; - - const state = this.widgets.get(id); - const nowCollapsed = !el.classList.contains('widget-collapsed'); - - el.classList.toggle('widget-collapsed', nowCollapsed); - - if (state) { - state.collapsed = nowCollapsed; - } else { - this.widgets.set(id, { id, title: id, visible: true, collapsed: nowCollapsed, order: 999 }); - } - - this.saveState(); - this.renderCustomizer(); - }, - - /* ---- Visibility Toggle ---- */ - - toggleVisibility(id) { - const el = document.querySelector(`.widget-wrapper[data-widget-id="${id}"]`); - const state = this.widgets.get(id); - const currentlyVisible = el ? el.style.display !== 'none' : (state ? state.visible : true); - const nextVisible = !currentlyVisible; - - if (el) el.style.display = nextVisible ? '' : 'none'; - - if (state) { - state.visible = nextVisible; - } else { - this.widgets.set(id, { id, title: id, visible: nextVisible, collapsed: false, order: 999 }); - } - - this.saveState(); - this.renderCustomizer(); - }, - - /* ---- Drag and Drop ---- */ - - initDragAndDrop() { - const wrappers = () => Array.from(document.querySelectorAll('.widget-wrapper[draggable="true"]')); - let dragSrc = null; - - document.addEventListener('dragstart', (e) => { - const wrapper = e.target.closest('.widget-wrapper[draggable="true"]'); - if (!wrapper) return; - dragSrc = wrapper; - document.body.classList.add('dragging-widget'); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', wrapper.dataset.widgetId || ''); - setTimeout(() => wrapper.style.opacity = '0.4', 0); - }); - - document.addEventListener('dragover', (e) => { - const wrapper = e.target.closest('.widget-wrapper[draggable="true"]'); - if (!wrapper || wrapper === dragSrc) return; - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - /* Highlight drop target */ - wrappers().forEach(w => w.classList.remove('drag-over')); - wrapper.classList.add('drag-over'); - }); - - document.addEventListener('dragleave', (e) => { - const wrapper = e.target.closest('.widget-wrapper[draggable="true"]'); - if (wrapper) wrapper.classList.remove('drag-over'); - }); - - document.addEventListener('drop', (e) => { - const target = e.target.closest('.widget-wrapper[draggable="true"]'); - if (!target || !dragSrc || target === dragSrc) return; - e.preventDefault(); - target.classList.remove('drag-over'); - - const parent = target.parentNode; - if (!parent) return; - - const allWidgets = wrappers().filter(w => w.parentNode === parent); - const srcIdx = allWidgets.indexOf(dragSrc); - const tgtIdx = allWidgets.indexOf(target); - - if (srcIdx < tgtIdx) { - parent.insertBefore(dragSrc, target.nextSibling); - } else { - parent.insertBefore(dragSrc, target); - } - - /* Update order state */ - Array.from(parent.querySelectorAll('.widget-wrapper[data-widget-id]')).forEach((el, i) => { - const id = el.dataset.widgetId; - const s = this.widgets.get(id); - if (s) s.order = i; - }); - this.saveState(); - }); - - document.addEventListener('dragend', (e) => { - document.body.classList.remove('dragging-widget'); - wrappers().forEach(w => { - w.style.opacity = ''; - w.classList.remove('drag-over'); - }); - dragSrc = null; - }); - }, - - /* ---- Customizer Panel ---- */ - - openCustomizer() { - const panel = document.getElementById('widget-customizer'); - if (panel) panel.removeAttribute('hidden'); - this.renderCustomizer(); - }, - - closeCustomizer() { - const panel = document.getElementById('widget-customizer'); - if (panel) panel.setAttribute('hidden', ''); - }, - - renderCustomizer() { - const list = document.getElementById('customizer-list'); - if (!list) return; - - list.innerHTML = ''; - - /* Merge DOM widgets not yet in registry */ - document.querySelectorAll('.widget-wrapper[data-widget-id]').forEach((el, i) => { - const id = el.dataset.widgetId; - if (!this.widgets.has(id)) { - const titleEl = el.querySelector('.widget-title'); - this.widgets.set(id, { - id, - title: titleEl ? titleEl.textContent.trim() : id, - visible: el.style.display !== 'none', - collapsed: el.classList.contains('widget-collapsed'), - order: i, - }); - } - }); - - const sorted = Array.from(this.widgets.values()).sort((a, b) => a.order - b.order); - - sorted.forEach(state => { - const item = document.createElement('div'); - item.className = 'customizer-item'; - - const titleSpan = document.createElement('span'); - titleSpan.className = 'customizer-item-title'; - titleSpan.textContent = state.title; - - const actions = document.createElement('div'); - actions.className = 'customizer-item-actions'; - - /* Visibility toggle switch */ - const label = document.createElement('label'); - label.className = 'toggle-switch'; - label.title = 'Toggle visibility'; - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.checked = state.visible; - checkbox.addEventListener('change', () => this.toggleVisibility(state.id)); - const track = document.createElement('span'); - track.className = 'toggle-track'; - label.appendChild(checkbox); - label.appendChild(track); - - /* Collapse button */ - const collapseBtn = document.createElement('button'); - collapseBtn.className = 'icon-btn widget-collapse-btn'; - collapseBtn.title = state.collapsed ? 'Expand' : 'Collapse'; - collapseBtn.textContent = state.collapsed ? '+' : '−'; - collapseBtn.addEventListener('click', () => this.toggleCollapse(state.id)); - - actions.appendChild(label); - actions.appendChild(collapseBtn); - item.appendChild(titleSpan); - item.appendChild(actions); - list.appendChild(item); - }); - }, - - /* ---- Scan DOM for widget wrappers ---- */ - - _scanWidgets() { - document.querySelectorAll('.widget-wrapper[data-widget-id]').forEach((el, i) => { - const id = el.dataset.widgetId; - if (!this.widgets.has(id)) { - const titleEl = el.querySelector('.widget-title'); - this.widgets.set(id, { - id, - title: titleEl ? titleEl.textContent.trim() : id, - visible: true, - collapsed: false, - order: i, - }); - } - }); - }, - - /* ---- Init ---- */ - - init() { - this._loadState(); - this.initTheme(); - this._scanWidgets(); - this._applyStates(); - this.initDragAndDrop(); - - const themeToggle = document.getElementById('theme-toggle'); - if (themeToggle) { - themeToggle.addEventListener('click', () => this.toggleTheme()); - } - - const openBtn = document.getElementById('open-customizer'); - if (openBtn) { - openBtn.addEventListener('click', () => this.openCustomizer()); - } - - const closeBtn = document.getElementById('close-customizer'); - if (closeBtn) { - closeBtn.addEventListener('click', () => this.closeCustomizer()); - } - - /* Close overlay when clicking the backdrop */ - const overlay = document.getElementById('widget-customizer'); - if (overlay) { - overlay.addEventListener('click', (e) => { - if (e.target === overlay) this.closeCustomizer(); - }); - } - }, - }; - - /* ------------------------------------------------------------------ */ - /* Bootstrap on DOMContentLoaded */ - /* ------------------------------------------------------------------ */ - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => WidgetManager.init()); - } else { - WidgetManager.init(); - } - - /* Expose globally */ - window.WidgetManager = WidgetManager; - -}()); diff --git a/aref/dashboard/templates/index.html b/aref/dashboard/templates/index.html index 2b59a5e..a776c0d 100644 --- a/aref/dashboard/templates/index.html +++ b/aref/dashboard/templates/index.html @@ -1,12 +1,12 @@ -
-
- - Pillar Overview -
- - -
-
-
-
-
-
+
-
-
- - CRS Score -
- - -
-
-
-
-
Composite Resilience Score
-
-
-
+
+
Composite Resilience Score
+
-
-
- - Maturity Radar -
- - -
-
-
-
-
Maturity Radar
-
-
-
+
+
Maturity Radar
+
@@ -1266,17 +1224,6 @@

Event Timeline

- - -