Merge remote-tracking branch 'origin/main' into vr/custom-scripts-integration

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Vojtech Rysanek 2026-05-21 14:20:37 +04:00
commit 7efcb10154
9 changed files with 94 additions and 23 deletions

View file

@ -21,6 +21,12 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
`app/instance_config.py::get_custom_scripts()`; surfaced in `app/instance_config.py::get_custom_scripts()`; surfaced in
`/admin/server-config` via `_KNOWN_FIELDS["instance"]`. Example `/admin/server-config` via `_KNOWN_FIELDS["instance"]`. Example
Marker.io block in `config/instance.yaml.example`. Marker.io block in `config/instance.yaml.example`.
- 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 ·
@ -35,6 +41,12 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
bar, and per-step number badges next to each install block. bar, and per-step number badges next to each install block.
### Changed ### Changed
- Default `instance.theme` flipped from `navy` to `blue`. The brand-blue
palette is now the out-of-the-box look; `navy` (dark hero + mint-green
CTAs) is the opt-in via `AGNES_INSTANCE_THEME` / `instance.theme`
/ admin server-config. Existing instances that explicitly set `navy`
are unaffected; instances relying on the implicit default will switch
to blue.
- `/home` palette shifted from blue to green/navy: brand accent is now - `/home` palette shifted from blue to green/navy: brand accent is now
`#2ea877` (mint green) on light surfaces, hero card is navy `#2ea877` (mint green) on light surfaces, hero card is navy
`#0f1b3a`, code panels are near-black `#0c1224` with warm-yellow `#0f1b3a`, code panels are near-black `#0c1224` with warm-yellow
@ -75,6 +87,25 @@ 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
- Pre-login pages (`/login`, magic-link screens, first-time `/setup`)
now honour the configured `instance.theme`. `base_login.html` sets
`<html data-theme="...">` from `instance_theme`, additionally loads
`design-tokens.css` so the `.btn-primary` Google SSO button gets
its `--ds-primary` green fill (previously rendered as invisible
white text on a white card because the `--ds-*` tokens weren't
defined), and the navy variant flips the `.login-features` hero
panel from brand-blue `--primary` to the deep-navy gradient —
eliminating the jarring blue → navy flip after sign-in on
navy-configured instances.
- 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
@ -296,13 +298,13 @@ _KNOWN_FIELDS: dict[str, dict[str, dict]] = {
# `app/instance_config.py::get_instance_theme()`. # `app/instance_config.py::get_instance_theme()`.
"theme": { "theme": {
"kind": "select", "kind": "select",
"options": ["navy", "blue"], "options": ["blue", "navy"],
"default": "navy", "default": "blue",
"hint": ( "hint": (
"Page-hero colour scheme. `navy` (default) uses the " "Page-hero colour scheme. `blue` (default) uses the "
"dark navy hero gradient + mint-green CTAs and " "brand-blue hero + blue CTAs. `navy` opts into the "
"eyebrow accents. `blue` reverts to the pre-redesign " "darker palette with the dark navy hero gradient + "
"brand-blue hero + blue CTAs." "mint-green CTAs and eyebrow accents."
), ),
}, },
# Operator-injected HTML/JS blocks rendered into base.html. # Operator-injected HTML/JS blocks rendered into base.html.
@ -829,6 +831,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

@ -234,24 +234,25 @@ def get_instance_theme() -> str:
(`--ds-*`) flips between palettes without touching markup. (`--ds-*`) flips between palettes without touching markup.
Values: Values:
- ``navy`` current default. Dark navy hero gradient, - ``blue`` current default. Brand-blue hero gradient,
mint-green CTAs + eyebrow accents.
- ``blue`` pre-redesign palette. Brand-blue hero gradient,
blue CTAs, translucent-white eyebrow. blue CTAs, translucent-white eyebrow.
- ``navy`` darker palette opted into via server config.
Dark navy hero gradient, mint-green CTAs +
eyebrow accents.
Resolution: ``AGNES_INSTANCE_THEME`` env var Resolution: ``AGNES_INSTANCE_THEME`` env var
(Terraform-friendly) > ``instance.theme`` in instance.yaml > (Terraform-friendly) > ``instance.theme`` in instance.yaml >
default ``"navy"``. Unrecognised values fall back to ``"navy"`` default ``"blue"``. Unrecognised values fall back to ``"blue"``
so a typo doesn't silently break every page. so a typo doesn't silently break every page.
""" """
raw = os.environ.get("AGNES_INSTANCE_THEME") raw = os.environ.get("AGNES_INSTANCE_THEME")
if raw is None: if raw is None:
raw = get_value("instance", "theme", default="navy") raw = get_value("instance", "theme", default="blue")
if not isinstance(raw, str): if not isinstance(raw, str):
return "navy" return "blue"
value = raw.strip().lower() value = raw.strip().lower()
if value not in ("navy", "blue"): if value not in ("navy", "blue"):
return "navy" return "blue"
return value return value

View file

@ -491,8 +491,8 @@ def _build_context(
"workspace_dir": get_workspace_dir_name(), "workspace_dir": get_workspace_dir_name(),
# Active palette — drives `<html data-theme="...">` in # Active palette — drives `<html data-theme="...">` in
# base.html so `--ds-*` tokens flip via CSS without # base.html so `--ds-*` tokens flip via CSS without
# touching markup. "navy" (default) = current design; # touching markup. "blue" (default) = brand-blue palette;
# "blue" = pre-redesign brand. Admin toggles via # "navy" = darker opt-in palette. Admin toggles via
# /admin/server-config. # /admin/server-config.
"instance_theme": get_instance_theme(), "instance_theme": get_instance_theme(),
# Whether /home renders the "Step 3 — turn on auto-accept mode" # Whether /home renders the "Step 3 — turn on auto-accept mode"
@ -1927,9 +1927,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

@ -2788,6 +2788,18 @@ a.slack-badge:hover {
justify-content: center; justify-content: center;
} }
/* Navy theme when the instance opts in via
`AGNES_INSTANCE_THEME=navy` / `instance.theme=navy`, the
`<html data-theme="navy">` attribute is set by base_login.html and
we flip the login-page hero panel from the legacy brand-blue
`--primary` to the deep-navy gradient used by the design-system
hero so /login matches the rest of the navy-themed app. Only
`.login-features` carries a brand colour on the pre-login screens;
the rest of the login chrome is theme-neutral. */
:root[data-theme="navy"] .login-features {
background: linear-gradient(135deg, #0f1b3a 0%, #0a1430 100%);
}
.features-content { .features-content {
max-width: 480px; max-width: 480px;
} }

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="{{ instance_theme | default('navy') }}"> <html lang="en" data-theme="{{ instance_theme | default('blue') }}">
<head> <head>
{# HTML5 requires <meta charset> within the first 1024 bytes; any {# HTML5 requires <meta charset> within the first 1024 bytes; any
operator-injected snippet must come AFTER charset + viewport to operator-injected snippet must come AFTER charset + viewport to

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="{{ instance_theme | default('blue') }}">
<head> <head>
{# HTML5 requires <meta charset> within the first 1024 bytes; any {# HTML5 requires <meta charset> within the first 1024 bytes; any
operator-injected snippet must come AFTER charset + viewport. See operator-injected snippet must come AFTER charset + viewport. See
@ -13,6 +13,11 @@
{% endfor %} {% endfor %}
<title>{% block title %}Data Analyst Portal{% endblock %}</title> <title>{% block title %}Data Analyst Portal{% endblock %}</title>
<link rel="stylesheet" href="{{ static_url('style-custom.css') }}"> <link rel="stylesheet" href="{{ static_url('style-custom.css') }}">
{# Design-system tokens (`--ds-*`) — required so `.btn-primary`
(Google SSO on /login) gets its `--ds-primary` green fill;
otherwise the white button text renders invisible on a white
card. Same reason base.html loads it globally. #}
<link rel="stylesheet" href="{{ static_url('css/design-tokens.css') }}">
{% include '_theme.html' %} {% include '_theme.html' %}
{# Operator-injected scripts (placement=head_end). Mirrors base.html. #} {# Operator-injected scripts (placement=head_end). Mirrors base.html. #}
{% for s in custom_scripts | default([]) if s.placement == 'head_end' %} {% for s in custom_scripts | default([]) if s.placement == 'head_end' %}

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`;