From a2b0ee866eb87e50dd3800f8ff316ae9ab25c5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Sun, 31 May 2026 19:57:24 +0200 Subject: [PATCH 1/2] feat(admin): logo-aligned green theme + Maven Pro + stat-card icons + asset cache-control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Begin the best-in-class admin dashboard redesign: - Theme retargeted to the pyfly logo's vivid lime-green brand: accent/primary, sidebar, focus/active states and chart palettes are now green; dark surfaces shifted from navy to a desaturated dark-forest charcoal (themes.css). Light theme accents greened too. - Typography switched to Maven Pro (rounded, friendly-professional — matches the logo), keeping JetBrains Mono for tabular/numeric data. - Implemented the previously-empty stat-card icons (health/beans/uptime/profiles) with inline Feather-style SVGs; refined card depth (subtle gradient + hover lift) and bolder Maven Pro card headers. - Admin static assets now serve with Cache-Control: no-cache and the SPA injects a ?v={__version__} query, so theme/JS changes are picked up on upgrade instead of being served stale from browser cache. CI (ruff/format/mypy --strict) green; admin+web tests pass. --- src/pyfly/admin/adapters/starlette.py | 29 +++++++++++- src/pyfly/admin/static/css/admin.css | 34 ++++++++++---- src/pyfly/admin/static/css/themes.css | 51 +++++++++++---------- src/pyfly/admin/static/js/charts.js | 6 +-- src/pyfly/admin/static/js/views/overview.js | 24 ++++++++-- 5 files changed, 100 insertions(+), 44 deletions(-) diff --git a/src/pyfly/admin/adapters/starlette.py b/src/pyfly/admin/adapters/starlette.py index d70b534..d47763d 100644 --- a/src/pyfly/admin/adapters/starlette.py +++ b/src/pyfly/admin/adapters/starlette.py @@ -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 @@ -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.""" @@ -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", ) ) @@ -376,6 +392,9 @@ 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") @@ -383,4 +402,12 @@ async def _handle_spa(self, request: Request) -> Response: # correctly regardless of whether the browser path has a trailing slash. base_href = self._props.path.rstrip("/") + "/" content = content.replace("", f'\n ', 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") diff --git a/src/pyfly/admin/static/css/admin.css b/src/pyfly/admin/static/css/admin.css index 2aca38b..b595130 100644 --- a/src/pyfly/admin/static/css/admin.css +++ b/src/pyfly/admin/static/css/admin.css @@ -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 ───────────────────────────────────────────────── */ *, @@ -245,12 +245,12 @@ 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; @@ -258,16 +258,21 @@ a:hover { /* ── 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 { @@ -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; } @@ -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 { @@ -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 { diff --git a/src/pyfly/admin/static/css/themes.css b/src/pyfly/admin/static/css/themes.css index e4e7d31..455f9b6 100644 --- a/src/pyfly/admin/static/css/themes.css +++ b/src/pyfly/admin/static/css/themes.css @@ -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; @@ -21,14 +22,14 @@ --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; @@ -36,9 +37,9 @@ --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; } @@ -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; @@ -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); diff --git a/src/pyfly/admin/static/js/charts.js b/src/pyfly/admin/static/js/charts.js index 1219a61..8158e3f 100644 --- a/src/pyfly/admin/static/js/charts.js +++ b/src/pyfly/admin/static/js/charts.js @@ -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() { @@ -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, { diff --git a/src/pyfly/admin/static/js/views/overview.js b/src/pyfly/admin/static/js/views/overview.js index 39d9a30..ea73af4 100644 --- a/src/pyfly/admin/static/js/views/overview.js +++ b/src/pyfly/admin/static/js/views/overview.js @@ -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'; @@ -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: '', + beans: '', + uptime: '', + profiles: '', +}; + /** * Build a wiring progress bar item. */ @@ -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 @@ -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 @@ -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); From 0095be3ef3eda0ca3c0b3895f2dcb17ad7aa200f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Sun, 31 May 2026 20:05:00 +0200 Subject: [PATCH 2/2] release(v26.05.07): admin dashboard brand refresh + UI foundation Bump to v26.05.07 + CHANGELOG. See the v26.05.06->07 admin UI work: logo-green theme, Maven Pro font, stat-card icons, card depth, asset cache-control/versioning, responsive-verified (390px + 1440px, no overflow). --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ pyproject.toml | 2 +- src/pyfly/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 935d97d..38e3001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 4548348..c56c60a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py index 15a42a2..86ccddd 100644 --- a/src/pyfly/__init__.py +++ b/src/pyfly/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. """PyFly — Enterprise Python Framework.""" -__version__ = "26.05.06" +__version__ = "26.05.07" diff --git a/uv.lock b/uv.lock index 6552274..216d1a6 100644 --- a/uv.lock +++ b/uv.lock @@ -1589,7 +1589,7 @@ wheels = [ [[package]] name = "pyfly" -version = "26.5.6" +version = "26.5.7" source = { editable = "." } dependencies = [ { name = "pydantic" },