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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## v26.05.07 (2026-05-31)

### Admin dashboard — brand refresh & UI foundation

First pass of the best-in-class admin dashboard overhaul.

- **Logo-aligned theme.** The palette is retargeted to the pyfly logo's vivid
lime-green brand — accent/primary, sidebar, focus/active states and all chart
palettes are green; dark surfaces shifted from navy to a desaturated
dark-forest charcoal. Light theme accents greened to match.
- **Typography.** Switched the UI font to **Maven Pro** (rounded,
friendly-professional — matches the logo), keeping JetBrains Mono for
tabular/numeric data.
- **Stat cards.** Implemented the previously-empty overview stat-card icons
(health / beans / uptime / profiles) and refined card depth (subtle gradient,
hover lift, bolder headers).
- **Asset caching.** Admin static assets now serve with `Cache-Control: no-cache`
and the SPA injects a `?v={__version__}` query, so theme/JS updates are picked
up on upgrade instead of being served stale from the browser cache.
- **Responsive.** Verified zero horizontal overflow at mobile (390px) and desktop
(1440px); mobile drawer, stacked stat cards and horizontally-scrollable tables
all behave.
- `pyfly.__version__` kept in sync with the packaged version.

---

## v26.05.06 (2026-05-31)

### Hardening pass — framework-wide bug fixes
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "pyfly"
# CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4);
# git tag, GitHub release and human-readable display use leading-zero form
# (v26.05.04) to match the Java/.NET/Go siblings.
version = "26.5.6"
version = "26.5.7"
description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more."
readme = "README.md"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion src/pyfly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""

__version__ = "26.05.06"
__version__ = "26.05.07"
29 changes: 28 additions & 1 deletion src/pyfly/admin/adapters/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from starlette.responses import JSONResponse, Response, StreamingResponse
from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles
from starlette.types import Scope

if TYPE_CHECKING:
from pyfly.admin.config import AdminProperties
Expand All @@ -46,6 +47,21 @@
from pyfly.admin.server.instance_registry import InstanceRegistry


class _NoCacheStaticFiles(StaticFiles):
"""Serve admin assets with ``Cache-Control: no-cache`` so browsers revalidate.

The dashboard's JS is loaded as ES modules whose relative imports carry no
version query, so a plain far-future cache would serve stale views/styles
after a framework upgrade. ``no-cache`` keeps revalidation cheap (304s) while
guaranteeing updated assets are always picked up.
"""

async def get_response(self, path: str, scope: Scope) -> Response:
response: Response = await super().get_response(path, scope)
response.headers["Cache-Control"] = "no-cache"
return response


class AdminRouteBuilder:
"""Builds Starlette routes for the admin dashboard."""

Expand Down Expand Up @@ -158,7 +174,7 @@ def build_routes(self) -> list[Route | Mount]:
routes.append(
Mount(
f"{base}/static",
app=StaticFiles(packages=[("pyfly.admin", "static")]),
app=_NoCacheStaticFiles(packages=[("pyfly.admin", "static")]),
name="admin-static",
)
)
Expand Down Expand Up @@ -376,11 +392,22 @@ async def _handle_sse_server(self, request: Request) -> Response:
async def _handle_spa(self, request: Request) -> Response:
"""Serve index.html for SPA client-side routing."""
import importlib.resources
import re

from pyfly import __version__

index_path = importlib.resources.files("pyfly.admin") / "static" / "index.html"
content = index_path.read_text(encoding="utf-8")
# Inject <base> so relative URLs (static/css/*, static/js/*) resolve
# correctly regardless of whether the browser path has a trailing slash.
base_href = self._props.path.rstrip("/") + "/"
content = content.replace("<head>", f'<head>\n <base href="{base_href}">', 1)
# Version-stamp local static assets so a framework upgrade busts stale
# browser caches (otherwise a cached themes.css/admin.css/app.js keeps the
# old look after an upgrade).
content = re.sub(
r'(href|src)="(static/[^"?]+)"',
rf'\1="\2?v={__version__}"',
content,
)
return Response(content, media_type="text/html")
34 changes: 24 additions & 10 deletions src/pyfly/admin/static/css/admin.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* PyFly Admin Dashboard — Main Stylesheet */

/* ── Google Fonts ───────────────────────────────────────────────── */
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');

/* ── Reset / Base ───────────────────────────────────────────────── */
*,
Expand Down Expand Up @@ -245,29 +245,34 @@ a:hover {
position: relative;
background:
linear-gradient(
rgba(59, 130, 246, 0.015) 1px,
rgba(95, 191, 46, 0.018) 1px,
transparent 1px
),
linear-gradient(
90deg,
rgba(59, 130, 246, 0.015) 1px,
rgba(95, 191, 46, 0.018) 1px,
transparent 1px
);
background-size: 40px 40px;
}

