diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38e3001..b4909a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
+## v26.05.08 (2026-05-31)
+
+### Admin dashboard — ⌘K command palette
+
+- A keyboard-first **command palette**: press ⌘K / Ctrl-K
+ (or the new navbar **Search** button) to fuzzy-filter every view plus quick
+ actions (toggle theme, wallboard mode) and jump on Enter.
+- Full keyboard navigation (↑/↓ with clamping, Enter to run, Esc to close),
+ click-to-run, and a blurred modal backdrop. Brand-green active state, Maven Pro.
+- Reuses the sidebar's navigation definition (now exported) so the palette
+ always stays in sync with the nav. The navbar Search trigger collapses to an
+ icon on mobile.
+
+---
+
## v26.05.07 (2026-05-31)
### Admin dashboard — brand refresh & UI foundation
diff --git a/pyproject.toml b/pyproject.toml
index c56c60a..0c2783e 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.7"
+version = "26.5.8"
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 86ccddd..e40a670 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.07"
+__version__ = "26.05.08"
diff --git a/src/pyfly/admin/static/css/admin.css b/src/pyfly/admin/static/css/admin.css
index b595130..7755484 100644
--- a/src/pyfly/admin/static/css/admin.css
+++ b/src/pyfly/admin/static/css/admin.css
@@ -1709,3 +1709,173 @@ body.wallboard-mode .admin-content {
opacity: 0.6;
pointer-events: none;
}
+
+/* ── Command Palette (⌘K) ───────────────────────────────────────── */
+.cmd-palette-trigger {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 34px;
+ padding: 0 10px;
+ background: var(--admin-bg-subtle);
+ border: 1px solid var(--admin-border);
+ border-radius: var(--admin-radius);
+ color: var(--admin-text-muted);
+ font-family: var(--admin-font-sans);
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: border-color var(--admin-transition), color var(--admin-transition);
+}
+
+.cmd-palette-trigger:hover {
+ border-color: var(--admin-primary-dim);
+ color: var(--admin-text-secondary);
+}
+
+.cmd-palette-trigger svg {
+ width: 15px;
+ height: 15px;
+}
+
+.cmd-palette-trigger kbd {
+ font-family: var(--admin-font-mono);
+ font-size: 0.68rem;
+ padding: 1px 6px;
+ border: 1px solid var(--admin-border);
+ border-radius: 4px;
+ background: var(--admin-surface);
+ color: var(--admin-text-muted);
+}
+
+.cmd-palette-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 12vh;
+ background: rgba(4, 8, 5, 0.55);
+ backdrop-filter: blur(3px);
+ opacity: 0;
+ transition: opacity 120ms ease;
+}
+
+.cmd-palette-overlay.open {
+ opacity: 1;
+}
+
+.cmd-palette {
+ width: min(92vw, 600px);
+ max-height: 60vh;
+ display: flex;
+ flex-direction: column;
+ background: var(--admin-card-bg);
+ border: 1px solid var(--admin-border);
+ border-radius: var(--admin-radius-lg);
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
+ overflow: hidden;
+ transform: translateY(-8px) scale(0.98);
+ transition: transform 140ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.cmd-palette-overlay.open .cmd-palette {
+ transform: translateY(0) scale(1);
+}
+
+.cmd-palette-input-wrap {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--admin-border);
+}
+
+.cmd-palette-input-wrap svg {
+ width: 18px;
+ height: 18px;
+ color: var(--admin-text-muted);
+ flex-shrink: 0;
+}
+
+.cmd-palette-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--admin-text);
+ font-family: var(--admin-font-sans);
+ font-size: 1rem;
+}
+
+.cmd-palette-input::placeholder {
+ color: var(--admin-text-muted);
+}
+
+.cmd-palette-esc {
+ font-family: var(--admin-font-mono);
+ font-size: 0.62rem;
+ padding: 2px 6px;
+ border: 1px solid var(--admin-border);
+ border-radius: 4px;
+ color: var(--admin-text-muted);
+}
+
+.cmd-palette-list {
+ overflow-y: auto;
+ padding: 6px;
+}
+
+.cmd-palette-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 12px;
+ border-radius: var(--admin-radius);
+ cursor: pointer;
+ color: var(--admin-text-secondary);
+}
+
+.cmd-palette-item.active {
+ background: var(--admin-primary-dim);
+ color: var(--admin-text);
+}
+
+.cmd-palette-item-icon {
+ width: 17px;
+ height: 17px;
+ color: var(--admin-text-muted);
+ flex-shrink: 0;
+}
+
+.cmd-palette-item.active .cmd-palette-item-icon {
+ color: var(--admin-primary);
+}
+
+.cmd-palette-item-label {
+ flex: 1;
+ font-family: var(--admin-font-sans);
+ font-size: 0.9rem;
+}
+
+.cmd-palette-item-hint {
+ font-family: var(--admin-font-mono);
+ font-size: 0.68rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--admin-text-muted);
+}
+
+.cmd-palette-empty {
+ padding: 28px;
+ text-align: center;
+ color: var(--admin-text-muted);
+ font-size: 0.9rem;
+}
+
+@media (max-width: 768px) {
+ .cmd-palette-trigger-label,
+ .cmd-palette-trigger kbd {
+ display: none;
+ }
+}
diff --git a/src/pyfly/admin/static/js/app.js b/src/pyfly/admin/static/js/app.js
index 78e771c..49bafae 100644
--- a/src/pyfly/admin/static/js/app.js
+++ b/src/pyfly/admin/static/js/app.js
@@ -8,7 +8,8 @@
import { api } from './api.js';
import { sse } from './sse.js';
-import { renderSidebar, updateSidebarActive } from './components/sidebar.js';
+import { installCommandPalette } from './components/command-palette.js';
+import { createSvgIcon, renderSidebar, updateSidebarActive } from './components/sidebar.js';
import { showToast } from './components/toast.js';
/* ── Route Registry ───────────────────────────────────────────── */
@@ -45,6 +46,7 @@ let settings = {
let currentRoute = '';
let currentCleanup = null; // Cleanup function from current view
+let commandPalette = null; // ⌘K launcher (installed in init)
/* ── DOM References ───────────────────────────────────────────── */
@@ -118,6 +120,23 @@ function renderNavbar() {
refreshLabel.textContent = `refresh: ${settings.refreshInterval / 1000}s`;
right.appendChild(refreshLabel);
+ // Command palette trigger (⌘K)
+ const searchBtn = document.createElement('button');
+ searchBtn.className = 'cmd-palette-trigger';
+ searchBtn.setAttribute('aria-label', 'Open command palette');
+ searchBtn.title = 'Search & commands (⌘K / Ctrl-K)';
+ const searchIcon = createSvgIcon('M21 21l-4.35-4.35 M11 19a8 8 0 100-16 8 8 0 000 16z');
+ searchBtn.appendChild(searchIcon);
+ const searchText = document.createElement('span');
+ searchText.className = 'cmd-palette-trigger-label';
+ searchText.textContent = 'Search';
+ searchBtn.appendChild(searchText);
+ const searchKbd = document.createElement('kbd');
+ searchKbd.textContent = '⌘K';
+ searchBtn.appendChild(searchKbd);
+ searchBtn.addEventListener('click', () => commandPalette && commandPalette.open());
+ right.appendChild(searchBtn);
+
// Theme toggle button
const themeBtn = document.createElement('button');
themeBtn.className = 'theme-toggle';
@@ -308,6 +327,13 @@ async function init() {
// Render navbar
renderNavbar();
+ // Install the ⌘K command palette
+ commandPalette = installCommandPalette({
+ onNavigate: (route) => navigateTo(route),
+ serverMode: settings.serverMode,
+ onToggleTheme: toggleTheme,
+ });
+
// Listen for hash changes
window.addEventListener('hashchange', () => {
const route = getRouteFromHash();
diff --git a/src/pyfly/admin/static/js/components/command-palette.js b/src/pyfly/admin/static/js/components/command-palette.js
new file mode 100644
index 0000000..c132094
--- /dev/null
+++ b/src/pyfly/admin/static/js/components/command-palette.js
@@ -0,0 +1,187 @@
+/**
+ * PyFly Admin — Command Palette (⌘K / Ctrl-K).
+ *
+ * A keyboard-first launcher that fuzzy-filters every view + quick action and
+ * navigates on Enter. Built with safe DOM construction (no innerHTML with data).
+ */
+
+import { createSvgIcon, ICONS, NAV_ITEMS, SERVER_ITEMS } from './sidebar.js';
+
+/**
+ * Install the command palette and its global ⌘K / Ctrl-K shortcut.
+ *
+ * @param {object} opts
+ * @param {function} opts.onNavigate Called with a route id to navigate.
+ * @param {boolean} [opts.serverMode] Include server-mode views.
+ * @param {function} [opts.onToggleTheme] Toggle dark/light.
+ * @returns {{ open: function, close: function }}
+ */
+export function installCommandPalette({ onNavigate, serverMode = false, onToggleTheme = null }) {
+ const navItems = serverMode ? [...NAV_ITEMS, ...SERVER_ITEMS] : NAV_ITEMS;
+
+ const commands = [
+ ...navItems.map((it) => ({
+ label: it.label,
+ hint: 'Go to',
+ icon: it.icon,
+ keywords: `${it.label} ${it.id} ${it.section || ''}`.toLowerCase(),
+ run: () => onNavigate(it.id),
+ })),
+ {
+ label: 'Toggle theme',
+ hint: 'Action',
+ icon: 'cog',
+ keywords: 'theme dark light mode toggle',
+ run: () => onToggleTheme && onToggleTheme(),
+ },
+ {
+ label: 'Wallboard mode',
+ hint: 'Action',
+ icon: 'chart',
+ keywords: 'wallboard fullscreen kiosk tv display',
+ run: () => onNavigate('wallboard'),
+ },
+ ];
+
+ let active = 0;
+ let filtered = commands;
+
+ // ── DOM ──────────────────────────────────────────────────────
+ const overlay = document.createElement('div');
+ overlay.className = 'cmd-palette-overlay';
+ overlay.setAttribute('role', 'dialog');
+ overlay.setAttribute('aria-modal', 'true');
+ overlay.setAttribute('aria-label', 'Command palette');
+
+ const panel = document.createElement('div');
+ panel.className = 'cmd-palette';
+
+ const inputWrap = document.createElement('div');
+ inputWrap.className = 'cmd-palette-input-wrap';
+ inputWrap.appendChild(createSvgIcon('M21 21l-4.35-4.35 M11 19a8 8 0 100-16 8 8 0 000 16z'));
+ const input = document.createElement('input');
+ input.className = 'cmd-palette-input';
+ input.type = 'text';
+ input.placeholder = 'Search views and actions…';
+ input.setAttribute('aria-label', 'Search');
+ inputWrap.appendChild(input);
+ const kbd = document.createElement('kbd');
+ kbd.className = 'cmd-palette-esc';
+ kbd.textContent = 'ESC';
+ inputWrap.appendChild(kbd);
+ panel.appendChild(inputWrap);
+
+ const list = document.createElement('div');
+ list.className = 'cmd-palette-list';
+ panel.appendChild(list);
+
+ overlay.appendChild(panel);
+
+ function renderList() {
+ list.textContent = '';
+ if (filtered.length === 0) {
+ const empty = document.createElement('div');
+ empty.className = 'cmd-palette-empty';
+ empty.textContent = 'No matching commands';
+ list.appendChild(empty);
+ return;
+ }
+ filtered.forEach((cmd, i) => {
+ const item = document.createElement('div');
+ item.className = 'cmd-palette-item' + (i === active ? ' active' : '');
+ item.setAttribute('role', 'option');
+
+ const iconData = ICONS[cmd.icon];
+ if (iconData) {
+ const ic = createSvgIcon(iconData);
+ ic.classList.add('cmd-palette-item-icon');
+ item.appendChild(ic);
+ }
+ const label = document.createElement('span');
+ label.className = 'cmd-palette-item-label';
+ label.textContent = cmd.label;
+ item.appendChild(label);
+
+ const hint = document.createElement('span');
+ hint.className = 'cmd-palette-item-hint';
+ hint.textContent = cmd.hint;
+ item.appendChild(hint);
+
+ item.addEventListener('mousemove', () => {
+ if (active !== i) { active = i; highlight(); }
+ });
+ item.addEventListener('click', () => execute(i));
+ list.appendChild(item);
+ });
+ }
+
+ function highlight() {
+ [...list.children].forEach((el, i) => el.classList.toggle('active', i === active));
+ const el = list.children[active];
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
+ }
+
+ function filter(query) {
+ const q = query.trim().toLowerCase();
+ filtered = q ? commands.filter((c) => q.split(/\s+/).every((t) => c.keywords.includes(t))) : commands;
+ active = 0;
+ renderList();
+ }
+
+ function execute(i) {
+ const cmd = filtered[i];
+ close();
+ if (cmd) cmd.run();
+ }
+
+ function open() {
+ if (overlay.classList.contains('open')) return;
+ input.value = '';
+ filter('');
+ document.body.appendChild(overlay);
+ // next frame for the transition
+ requestAnimationFrame(() => overlay.classList.add('open'));
+ input.focus();
+ }
+
+ function close() {
+ overlay.classList.remove('open');
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
+ }
+
+ // ── Events ───────────────────────────────────────────────────
+ input.addEventListener('input', () => filter(input.value));
+
+ overlay.addEventListener('mousedown', (e) => {
+ if (e.target === overlay) close();
+ });
+
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ active = Math.min(active + 1, filtered.length - 1);
+ highlight();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ active = Math.max(active - 1, 0);
+ highlight();
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ execute(active);
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ close();
+ }
+ });
+
+ // Global shortcut: ⌘K / Ctrl-K (and Ctrl-/ as an alias)
+ document.addEventListener('keydown', (e) => {
+ const meta = e.metaKey || e.ctrlKey;
+ if (meta && (e.key === 'k' || e.key === 'K')) {
+ e.preventDefault();
+ overlay.classList.contains('open') ? close() : open();
+ }
+ });
+
+ return { open, close };
+}
diff --git a/src/pyfly/admin/static/js/components/sidebar.js b/src/pyfly/admin/static/js/components/sidebar.js
index 12a2bf6..1a04aac 100644
--- a/src/pyfly/admin/static/js/components/sidebar.js
+++ b/src/pyfly/admin/static/js/components/sidebar.js
@@ -6,7 +6,7 @@
*/
/* ── SVG Icon Paths ───────────────────────────────────────────── */
-const ICONS = {
+export const ICONS = {
home: 'M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z M9 22V12h6v10',
cube: 'M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z M3.27 6.96L12 12.01l8.73-5.05 M12 22.08V12',
heart: 'M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z',
@@ -29,7 +29,7 @@ const ICONS = {
* Each item: { id, label, icon, section? }
* section creates a section header before the item.
*/
-const NAV_ITEMS = [
+export const NAV_ITEMS = [
{ id: '', label: 'Overview', icon: 'home', section: 'Dashboard' },
{ id: 'health', label: 'Health', icon: 'heart' },
{ id: 'beans', label: 'Beans', icon: 'cube', section: 'Application' },
@@ -49,7 +49,7 @@ const NAV_ITEMS = [
];
/** Navigation item only shown in server mode. */
-const SERVER_ITEMS = [
+export const SERVER_ITEMS = [
{ id: 'instances', label: 'Instances', icon: 'server', section: 'Fleet' },
];
@@ -58,7 +58,7 @@ const SERVER_ITEMS = [
* @param {string} pathData Space-separated SVG path "d" strings.
* @returns {SVGElement}
*/
-function createSvgIcon(pathData) {
+export function createSvgIcon(pathData) {
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', '18');