Skip to content
Open
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
12 changes: 7 additions & 5 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "Python 3",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bookworm",
// If you want packages rather than install as post commands add them as features
"customizations": {
"codespaces": {
"openFiles": [
Expand All @@ -17,10 +18,8 @@
]
}
},
"updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; [ -f requirements.txt ] && pip3 install --user -r requirements.txt; pip3 install --user streamlit; echo '✅ Packages installed and Requirements met'",
"postAttachCommand": {
"server": "streamlit run app.py --server.enableCORS false --server.enableXsrfProtection false"
},
// This command runs after creation and installs packages
"postCreateCommand": "[ -f requirements.txt ] && pip3 install --user -r requirements.txt; echo '✅ Packages installed and Requirements met'",
"portsAttributes": {
"8501": {
"label": "Application",
Expand All @@ -29,5 +28,8 @@
},
"forwardPorts": [
8501
]
],
// This command runs after the container is created, or when you attach to an existing container
// and just runs streamlit
"postAttachCommand": "streamlit run Homepage.py"
}
5 changes: 5 additions & 0 deletions .streamlit/config.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[theme]
base="light"
primaryColor="#0F62FE"

[server]
headless = true
enableXsrfProtection = false
enableCORS = false
14 changes: 7 additions & 7 deletions pages/1_Quarterly_Rankings.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,8 @@ def make_summary_html(df: pd.DataFrame,

# Build narrative paragraphs
p1 = (
f"<p><b>{prov_name}</b> achieved a <b>{pct_disp}</b> performance rate for "
f"<b>{domain} → {metric}</b> in <b>{quarter}</b> across the <b>{provider_region}</b> region. "
f"<p><b>{html.escape(prov_name)}</b> achieved a <b>{pct_disp}</b> performance rate for "
f"<b>{html.escape(domain)} → {html.escape(metric)}</b> in <b>{html.escape(quarter)}</b> across the <b>{html.escape(provider_region)}</b> region. "
f"This placed them <b>{rank_val}{'th' if rank_val else ''} nationally</b> out of {nat_n} trusts"
)

Expand Down Expand Up @@ -603,8 +603,8 @@ def make_summary_html(df: pd.DataFrame,
change_txt = f" Regional performance {direction} by {abs(dpp):.1f} percentage points compared to {prev_q}."

p1 = (
f"<p>The <b>{region_selected}</b> region achieved a weighted average performance of "
f"<b>{reg_pct_disp}</b> for <b>{domain} → {metric}</b> in <b>{quarter}</b>. "
f"<p>The <b>{html.escape(region_selected)}</b> region achieved a weighted average performance of "
f"<b>{reg_pct_disp}</b> for <b>{html.escape(domain)} → {html.escape(metric)}</b> in <b>{html.escape(quarter)}</b>. "
f"This regional view includes <b>{reg_n} trusts</b> out of <b>{nat_n}</b> trusts nationally."
f"{comparison_txt}{change_txt}</p>"
)
Expand Down Expand Up @@ -952,16 +952,16 @@ def read_svg_file(p: Path) -> str:

# ===================== Context banner ======================
context_html = (
f'Showing <b>{domain}</b> → <b>{metric}</b> in <b>{quarter}</b>'
+ (f' for <b>{region_selected}</b> region' if region_selected else ' across <b>all regions</b>')
f'Showing <b>{html.escape(domain)}</b> → <b>{html.escape(metric)}</b> in <b>{html.escape(quarter)}</b>'
+ (f' for <b>{html.escape(region_selected)}</b> region' if region_selected else ' across <b>all regions</b>')
+ '.'
)
st.markdown(f'<div id="context-banner">{context_html}</div>', unsafe_allow_html=True)

# ===================== Provider heading ====================
if provider_code:
prov_name = provider_name_from_code(df_qdmr, provider_code)
st.markdown(f"<h2 class='kpi-heading'>{prov_name} ({provider_code})</h2>", unsafe_allow_html=True)
st.markdown(f"<h2 class='kpi-heading'>{html.escape(prov_name)} ({html.escape(provider_code)})</h2>", unsafe_allow_html=True)
else:
st.info("Select a provider to see KPIs and trend.")

Expand Down
12 changes: 6 additions & 6 deletions pages/2_Monthly_Rankings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import plotly.express as px
import plotly.io as pio
import streamlit as st
import html as _html
import html
from pathlib import Path

# add (or keep) this import in the file header
Expand Down Expand Up @@ -521,16 +521,16 @@ def provider_labels(sub: pd.DataFrame) -> list:

# ===================== Context banner ======================
context_html = (
f'Showing <b>{domain}</b> → <b>{metric}</b> in <b>{month_long}</b>'
+ (f' for <b>{region_selected}</b> region' if region_selected else ' across <b>all regions</b>')
f'Showing <b>{html.escape(domain)}</b> → <b>{html.escape(metric)}</b> in <b>{html.escape(month_long)}</b>'
+ (f' for <b>{html.escape(region_selected)}</b> region' if region_selected else ' across <b>all regions</b>')
+ '.'
)
st.markdown(f'<div id="context-banner">{context_html}</div>', unsafe_allow_html=True)

# ===================== Provider heading ====================
if provider_code:
prov_name = provider_name_from_code(df_mdmr, provider_code) # Month+Domain+Metric(+Region)
st.markdown(f"<h2 class='kpi-heading'>{prov_name} ({provider_code})</h2>", unsafe_allow_html=True)
st.markdown(f"<h2 class='kpi-heading'>{html.escape(prov_name)} ({html.escape(provider_code)})</h2>", unsafe_allow_html=True)
else:
st.info("Select a provider to see KPIs and trend.")

Expand Down Expand Up @@ -861,13 +861,13 @@ def provider_labels(sub: pd.DataFrame) -> list:
card_html = (
f'<div class="progress-card">'
f' <div class="progress-head">'
f' <div class="progress-name">{_html.escape(str(m))}</div>'
f' <div class="progress-name">{html.escape(str(m))}</div>'
f' <div class="progress-percent">{pct_txt}</div>'
f' </div>'
f' <div class="progress-track"><div class="progress-fill" style="width:{width:.2f}%;"></div></div>'
f' <div class="progress-caption">'
f' <span>{nat_label}</span>'
f' <span class="muted">{_html.escape(reg_label)}</span>'
f' <span class="muted">{html.escape(reg_label)}</span>'
f' </div>'
f'</div>'
)
Expand Down
21 changes: 10 additions & 11 deletions pages/3_Compare_Providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,17 +457,17 @@ def fmt_pct(val, met):
reg_sizeB = int(dataB["Region_Size"]) if pd.notna(dataB.get("Region_Size")) else None
reg_rankB = int(dataB["Rank_Region"]) if pd.notna(dataB.get("Rank_Region")) else None

region_txt = f" within the <b>{region_selected}</b> region" if region_selected else " across all regions"
region_txt = f" within the <b>{html.escape(region_selected)}</b> region" if region_selected else " across all regions"

p1 = (
f"<p>In <b>{period}</b>, <b>{nameA} ({provA})</b> achieved a <b>{fmt_pct(pctA, metric)}</b> "
f"performance rate for <b>{domain} → {metric}</b>{region_txt}, "
f"<p>In <b>{html.escape(period)}</b>, <b>{html.escape(nameA)} ({html.escape(provA)})</b> achieved a <b>{fmt_pct(pctA, metric)}</b> "
f"performance rate for <b>{html.escape(domain)} → {html.escape(metric)}</b>{region_txt}, "
f"securing an overall national rank of <b>{rankA}</b> out of {nat_count} trusts"
)
if reg_rankA and reg_sizeA:
p1 += f" and a regional rank of <b>{reg_rankA}</b> out of {reg_sizeA} trusts"

p1 += f". By comparison, <b>{nameB} ({provB})</b> recorded <b>{fmt_pct(pctB, metric)}</b>, "
p1 += f". By comparison, <b>{html.escape(nameB)} ({html.escape(provB)})</b> recorded <b>{fmt_pct(pctB, metric)}</b>, "
p1 += f"ranking <b>{rankB}</b> nationally"

if reg_rankB and reg_sizeB:
Expand All @@ -476,8 +476,7 @@ def fmt_pct(val, met):
if pd.notna(pctA) and pd.notna(pctB):
gap = abs(pctB - pctA)
leader = nameB if pctB > pctA else nameA
p1 += f". This represents a <b>{gap:.1f} percentage point</b> gap, with {leader} demonstrating stronger performance"

p1 += f". This represents a <b>{gap:.1f} percentage point</b> gap, with {html.escape(leader)} demonstrating stronger performance"
p1 += "."

# Add weighted averages comparison
Expand All @@ -496,12 +495,12 @@ def fmt_pct(val, met):
nat_count = scope0["Provider_Code"].nunique()

p1 = (
f"<p>In <b>{period}</b>, <b>{nameA} ({provA})</b> achieved a <b>{fmt_pct(pctA, metric)}</b> "
f"performance rate for <b>{domain} → {metric}</b>, ranking <b>{rankA}</b> out of {nat_count} trusts. "
f"<p>In <b>{html.escape(period)}</b>, <b>{html.escape(nameA)} ({html.escape(provA)})</b> achieved a <b>{fmt_pct(pctA, metric)}</b> "
f"performance rate for <b>{html.escape(domain)} → {html.escape(metric)}</b>, ranking <b>{rankA}</b> out of {nat_count} trusts. "
f"<b>Provider B</b> has not been selected for comparison.</p>"
)
else:
p1 = f"<p>No provider data available for the selected filters in <b>{period}</b>.</p>"
p1 = f"<p>No provider data available for the selected filters in <b>{html.escape(period)}</b>.</p>"

# Paragraph 2: Trend analysis
period_col = "Month_dt" if mode == "Monthly" else "Quarter"
Expand All @@ -527,7 +526,7 @@ def fmt_pct(val, met):
min_pct = pcts.min()
max_pct = pcts.max()
p2_parts.append(
f"<b>{nameA} ({provA})</b> has shown performance ranging from "
f"<b>{html.escape(nameA)} ({html.escape(provA)})</b> has shown performance ranging from "
f"<b>{fmt_pct(min_pct, metric)}</b> to <b>{fmt_pct(max_pct, metric)}</b> over the preceding {lookback} {period_label}"
)

Expand All @@ -539,7 +538,7 @@ def fmt_pct(val, met):
min_pct = pcts.min()
max_pct = pcts.max()
p2_parts.append(
f"<b>{nameB} ({provB})</b> has demonstrated performance between "
f"<b>{html.escape(nameB)} ({html.escape(provB)})</b> has demonstrated performance between "
f"<b>{fmt_pct(min_pct, metric)}</b> and <b>{fmt_pct(max_pct, metric)}</b> over the same period"
)

Expand Down
4 changes: 2 additions & 2 deletions pages/4_Foundation_Group.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,9 +635,9 @@ def render_empty_state():
unsafe_allow_html=True,
)

ctx_bits = [f"{domain}", f"{metric}", f"{period}"]
ctx_bits = [f"{html.escape(domain)}", f"{html.escape(metric)}", f"{html.escape(period)}"]
if region != "(All Regions)":
ctx_bits.append(f"{region}")
ctx_bits.append(f"{html.escape(region)}")
st.markdown(
f"<div id='context-banner'>Viewing <b>{' → '.join(ctx_bits)}</b>.</div>",
unsafe_allow_html=True,
Expand Down
117 changes: 117 additions & 0 deletions setup_service.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<#
.SYNOPSIS
Sets up the Streamlit application as a Windows Service using NSSM.

.DESCRIPTION
This script installs Chocolatey (if not present), installs NSSM via Chocolatey,
and configures a Windows Service to run the Streamlit application.

.NOTES
Run this script as Administrator.
#>

# Ensure the script is run as Administrator
if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!"
Break
}

# --- Configuration ---
$ServiceName = "NOF_Dashboards"
$DisplayName = "NOF Dashboards Streamlit App"
$Description = "Runs the NOF Dashboards Streamlit application."
$AppScript = "Homepage.py"
$AppPort = 8501

# Get the script's directory (assumed to be repo root)
$RepoRoot = $PSScriptRoot

# Path to Python executable in the virtual environment
$PythonPath = Join-Path $RepoRoot ".venv\Scripts\python.exe"

# check if venv exists
if (-not (Test-Path $PythonPath)) {
Write-Warning "Virtual environment python not found at: $PythonPath"
$PythonPath = Read-Host "Please enter the full path to your Python executable"
if (-not (Test-Path $PythonPath)) {
Write-Error "Python executable not found. Please create a .venv or provide a valid path."
Exit 1
}
}

Write-Host "Using Python: $PythonPath"
Write-Host "Repo Root: $RepoRoot"

# --- Install Chocolatey (if not installed) ---
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Host "Chocolatey not found. Installing..."
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
} else {
Write-Host "Chocolatey is already installed."
}

