{% extends "layouts/main.html" %} {% block title %}{{ page_title }} — pyvelm{% endblock %} {% block page_title %}{{ page_title }}{% endblock %} {# Module catalog (Slice 1 — read-only). Cards are grouped by category. Each card carries name, summary, dependency list, and a state badge (Installed / To upgrade / Not installed). Action buttons land in Slice 2; for now everything is informational. #} {% block content %}
{% set grouped = {} %} {% for app in catalog %} {% set _ = grouped.setdefault(app.category, []).append(app) %} {% endfor %} {% set categories = grouped.keys() | list %} {# ── Toolbar: search + state pills + category dropdown. All three filters compose with AND. Filtering is client-side (the catalog fits comfortably on one page) so there's no server round-trip; the @input handlers just toggle Alpine state and CSS classes react. #}
{# State pill group — single-select. The fourth chip ("All") resets the state filter. #}
{# Category dropdown — populated from the categories present in the rendered catalog. #}
{# Match count — updated by apply(). #}
{# Empty-results panel that shows when the filters exclude every card. #}

No modules match the current filters.

{% for category, apps in grouped.items() %}

{{ category }}

{% for app in apps %}
{% if app.icon %} {{ app.icon | safe }} {% else %} {% endif %}

{{ app.name }}

{% if app.author %}

by {{ app.author }}

{% endif %}
{# State badge — colour-coded by lifecycle stage. #} {% if app.state == "installed" %} Installed {% elif app.state == "to_upgrade" %} Upgrade → {% else %} Not installed {% endif %}
{% if app.summary %}

{{ app.summary }}

{% endif %}

Version: {% if app.installed_version and app.state == "to_upgrade" %} {{ app.installed_version }} → {{ app.available_version }} {% elif app.installed_version %} {{ app.installed_version }} {% else %} {{ app.available_version }} {% endif %}

{% if app.depends %}

Depends: {% for d in app.depends %}{{ d }}{% if not loop.last %}, {% endif %}{% endfor %}

{% endif %}
{# Action footer — install / upgrade / uninstall. Buttons POST via HTMX so the response can swap the page chrome via HX-Redirect; the styled pvConfirm dialog wraps each one with a "are you sure" prompt. #}
{% if app.state == "uninstalled" %} {% if app.deps_missing %} {% else %} {% endif %} {% elif app.state == "to_upgrade" %} {% else %} {% endif %} {# Uninstall — only on installed/to_upgrade modules, and never on `base`. The button fetches the dry-run preview first and feeds the result into a pvConfirm prompt so the user sees exactly what will be dropped before committing. #} {% if app.state != "uninstalled" and app.name != "base" %} {% endif %}
{% endfor %}
{% endfor %} {% if not catalog %}

No modules discovered. Configure module_roots on create_app() to point at a directory of modules.

{% endif %}
{% endblock %}