diff --git a/CHANGELOG.md b/CHANGELOG.md index fd901fe..a5985b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C ## [Unreleased] ### Added +- `instance.support`: operator-authored HTML body rendered in a + mint-accent callout panel inside the welcome hero on `/home`, + below the Overview footnotes. Designed for a one-line invitation + pointing analysts at a chat space, mailing list, or runbook so + every user sees where to ask for help. HTML in, HTML out (same + `| safe` filter as `instance.overview`); empty default keeps the + OSS vendor-neutral. Resolved by + `app/instance_config.py::get_instance_support()`; surfaced in + `/admin/server-config` via `_KNOWN_FIELDS["instance"]` so it + appears as "Available but unset" for operators who haven't + populated it yet. Env override: `AGNES_INSTANCE_SUPPORT`. - `instance.custom_scripts`: operator-injected HTML/JS blocks rendered into every page that extends `base.html`. Each entry takes `name`, `enabled`, `placement` (`head_start` | `head_end` | `body_end`), and diff --git a/app/api/admin.py b/app/api/admin.py index 962b39a..8a6a419 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -325,6 +325,26 @@ _KNOWN_FIELDS: dict[str, dict[str, dict]] = { "capture session data). Restart required after save." ), }, + # Operator-authored Support HTML rendered inside the welcome + # hero on /home, below the operator-owned Overview footnotes. + # Resolved by `app/instance_config.py::get_instance_support()`. + # Typical content: a one-line invitation pointing at a chat + # space, mailing list, or internal runbook. Empty value = + # block hidden (OSS stays vendor-neutral). + "support": { + "kind": "string", + "hint": ( + "HTML body rendered inside the welcome hero's Support " + "block on /home (mint-accent panel below the Overview " + "footnotes). Typically a one-line invitation linking to " + "a chat space, mailing list, or runbook — e.g. " + "'
Need help? Drop into our " + "Support " + "chat space.
'. Rendered with | safe — admin trust " + "boundary (link target is operator-controlled). Empty " + "value hides the block." + ), + }, }, "data_source": { "bigquery": { diff --git a/app/instance_config.py b/app/instance_config.py index bb0af83..a639ffb 100644 --- a/app/instance_config.py +++ b/app/instance_config.py @@ -365,6 +365,28 @@ def get_instance_overview() -> str: return (raw or "").strip() +def get_instance_support() -> str: + """Operator-authored Support body rendered inside the welcome hero + on ``/home``. Same ``| safe``-filter shape as + :func:`get_instance_overview` — operators paste HTML. Distinct + config field so help/chat pointers can be updated independently + from the product framing in ``instance.overview``. + + Typical content: a one-line invitation pointing at a chat space + (Google Chat / Slack / Teams), a mailing list, or an internal + runbook. Empty default = block hidden, keeping the OSS + vendor-neutral when an instance ships without an operator-defined + support channel. + + Resolution: ``AGNES_INSTANCE_SUPPORT`` env > ``instance.support`` + YAML > ``""``. Mirrors :func:`get_instance_overview`. + """ + raw = os.environ.get("AGNES_INSTANCE_SUPPORT") + if raw is None: + raw = get_value("instance", "support", default="") + return (raw or "").strip() + + _CUSTOM_SCRIPT_PLACEMENTS = ("head_start", "head_end", "body_end") diff --git a/app/web/router.py b/app/web/router.py index b119dbc..d2e8cea 100644 --- a/app/web/router.py +++ b/app/web/router.py @@ -25,7 +25,7 @@ from app.instance_config import ( get_gws_oauth_credentials, get_home_automode_visibility, get_instance_admin_email, get_atlassian_base_url, get_instance_brand, get_workspace_dir_name, - get_instance_logo_svg, get_instance_overview, + get_instance_logo_svg, get_instance_overview, get_instance_support, get_instance_theme, get_custom_scripts, ) from app.web.connector_prompts import all_connector_prompts @@ -378,6 +378,7 @@ def _build_context( INSTANCE_COPYRIGHT = "" LOGO_SVG = get_instance_logo_svg() INSTANCE_OVERVIEW = get_instance_overview() + INSTANCE_SUPPORT = get_instance_support() TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME", "") SSH_ALIAS = "data-analyst" SERVER_HOST = os.environ.get("SERVER_HOST", "") diff --git a/app/web/templates/home_not_onboarded.html b/app/web/templates/home_not_onboarded.html index 3a6a1b7..14c34eb 100644 --- a/app/web/templates/home_not_onboarded.html +++ b/app/web/templates/home_not_onboarded.html @@ -1131,6 +1131,59 @@ font-family: var(--ds-font-mono); font-size: 11.5px; } +/* Support callout — operator-owned panel rendered inside the welcome + hero, below the Overview footnotes. Mint-tint background + brand- + accent border reads as a complementary help affordance (distinct + from the flat hairline-separated footnotes above it). Speech-bubble + icon on the left, operator HTML on the right. */ +.home-mock .home-hero-support { + margin-top: 22px; + padding: 16px 20px; + background: rgba(84, 211, 160, 0.10); + border: 1px solid rgba(84, 211, 160, 0.28); + border-radius: 12px; + display: flex; + align-items: flex-start; + gap: 14px; + position: relative; +} +.home-mock .home-hero-support-icon { + font-size: 22px; + line-height: 1.2; + flex-shrink: 0; + color: var(--ds-brand-accent); +} +.home-mock .home-hero-support-body { + font-size: 13.5px; + color: rgba(255, 255, 255, 0.92); + line-height: 1.55; + flex: 1; + min-width: 0; +} +.home-mock .home-hero-support-body p { margin: 0 0 8px; } +.home-mock .home-hero-support-body p:last-child { margin-bottom: 0; } +.home-mock .home-hero-support-body strong { color: #ffffff; font-weight: 600; } +.home-mock .home-hero-support-body a { + color: #ffffff; + text-decoration: underline; + text-decoration-color: var(--ds-brand-accent); + text-underline-offset: 2px; + font-weight: 600; +} +.home-mock .home-hero-support-body a:hover, +.home-mock .home-hero-support-body a:focus { + text-decoration-color: #ffffff; + color: #ffffff; +} +.home-mock .home-hero-support-body code { + background: rgba(15, 23, 42, 0.55); + color: #FBBF24; + border: 1px solid rgba(251, 191, 36, 0.30); + padding: 1px 6px; + border-radius: 4px; + font-family: var(--ds-font-mono); + font-size: 11.5px; +} @media (max-width: 880px) { .home-mock .home-hero-intro { padding: 32px 24px 28px; } .home-mock .home-hero-intro h1 { font-size: 26px; } @@ -2310,6 +2363,20 @@ {% if config.INSTANCE_OVERVIEW %}Free-form HTML — paragraphs, links, lists.
- # # Overview section on /home (between Getting Started and - # # Usage modes). Use for product framing, privacy posture, - # # what-data-flows summary — operator-specific copy stays - # # out of the OSS this way. HTML in, HTML out (same `| safe` - # # filter as news_intro). Empty/unset = section hidden. + # # welcome hero on /home (hairline-separated footnotes + # # below the four-pillar row). Use for product framing, + # # privacy posture, what-data-flows summary — + # # operator-specific copy stays out of the OSS this way. + # # HTML in, HTML out (same `| safe` filter as news_intro). + # # Empty/unset = block hidden. # # Env override: AGNES_INSTANCE_OVERVIEW. + # support: | # Operator-authored Support body rendered in a + #Need help? Drop into our Support chat space.
+ # # mint-accent panel inside the welcome hero on /home + # # (below the Overview footnotes). Use to point analysts + # # at a chat space, mailing list, or runbook so they know + # # where to ask for help. HTML in, HTML out (same `| safe` + # # filter as overview). Empty/unset = block hidden. + # # Env override: AGNES_INSTANCE_SUPPORT. # sync_interval: "1 hour" # Cadence shown in analyst CLAUDE.md (e.g., "1 hour", "30 minutes", "daily") # admin_email: "ops@acme.com" # Operator contact shown on /home GWS connector tile as # an "Email admin" mailto button (analysts whose operator diff --git a/tests/test_web_home_page.py b/tests/test_web_home_page.py index 5fc9bb1..8f2b9d9 100644 --- a/tests/test_web_home_page.py +++ b/tests/test_web_home_page.py @@ -398,3 +398,62 @@ def test_welcome_footnotes_hidden_when_overview_unset(fresh_db, monkeypatch): close_system_db() body = _client().get("/home", cookies={"access_token": sess}).text assert 'SUPPORT_TEST_MARKER
") + from src.db import get_system_db, close_system_db + + conn = get_system_db() + try: + _, sess = _make_user_and_session(conn) + finally: + conn.close() + close_system_db() + body = _client().get("/home", cookies={"access_token": sess}).text + assert '