From 389002fc6fa134c3b98352c192ad974d93269510 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 16 Jun 2026 14:22:19 -0400 Subject: [PATCH] feat(ui): breadcrumb navigation across nested pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a breadcrumb trail to nested views for orientation. Mechanism: a {% block breadcrumb %} slot in base.html (+ styling) and a shared crumbs() macro in templates/_macros.html; each nested page fills the block with its trail (root→current, last item is the current page). Pages without the block render no bar, so top-level nav roots stay clean. Applied to: Ansible (browse, schedules, playbook editor, run detail) + inventory (targets/groups + detail), host_agent (fleet, host detail, settings), hosts form, settings tabs (ansible/auth/notifications/plugins/ reports + plugin detail), dashboard (list, edit), and alerts (rule form, maintenance + new). Dynamic labels (host/target/run/dashboard names) come from the page context — no route changes. All 60 templates Jinja-compile. Task #873. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/host_agent/templates/host_detail.html | 2 ++ plugins/host_agent/templates/host_list.html | 2 ++ .../host_agent/templates/settings_list.html | 2 ++ steward/templates/_macros.html | 18 ++++++++++++++++++ steward/templates/alerts/maintenance.html | 2 ++ steward/templates/alerts/maintenance_form.html | 2 ++ steward/templates/alerts/rules_form.html | 2 ++ steward/templates/ansible/browse.html | 8 ++++++++ .../ansible/inventory/group_detail.html | 2 ++ .../templates/ansible/inventory/groups.html | 2 ++ .../ansible/inventory/target_detail.html | 2 ++ .../templates/ansible/inventory/targets.html | 2 ++ steward/templates/ansible/playbook_editor.html | 2 ++ steward/templates/ansible/run_detail.html | 2 ++ steward/templates/ansible/schedules.html | 2 ++ steward/templates/base.html | 8 ++++++++ steward/templates/dashboard/edit.html | 2 ++ steward/templates/dashboard/list.html | 2 ++ steward/templates/hosts/form.html | 2 ++ steward/templates/settings/ansible.html | 2 ++ steward/templates/settings/auth.html | 2 ++ steward/templates/settings/notifications.html | 2 ++ steward/templates/settings/plugin_detail.html | 2 ++ steward/templates/settings/plugins.html | 2 ++ steward/templates/settings/reports.html | 2 ++ 25 files changed, 78 insertions(+) create mode 100644 steward/templates/_macros.html diff --git a/plugins/host_agent/templates/host_detail.html b/plugins/host_agent/templates/host_detail.html index 1129759..55a0cc3 100644 --- a/plugins/host_agent/templates/host_detail.html +++ b/plugins/host_agent/templates/host_detail.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}{{ host.name }} — Host Agent — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Host Agents", "/plugins/host_agent/"), (host.name, "")]) }}{% endblock %} {% macro fmt_bps(v) %} {%- if v is none -%}— diff --git a/plugins/host_agent/templates/host_list.html b/plugins/host_agent/templates/host_list.html index 69aad8e..79d63a0 100644 --- a/plugins/host_agent/templates/host_list.html +++ b/plugins/host_agent/templates/host_list.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Host Agents — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Dashboard", "/"), ("Host Agents", "")]) }}{% endblock %} {% block content %}

Host Agents

diff --git a/plugins/host_agent/templates/settings_list.html b/plugins/host_agent/templates/settings_list.html index dc52e79..03b6958 100644 --- a/plugins/host_agent/templates/settings_list.html +++ b/plugins/host_agent/templates/settings_list.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Host Agent — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Host Agents", "/plugins/host_agent/"), ("Settings", "")]) }}{% endblock %} {% block content %}

Host Agent — Registered Hosts

diff --git a/steward/templates/_macros.html b/steward/templates/_macros.html new file mode 100644 index 0000000..4cb4b16 --- /dev/null +++ b/steward/templates/_macros.html @@ -0,0 +1,18 @@ +{# Shared template macros. Import with: {% from "_macros.html" import crumbs %} #} + +{# Breadcrumb trail. `items` is a list of (label, href) pairs, ordered root→current. + Intermediate items with a truthy href render as links; the last item (or any + item with an empty href) renders as plain current text. #} +{% macro crumbs(items) -%} + +{%- endmacro %} diff --git a/steward/templates/alerts/maintenance.html b/steward/templates/alerts/maintenance.html index 284c413..6061a7e 100644 --- a/steward/templates/alerts/maintenance.html +++ b/steward/templates/alerts/maintenance.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Maintenance Windows — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Alerts", "/alerts/"), ("Maintenance", "")]) }}{% endblock %} {% block content %}

Maintenance Windows

diff --git a/steward/templates/alerts/maintenance_form.html b/steward/templates/alerts/maintenance_form.html index 2336c2a..860102b 100644 --- a/steward/templates/alerts/maintenance_form.html +++ b/steward/templates/alerts/maintenance_form.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}New Maintenance Window — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Alerts", "/alerts/"), ("Maintenance", "/alerts/maintenance"), ("New window", "")]) }}{% endblock %} {% block content %}

