Marketplace: configurable 'See all curators' URL + flea-inner hero name fix (#370)

* fix(web): flea-inner skill/agent hero shows skill name, not parent plugin name

* feat(marketplace): admin-configurable 'See all curators' link URL

---------

Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com>
This commit is contained in:
minasarustamyan 2026-05-21 11:02:29 +02:00 committed by GitHub
parent 001e5ce40e
commit be9549c266
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 43 additions and 6 deletions

View file

@ -11,6 +11,12 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
## [Unreleased] ## [Unreleased]
### Added ### Added
- New `marketplace.curators_url` config item (editable via
`/admin/server-config`**Marketplace** section). Drives the
"See all curators →" link on the `/marketplace` curated-tab info
block; when empty the link is hidden (matches today's behaviour).
SSRF-guarded on save (private-IP allowlist, same posture as
`data_source.keboola.stack_url`).
- `/home` now opens with a value-first intro hero — eyebrow greeting, - `/home` now opens with a value-first intro hero — eyebrow greeting,
one-line product framing, **Set up in ~15 min** / **Just browse** one-line product framing, **Set up in ~15 min** / **Just browse**
CTAs, and a four-pillar row (Data packages · Plugins · Skills · CTAs, and a four-pillar row (Data packages · Plugins · Skills ·
@ -65,6 +71,15 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
standard step-lede size instead of the previous 13px chip. standard step-lede size instead of the previous 13px chip.
### Fixed ### Fixed
- Skill / agent detail pages nested inside a Flea Market plugin
rendered the parent plugin's title on the hero instead of the
skill/agent name. The frontend fallback chain branched on
`source === 'curated'` and so flea-inner items fell through to
`d.plugin_name`, which the inner-detail API populates with the
parent entity name. Branch now keys on the presence of an inner
segment in the URL so inner items use `d.name || innerName`
(the actual skill/agent name) and standalone flea plugins keep
their `d.plugin_name`.
- `/activity-center` audit-log hero rendered as half-width because - `/activity-center` audit-log hero rendered as half-width because
`_page_hero.html` was nested inside `<header class="obs-topbar">`, `_page_hero.html` was nested inside `<header class="obs-topbar">`,
a flex row that pinned the time-range + auto-refresh controls a flex row that pinned the time-range + auto-refresh controls

View file

@ -167,6 +167,7 @@ def _normalize_primary_key(v):
# Devin ANALYSIS_0001 on PR #141 5f649a4 review. # Devin ANALYSIS_0001 on PR #141 5f649a4 review.
_URL_BEARING_FIELDS: tuple[tuple[str, ...], ...] = ( _URL_BEARING_FIELDS: tuple[tuple[str, ...], ...] = (
("data_source", "keboola", "stack_url"), ("data_source", "keboola", "stack_url"),
("marketplace", "curators_url"),
) )
@ -257,6 +258,7 @@ _EDITABLE_SECTIONS: tuple[str, ...] = (
"corporate_memory", "corporate_memory",
"materialize", "materialize",
"guardrails", "guardrails",
"marketplace",
) )
# "Danger-zone" sections — flipping these can lock operators out (auth.*) or # "Danger-zone" sections — flipping these can lock operators out (auth.*) or
@ -811,6 +813,17 @@ _KNOWN_FIELDS: dict[str, dict[str, dict]] = {
), ),
}, },
}, },
"marketplace": {
"curators_url": {
"kind": "string",
"hint": (
"URL the 'See all curators →' link on /marketplace points to "
"(e.g. an internal wiki page listing curators accountable for "
"the curated marketplace). Empty → the link is hidden. "
"Validated against private-IP allowlist on save (SSRF guard)."
),
},
},
} }
# Keys whose values must be redacted from the audit diff. We match # Keys whose values must be redacted from the audit diff. We match

View file

@ -1921,9 +1921,12 @@ async def marketplace_listing(
): ):
import json as _json import json as _json
from src.category_icons import all_paths from src.category_icons import all_paths
from app.instance_config import get_value
curators_url = (get_value("marketplace", "curators_url") or "").strip()
ctx = _build_context( ctx = _build_context(
request, user=user, request, user=user,
category_icons_json=_json.dumps(all_paths()), category_icons_json=_json.dumps(all_paths()),
curators_url=curators_url,
) )
return templates.TemplateResponse(request, "marketplace.html", ctx) return templates.TemplateResponse(request, "marketplace.html", ctx)

View file

@ -518,7 +518,9 @@
<div class="title">Each plugin here has a named curator accountable for it.</div> <div class="title">Each plugin here has a named curator accountable for it.</div>
<div class="body">Each plugin in this marketplace has a named curator and meets a baseline review bar (security, telemetry hygiene, documentation).</div> <div class="body">Each plugin in this marketplace has a named curator and meets a baseline review bar (security, telemetry hygiene, documentation).</div>
</div> </div>
<a class="link" href="#">See all curators →</a> {% if curators_url %}
<a class="link" href="{{ curators_url }}" target="_blank" rel="noopener">See all curators →</a>
{% endif %}
</div> </div>
<!-- Flea Market info block — open-shelf signal, mirror structure of <!-- Flea Market info block — open-shelf signal, mirror structure of

View file

@ -1015,12 +1015,16 @@
const d = await res.json(); const d = await res.json();
// ── Title resolution per source ───────────────────────────────────── // ── Title resolution per source ─────────────────────────────────────
// Curated: marketplace-metadata.json `display_name` wins, else frontmatter `name`. // Curated inner + flea inner: prefer marketplace-metadata `display_name`,
// Flea standalone skill/agent reuses PluginDetailResponse — `display_name` // then frontmatter `name` (returned as `d.name`), then the URL slug.
// populated by the same on-demand parser path, otherwise `plugin_name` // Standalone flea (no innerName) falls back to `d.plugin_name` — for
// (the entity name; manifest_name is the suffixed `<name>-by-<username>`). // plugin entities that field IS the entity's user-set title. The earlier
// shape branched on `source === 'curated'` and so flea inner skills/agents
// fell through to `d.plugin_name`, which the flea inner-detail API
// populates with the *parent plugin's* entity name — so a skill nested
// inside a flea plugin rendered the plugin's title on its hero.
const heroTitle = d.display_name const heroTitle = d.display_name
|| (source === 'curated' ? (d.name || innerName) : (d.plugin_name || '')); || (innerName ? (d.name || innerName) : (d.plugin_name || ''));
document.title = `${heroTitle} — Marketplace`; document.title = `${heroTitle} — Marketplace`;