diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02a0fd1..cfb7153 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/CITATION.cff b/CITATION.cff index 3899e5c..8500a27 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -13,8 +13,8 @@ authors: given-names: "Don Michael" license: MIT repository-code: "https://github.com/dfeen87/Goodwill-KPI" -version: "1.0.0" -date-released: "2026-03-01" +version: "1.1.0" +date-released: "2026-03-04" keywords: - goodwill - KPI diff --git a/app/main.py b/app/main.py index 9692e3d..904539c 100644 --- a/app/main.py +++ b/app/main.py @@ -19,17 +19,19 @@ import csv import io +import math import os from datetime import datetime, timezone -from typing import Optional +from typing import NamedTuple, Optional from fastapi import FastAPI, Query, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse from fastapi.templating import Jinja2Templates -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from goodwill.metrics import compute_CG, compute_G, compute_UGS from goodwill import config as _cfg +from goodwill import __version__ # --------------------------------------------------------------------------- # App setup @@ -38,13 +40,32 @@ app = FastAPI( title="Goodwill KPI Dashboard", description="Read-only governance view for Goodwill metric calculations.", - version="1.0.0", + version=__version__, ) _TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "templates") templates = Jinja2Templates(directory=_TEMPLATES_DIR) +# --------------------------------------------------------------------------- +# Security headers middleware +# --------------------------------------------------------------------------- + + +@app.middleware("http") +async def add_security_headers(request: Request, call_next): + """Add standard security headers to every response.""" + response = await call_next(request) + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["Content-Security-Policy"] = ( + # 'unsafe-inline' is required for the dashboard's inline