New Maintenance Window

diff --git a/steward/templates/alerts/rules_form.html b/steward/templates/alerts/rules_form.html index 6805827..c44f22d 100644 --- a/steward/templates/alerts/rules_form.html +++ b/steward/templates/alerts/rules_form.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}{% if rule %}Edit Rule{% else %}New Alert Rule{% endif %} — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Alerts", "/alerts/"), ("Edit rule" if rule else "New rule", "")]) }}{% endblock %} {% block content %}

{% if rule %}Edit Rule{% else %}New Alert Rule{% endif %}

diff --git a/steward/templates/ansible/browse.html b/steward/templates/ansible/browse.html index 578bc64..afdcbcc 100644 --- a/steward/templates/ansible/browse.html +++ b/steward/templates/ansible/browse.html @@ -1,5 +1,13 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Browse Playbooks — Steward{% endblock %} +{% block breadcrumb %} +{%- if view_contents is defined -%} +{{ crumbs([("Ansible", "/ansible/"), ("Browse", "/ansible/browse"), (view_path, "")]) }} +{%- else -%} +{{ crumbs([("Ansible", "/ansible/"), ("Browse", "")]) }} +{%- endif -%} +{% endblock %} {% block content %}

Browse Playbooks

diff --git a/steward/templates/ansible/inventory/group_detail.html b/steward/templates/ansible/inventory/group_detail.html index fc35507..84e6975 100644 --- a/steward/templates/ansible/inventory/group_detail.html +++ b/steward/templates/ansible/inventory/group_detail.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Group: {{ group.name }} — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Inventory", "/ansible/inventory/targets"), ("Groups", "/ansible/inventory/groups"), (group.name, "")]) }}{% endblock %} {% block content %}

Group: {{ group.name }}

diff --git a/steward/templates/ansible/inventory/groups.html b/steward/templates/ansible/inventory/groups.html index c8c8336..da2b1e1 100644 --- a/steward/templates/ansible/inventory/groups.html +++ b/steward/templates/ansible/inventory/groups.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Inventory Groups — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Inventory", "/ansible/inventory/targets"), ("Groups", "")]) }}{% endblock %} {% block content %}

Inventory Groups

diff --git a/steward/templates/ansible/inventory/target_detail.html b/steward/templates/ansible/inventory/target_detail.html index 588c756..4837f72 100644 --- a/steward/templates/ansible/inventory/target_detail.html +++ b/steward/templates/ansible/inventory/target_detail.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Target: {{ target.name }} — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Inventory", "/ansible/inventory/targets"), ("Targets", "/ansible/inventory/targets"), (target.name, "")]) }}{% endblock %} {% block content %}

Target: {{ target.name }}

diff --git a/steward/templates/ansible/inventory/targets.html b/steward/templates/ansible/inventory/targets.html index ecfb31a..f92d3d4 100644 --- a/steward/templates/ansible/inventory/targets.html +++ b/steward/templates/ansible/inventory/targets.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Inventory Targets — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Inventory", "/ansible/inventory/targets"), ("Targets", "")]) }}{% endblock %} {% block content %}

Inventory Targets

diff --git a/steward/templates/ansible/playbook_editor.html b/steward/templates/ansible/playbook_editor.html index 24dca51..4d5d51e 100644 --- a/steward/templates/ansible/playbook_editor.html +++ b/steward/templates/ansible/playbook_editor.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}{{ "Edit" if editing else "New" }} Playbook — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Browse", "/ansible/browse"), (("Edit " ~ playbook_path) if editing else "New playbook", "")]) }}{% endblock %} {% block content %}

{{ "Edit playbook" if editing else "New playbook" }}

diff --git a/steward/templates/ansible/run_detail.html b/steward/templates/ansible/run_detail.html index 1aa094a..8f03613 100644 --- a/steward/templates/ansible/run_detail.html +++ b/steward/templates/ansible/run_detail.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Run {{ run.id[:8] }} — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Run " ~ run.id[:8], "")]) }}{% endblock %} {% block content %}
← Runs diff --git a/steward/templates/ansible/schedules.html b/steward/templates/ansible/schedules.html index ecf149b..985da9d 100644 --- a/steward/templates/ansible/schedules.html +++ b/steward/templates/ansible/schedules.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Ansible Schedules — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Ansible", "/ansible/"), ("Schedules", "")]) }}{% endblock %} {% macro fmt_interval(secs) %} {%- set m = {300: "every 5 min", 3600: "hourly", 21600: "every 6h", 43200: "every 12h", 86400: "daily", 604800: "weekly"} -%} diff --git a/steward/templates/base.html b/steward/templates/base.html index a23329d..7c082ca 100644 --- a/steward/templates/base.html +++ b/steward/templates/base.html @@ -145,6 +145,13 @@ textarea { resize: vertical; } /* Empty state */ .empty { color: var(--text-muted); font-size: 0.9rem; padding: 2rem; text-align: center; } +/* Breadcrumbs */ +.breadcrumb { display:flex; align-items:center; flex-wrap:wrap; gap:0.35rem; font-size:0.8rem; color:var(--text-dim); margin-bottom:1rem; } +.breadcrumb a { color:var(--text-muted); } +.breadcrumb a:hover { color:var(--text); } +.breadcrumb-sep { color:var(--border-mid); } +.breadcrumb-cur { color:var(--text-muted); } + /* Stat cards (dashboard) */ .stat-val { font-size: 2rem; font-weight: 700; line-height: 1; } .stat-label { font-size: 0.8rem; color: var(--text-muted); margin-left: 0.3rem; } @@ -228,6 +235,7 @@ function setTimeRange(val) {
+ {% block breadcrumb %}{% endblock %} {% if plugin_failures and session.user_role == 'admin' %}