/* ── Cards ──────────────────────────────────────────────────────── */
.admin-card {
background: var(--admin-surface);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.018), transparent 120px),
var(--admin-surface);
border: 1px solid var(--admin-border);
border-radius: var(--admin-radius-lg);
box-shadow: var(--admin-shadow);
overflow: hidden;
transition: box-shadow var(--admin-transition);
transition:
box-shadow var(--admin-transition),
border-color var(--admin-transition);
}

.admin-card:hover {
box-shadow: var(--admin-shadow-lg);
border-color: var(--admin-primary-dim);
}

.admin-card-header {
Expand All @@ -281,7 +286,8 @@ a:hover {
.admin-card-header h3 {
font-family: var(--admin-font-sans);
font-size: 0.95rem;
font-weight: 600;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--admin-text);
margin: 0;
}
Expand Down Expand Up @@ -654,20 +660,27 @@ a:hover {

/* ── Stat Cards ─────────────────────────────────────────────────── */
.stat-card {
background: var(--admin-surface);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 90px),
var(--admin-surface);
border: 1px solid var(--admin-border);
border-radius: var(--admin-radius-lg);
padding: 20px;
display: flex;
align-items: flex-start;
justify-content: space-between;
transition: all var(--admin-transition);
gap: 12px;
transition:
transform var(--admin-transition),
border-color var(--admin-transition),
box-shadow var(--admin-transition);
box-shadow: var(--admin-shadow);
}

.stat-card:hover {
border-color: var(--admin-primary-dim);
box-shadow: var(--admin-shadow-lg);
transform: translateY(-2px);
}

.stat-card-content {
Expand Down Expand Up @@ -695,13 +708,14 @@ a:hover {
}

.stat-card-icon {
width: 40px;
height: 40px;
width: 42px;
height: 42px;
border-radius: var(--admin-radius);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}

.stat-card-icon svg {
Expand Down
51 changes: 26 additions & 25 deletions src/pyfly/admin/static/css/themes.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

:root,
[data-theme="dark"] {
--admin-bg: #0c1222;
--admin-bg-subtle: #111a2e;
--admin-surface: #162032;
--admin-surface-hover: #1c2a42;
--admin-surface-active: #243352;
--admin-text: #e2e8f0;
--admin-text-secondary: #94a3b8;
--admin-text-muted: #64748b;
--admin-primary: #3b82f6;
--admin-primary-hover: #60a5fa;
--admin-primary-dim: rgba(59, 130, 246, 0.15);
--admin-bg: #0b120d;
--admin-bg-subtle: #0f1812;
--admin-surface: #141f18;
--admin-surface-hover: #1b2a20;
--admin-surface-active: #233529;
--admin-text: #e6f0e0;
--admin-text-secondary: #9db59a;
--admin-text-muted: #6b806a;
/* Brand accent — derived from the pyfly logo's vivid lime-green */
--admin-primary: #5fbf2e;
--admin-primary-hover: #7ad94a;
--admin-primary-dim: rgba(95, 191, 46, 0.16);
--admin-success: #10b981;
--admin-success-dim: rgba(16, 185, 129, 0.15);
--admin-danger: #f43f5e;
Expand All @@ -21,24 +22,24 @@
--admin-warning-dim: rgba(245, 158, 11, 0.15);
--admin-info: #06b6d4;
--admin-info-dim: rgba(6, 182, 212, 0.15);
--admin-border: #1e293b;
--admin-border-subtle: #162032;
--admin-sidebar-bg: #070d1a;
--admin-sidebar-text: #94a3b8;
--admin-sidebar-text-active: #e2e8f0;
--admin-sidebar-hover: #0f1729;
--admin-sidebar-active: #162032;
--admin-sidebar-accent: #3b82f6;
--admin-border: #213127;
--admin-border-subtle: #18241c;
--admin-sidebar-bg: #070d09;
--admin-sidebar-text: #9db59a;
--admin-sidebar-text-active: #e6f0e0;
--admin-sidebar-hover: #0e160f;
--admin-sidebar-active: #15211a;
--admin-sidebar-accent: #5fbf2e;
--admin-sidebar-width: 260px;
--admin-navbar-height: 56px;
--admin-radius: 8px;
--admin-radius-sm: 4px;
--admin-radius-lg: 12px;
--admin-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
--admin-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4);
--admin-font-sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
--admin-font-sans: 'Maven Pro', -apple-system, BlinkMacSystemFont, sans-serif;
--admin-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
--admin-card-bg: #1a2744;
--admin-card-bg: #16241a;
--admin-transition: 150ms ease;
}

Expand All @@ -51,9 +52,9 @@
--admin-text: #0f172a;
--admin-text-secondary: #475569;
--admin-text-muted: #94a3b8;
--admin-primary: #2563eb;
--admin-primary-hover: #3b82f6;
--admin-primary-dim: rgba(37, 99, 235, 0.1);
--admin-primary: #4a9e1f;
--admin-primary-hover: #5fbf2e;
--admin-primary-dim: rgba(74, 158, 31, 0.12);
--admin-success: #059669;
--admin-success-dim: rgba(5, 150, 105, 0.1);
--admin-danger: #e11d48;
Expand All @@ -69,7 +70,7 @@
--admin-sidebar-text-active: #f1f5f9;
--admin-sidebar-hover: #1e293b;
--admin-sidebar-active: #1e293b;
--admin-sidebar-accent: #3b82f6;
--admin-sidebar-accent: #5fbf2e;
--admin-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
--admin-card-bg: #ffffff;
--admin-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.1);
Expand Down
6 changes: 3 additions & 3 deletions src/pyfly/admin/static/js/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export function hexToRgba(hex, alpha) {
}

function resolveColor(colorVar) {
if (!colorVar) return cssVar('--admin-primary') || '#3b82f6';
if (!colorVar) return cssVar('--admin-primary') || '#5fbf2e';
if (colorVar.startsWith('#')) return colorVar;
return cssVar(colorVar) || cssVar('--admin-primary') || '#3b82f6';
return cssVar(colorVar) || cssVar('--admin-primary') || '#5fbf2e';
}

function themeDefaults() {
Expand Down Expand Up @@ -117,7 +117,7 @@ export function createLineChart(canvas, options = {}) {
*/
export function createBarChart(canvas, options = {}) {
const theme = themeDefaults();
const defaultColor = cssVar('--admin-primary') || '#3b82f6';
const defaultColor = cssVar('--admin-primary') || '#5fbf2e';
const colors = (options.colors || []).map(c => resolveColor(c));

const chart = new Chart(canvas, {
Expand Down
24 changes: 19 additions & 5 deletions src/pyfly/admin/static/js/views/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function formatUptime(seconds) {
/**
* Create a stat card element.
*/
function createStatCard({ label, value, subtitle, iconClass = 'primary' }) {
function createStatCard({ label, value, subtitle, iconClass = 'primary', icon = '' }) {
const card = document.createElement('div');
card.className = 'stat-card';

Expand Down Expand Up @@ -67,13 +67,22 @@ function createStatCard({ label, value, subtitle, iconClass = 'primary' }) {

card.appendChild(content);

const icon = document.createElement('div');
icon.className = `stat-card-icon ${iconClass}`;
card.appendChild(icon);
const iconEl = document.createElement('div');
iconEl.className = `stat-card-icon ${iconClass}`;
if (icon) iconEl.innerHTML = icon;
card.appendChild(iconEl);

return card;
}

// Feather-style line icons (20×20, inherit `currentColor` from the icon tint).
const STAT_ICONS = {
health: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>',
beans: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
uptime: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
profiles: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>',
};

/**
* Build a wiring progress bar item.
*/
Expand Down Expand Up @@ -182,13 +191,16 @@ export async function render(container, api) {

// 1) Health status
const healthBadge = createStatusBadge(health.status || 'UNKNOWN');
statsRow.appendChild(createStatCard({ label: 'Health Status', value: healthBadge, iconClass: 'success' }));
statsRow.appendChild(
createStatCard({ label: 'Health Status', value: healthBadge, iconClass: 'success', icon: STAT_ICONS.health }),
);

// 2) Total beans
statsRow.appendChild(createStatCard({
label: 'Total Beans',
value: String(beans.total != null ? beans.total : 0),
iconClass: 'primary',
icon: STAT_ICONS.beans,
}));

// 3) Uptime
Expand All @@ -197,6 +209,7 @@ export async function render(container, api) {
value: formatUptime(app.uptime_seconds),
subtitle: `Port ${app.web_port || 8080}`,
iconClass: 'info',
icon: STAT_ICONS.uptime,
}));

// 4) Active profiles
Expand All @@ -206,6 +219,7 @@ export async function render(container, api) {
value: profiles.length > 0 ? profiles.join(', ') : 'default',
subtitle: `Python ${app.python_version || ''}`,
iconClass: 'warning',
icon: STAT_ICONS.profiles,
}));

wrapper.appendChild(statsRow);
Expand Down
Loading
Loading