# --- Install NSSM ---
if (-not (Get-Command nssm -ErrorAction SilentlyContinue)) {
Write-Host "NSSM not found. Installing via Chocolatey..."
choco install nssm -y
# Refresh env vars so nssm is available in current session
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
} else {
Write-Host "NSSM is already installed."
}

# Check if nssm is now available
if (-not (Get-Command nssm -ErrorAction SilentlyContinue)) {
Write-Error "NSSM installation failed or not found in PATH. Please restart the shell or install manually."
Exit 1
}

# --- Configure Service ---

# Check if service already exists
$ServiceStatus = Get-Service $ServiceName -ErrorAction SilentlyContinue
if ($ServiceStatus) {
Write-Host "Service '$ServiceName' already exists. Removing it..."
nssm stop $ServiceName
nssm remove $ServiceName confirm
}

Write-Host "Installing Service '$ServiceName'..."

# Arguments for Streamlit
# Note: absolute paths for script
$ScriptPath = Join-Path $RepoRoot $AppScript
$Arguments = "-m streamlit run `"$ScriptPath`" --server.port $AppPort --server.headless true"

# Install service
nssm install $ServiceName "$PythonPath" $Arguments

# Set additional parameters
nssm set $ServiceName DisplayName "$DisplayName"
nssm set $ServiceName Description "$Description"
nssm set $ServiceName AppDirectory "$RepoRoot"
nssm set $ServiceName Start SERVICE_AUTO_START

# Logging (Redirect stdout/stderr)
$LogDir = Join-Path $RepoRoot "logs"
if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null }

$StdoutLog = Join-Path $LogDir "service_stdout.log"
$StderrLog = Join-Path $LogDir "service_stderr.log"

nssm set $ServiceName AppStdout "$StdoutLog"
nssm set $ServiceName AppStderr "$StderrLog"
# Enable log rotation (optional but recommended)
nssm set $ServiceName AppRotateFiles 1
nssm set $ServiceName AppRotateOnline 1
nssm set $ServiceName AppRotateSeconds 86400
nssm set $ServiceName AppRotateBytes 5242880

# --- Start Service ---
Write-Host "Starting Service '$ServiceName'..."
nssm start $ServiceName

Write-Host "Service setup complete!"
Write-Host "You can access the app at http://localhost:$AppPort"

Read-Host -Prompt "Press Enter to exit"