Edit: {{ dashboard.name }}

diff --git a/steward/templates/dashboard/list.html b/steward/templates/dashboard/list.html index a9372e4..213e1ef 100644 --- a/steward/templates/dashboard/list.html +++ b/steward/templates/dashboard/list.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Dashboards — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Dashboard", "/"), ("Dashboards", "")]) }}{% endblock %} {% block content %}

Dashboards

diff --git a/steward/templates/hosts/form.html b/steward/templates/hosts/form.html index 1ff251f..e39507c 100644 --- a/steward/templates/hosts/form.html +++ b/steward/templates/hosts/form.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}{% if host %}Edit Host{% else %}New Host{% endif %} — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (("Edit " ~ host.name) if host else "New host", "")]) }}{% endblock %} {% block content %}

{% if host %}Edit Host{% else %}Add Host{% endif %}

diff --git a/steward/templates/settings/ansible.html b/steward/templates/settings/ansible.html index 64aa5a2..3861c88 100644 --- a/steward/templates/settings/ansible.html +++ b/steward/templates/settings/ansible.html @@ -1,6 +1,8 @@ {# steward/templates/settings/ansible.html #} {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Settings — Ansible — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Ansible", "")]) }}{% endblock %} {% block content %} {% set active_tab = "ansible" %} {% include "settings/_tabs.html" %} diff --git a/steward/templates/settings/auth.html b/steward/templates/settings/auth.html index 41b69c4..f273db0 100644 --- a/steward/templates/settings/auth.html +++ b/steward/templates/settings/auth.html @@ -1,6 +1,8 @@ {# steward/templates/settings/auth.html #} {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Settings — Auth — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Authentication", "")]) }}{% endblock %} {% block content %} {% set active_tab = "auth" %} {% include "settings/_tabs.html" %} diff --git a/steward/templates/settings/notifications.html b/steward/templates/settings/notifications.html index ed2d4e9..d105694 100644 --- a/steward/templates/settings/notifications.html +++ b/steward/templates/settings/notifications.html @@ -1,6 +1,8 @@ {# steward/templates/settings/notifications.html #} {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Settings — Notifications — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Notifications", "")]) }}{% endblock %} {% block content %} {% set active_tab = "notifications" %} {% include "settings/_tabs.html" %} diff --git a/steward/templates/settings/plugin_detail.html b/steward/templates/settings/plugin_detail.html index aac1c00..b0a1fd7 100644 --- a/steward/templates/settings/plugin_detail.html +++ b/steward/templates/settings/plugin_detail.html @@ -1,6 +1,8 @@ {# steward/templates/settings/plugin_detail.html #} {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}{{ plugin.get('name', plugin._dir) }} Settings — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Plugins", "/settings/plugins/"), (plugin.get('name', plugin._dir), "")]) }}{% endblock %} {% block content %} {% set active_tab = "plugins" %} {% include "settings/_tabs.html" %} diff --git a/steward/templates/settings/plugins.html b/steward/templates/settings/plugins.html index 2b2a5c6..60bd70d 100644 --- a/steward/templates/settings/plugins.html +++ b/steward/templates/settings/plugins.html @@ -1,6 +1,8 @@ {# steward/templates/settings/plugins.html #} {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Settings — Plugins — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Plugins", "")]) }}{% endblock %} {% block content %} {% set active_tab = "plugins" %} {% include "settings/_tabs.html" %} diff --git a/steward/templates/settings/reports.html b/steward/templates/settings/reports.html index 8df780b..c778f71 100644 --- a/steward/templates/settings/reports.html +++ b/steward/templates/settings/reports.html @@ -1,6 +1,8 @@ {# steward/templates/settings/reports.html #} {% extends "base.html" %} +{% from "_macros.html" import crumbs %} {% block title %}Settings — Reports — Steward{% endblock %} +{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Reports", "")]) }}{% endblock %} {% block content %} {% set active_tab = "reports" %} {% include "settings/_tabs.html" %}