feat(brand): wire instance.logo_svg into header brand slot (release 0.54.6) (#289)

* feat(brand): inline operator SVG logo + drop header subtitle (release 0.54.6)

Three header tweaks, one PR:

1. _app_header.html drops the small uppercase subtitle line below the
   brand. instance.subtitle still flows into the CLAUDE.md preamble +
   init welcome template ("Operated by …"); only the web header chrome
   loses it.

2. get_instance_logo_svg() in app/instance_config.py reads
   instance.logo_svg (yaml) / AGNES_INSTANCE_LOGO_SVG (env). The
   yaml field was already documented in instance.yaml.example and the
   template already supported inline <svg> via {{ config.LOGO_SVG |
   safe }}, but router.py:344 hard-coded LOGO_SVG = "" — the middle
   wire was missing. Now operators can paste a lockup directly into
   their instance.yaml under instance.logo_svg: | and have it render
   in the header. Resolution mirrors get_instance_brand (env > yaml >
   ""). instance.name remains independent: drives browser <title>
   tags + page h1s + CLAUDE.md heading; the SVG is the web-header
   visual only.

3. .app-header-logo svg gains max-height: 40px; width: auto; so any
   operator's lockup scales via its viewBox to fit the 72px header
   without per-asset width/height edits. Pairs with #2 — without the
   clamp, raw artwork (e.g. a 1600x430 lockup) overflows the chrome.

Release-cut included per the same-PR rule (Unreleased contained only
these bullets after rebase onto 0.54.5).

* revert: keep app-header-subtitle span — out of scope for this PR

Initial commit dropped the subtitle line on the assumption that
the user wanted both the secondary header line AND the future-SVG
brand cleaned up. The actual ask was narrower: drop the hostname
suffix that renders inside instance.name ("Foundry AI (hostname)"),
which is a startup.sh concern, not a template one. Restore the
subtitle span and the CHANGELOG bullet that announced its removal.
PR scope narrows to LOGO_SVG wiring + CSS clamp only.

* fix(header): hide subtitle span when instance.subtitle is empty

Pre-fix the template fell back to the literal string 'Data Analyst
Portal' when INSTANCE_SUBTITLE was unset, so operators who left the
field empty saw a stray hardcoded label below their brand. Switched
to a Jinja {% if %} guard around the whole <span class="app-header-
subtitle"> so an empty subtitle produces no element at all — clean
header chrome instead of placeholder leak.

* feat(home): hide install-hero once onboarded + X close button

- Wrap the entire install-hero in `{% if not onboarded %}` so once
  `users.onboarded=true` (auto-flipped by `agnes init` POSTing
  /api/me/onboarded, or by the new X / existing fallback button) the
  blue hero disappears entirely. Pre-PR the onboarded branch reused
  the same shell with a "Welcome back" header + "Steps 1–4 done" badge
  + minimize toggle, which visually outweighed the actual nav hub.
- Add a circular × close button (top-right of the hero, rendered only
  when not-onboarded). Click → window.confirm() asking the user to
  acknowledge onboarding → POST /api/me/onboarded → reload. The
  confirm string intentionally avoids the literal phrase
  "Mark me as offboarded" because cli/commands/onboarded.py::status
  scans /home's rendered HTML for that exact marker as a fallback for
  the api/me/profile check.
- Lift the offboard escape hatch out of the hero into a discrete
  `.offboard-strip` rendered below, gated `{% if onboarded %}`. Lets
  the analyst flip back to the install view after wiping their
  workspace folder.
- Centralize the /api/me/onboarded POST into a `postOnboarded()` JS
  helper reused by the hero X, the existing "Mark me as onboarded"
  fallback button, and the new offboard button.

Tests updated to match the new behavior:
- `test_home_onboarded_user_sees_nav_hub` — asserts the hero is gone
  and the offboard strip is the only setup-flow remnant.
- `test_minimize_toggle_no_longer_rendered` (renamed) — asserts the
  minimize toggle is absent in both states (was previously rendered
  inside the now-hidden onboarded branch of the hero).
- `test_home_no_auto_transition_after_post_until_reload` — checks
  offboard-strip presence post-flip instead of the removed
  "Welcome back" hero copy.

* fix(home): X-close button used invalid source enum, hit 422

The X button's data-target-source was 'self_acknowledged_x' to give
audit_log a separate marker for X-vs-button-driven flips. But
app/api/me.py:38's OnboardedRequest pins source to a Literal of
['agnes_init', 'self_acknowledged', 'self_unmark']  — pydantic
returned 422 on every X click.

Confusing side effect: both buttons share self-mark-status as the
status element, so the failed X click rendered 'Failed (422)' next
to the still-functional 'Mark me as onboarded' button. Looked like
the button itself broke.

Fix: drop the _x suffix. Both surfaces now POST source='self_acknowledged'.
Distinction in audit_log is not load-bearing — the source field
captures user intent ('I'm onboarded'), not the specific UI affordance.
This commit is contained in:
Vojtech 2026-05-13 21:25:46 +04:00 committed by GitHub
parent 471c63d711
commit 14ddaf1e8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 239 additions and 109 deletions

View file

@ -10,6 +10,42 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
## [Unreleased] ## [Unreleased]
## [0.54.6] — 2026-05-13
### Changed
- Header brand: wired `instance.logo_svg` (yaml) /
`AGNES_INSTANCE_LOGO_SVG` (env) into the brand slot via a new
`get_instance_logo_svg()` helper in `app/instance_config.py`.
Previously the yaml field was documented in
`config/instance.yaml.example` and the template already supported
inline SVG via `config.LOGO_SVG | safe`, but the router
hard-coded `LOGO_SVG = ""` — operators can now drop inline SVG
markup into their `instance.yaml` and have it appear in the
header. `instance.name` continues to drive browser titles and
page headings; the two fields are independent.
- Header brand: clamped `.app-header-logo svg` to `max-height: 40px;
width: auto;` (was just `display: block;`) so any operator's
`logo_svg` scales via its viewBox to fit the 72px-tall header
without per-asset width/height edits.
- Header subtitle: empty `instance.subtitle` now renders nothing
(the whole `<span class="app-header-subtitle">` is skipped)
instead of falling back to the literal placeholder string
"Data Analyst Portal". Operators who leave the field unset get a
clean header instead of a stray hardcoded label.
- `/home` install-hero now disappears entirely once the user is
onboarded (`users.onboarded=true`, set by `agnes init`'s POST to
`/api/me/onboarded` or by an explicit click). Pre-fix the hero
kept rendering a "Welcome back — you're set up" variant that
visually outweighed the actual nav hub. Adds a close (×) button
in the top-right of the hero — confirms with a `window.confirm()`
dialog asking the user to acknowledge onboarding before flipping
state, so a stray click won't hide the setup steps. The
offboarding escape hatch (previously living inside the hero's
onboarded branch) moves to a discrete strip below — visible only
when onboarded, so analysts who wipe `~/{{ workspace_dir }}` can
flip back without digging through settings.
## [0.54.5] — 2026-05-13 ## [0.54.5] — 2026-05-13
### Internal ### Internal

View file

@ -278,6 +278,23 @@ def get_instance_brand() -> str:
return value or "Agnes" return value or "Agnes"
def get_instance_logo_svg() -> str:
"""Raw inline ``<svg>`` markup rendered into the header brand slot
(``_app_header.html``). When non-empty, replaces the text brand in
the header typical use is a lockup that already contains the
brand wordmark. When empty, the header falls back to
:func:`get_instance_name` as text.
Resolution: ``AGNES_INSTANCE_LOGO_SVG`` env > ``instance.logo_svg``
YAML > ``""``. Mirrors :func:`get_instance_brand` so Terraform env
overrides work the same way.
"""
raw = os.environ.get("AGNES_INSTANCE_LOGO_SVG")
if raw is None:
raw = get_value("instance", "logo_svg", default="")
return (raw or "").strip()
def get_workspace_dir_name() -> str: def get_workspace_dir_name() -> str:
"""Filesystem-safe folder name for the analyst's local workspace """Filesystem-safe folder name for the analyst's local workspace
(``~/<workspace_dir_name>``). Defaults to :func:`get_instance_brand` (``~/<workspace_dir_name>``). Defaults to :func:`get_instance_brand`

View file

@ -25,6 +25,7 @@ from app.instance_config import (
get_gws_oauth_credentials, get_home_automode_visibility, get_gws_oauth_credentials, get_home_automode_visibility,
get_instance_admin_email, get_atlassian_base_url, get_instance_admin_email, get_atlassian_base_url,
get_instance_brand, get_workspace_dir_name, get_instance_brand, get_workspace_dir_name,
get_instance_logo_svg,
) )
from app.web.connector_prompts import all_connector_prompts from app.web.connector_prompts import all_connector_prompts
from src.repositories.sync_state import SyncStateRepository from src.repositories.sync_state import SyncStateRepository
@ -343,7 +344,7 @@ def _build_context(
INSTANCE_NAME = get_instance_name() INSTANCE_NAME = get_instance_name()
INSTANCE_SUBTITLE = get_instance_subtitle() INSTANCE_SUBTITLE = get_instance_subtitle()
INSTANCE_COPYRIGHT = "" INSTANCE_COPYRIGHT = ""
LOGO_SVG = "" LOGO_SVG = get_instance_logo_svg()
TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME", "") TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME", "")
SSH_ALIAS = "data-analyst" SSH_ALIAS = "data-analyst"
SERVER_HOST = os.environ.get("SERVER_HOST", "") SERVER_HOST = os.environ.get("SERVER_HOST", "")

View file

@ -2109,7 +2109,11 @@ a.slack-badge:hover {
font-weight: 600; font-weight: 600;
font-size: 16px; font-size: 16px;
} }
.app-header-logo svg { display: block; } .app-header-logo svg {
display: block;
max-height: 40px;
width: auto;
}
a.app-header-logo:focus-visible { a.app-header-logo:focus-visible {
outline: 2px solid var(--primary, #6366f1); outline: 2px solid var(--primary, #6366f1);
outline-offset: 2px; outline-offset: 2px;

View file

@ -6,7 +6,7 @@
<a class="app-header-logo" href="/" aria-label="Home"> <a class="app-header-logo" href="/" aria-label="Home">
{% if config.LOGO_SVG %}{{ config.LOGO_SVG | safe }}{% else %}{{ config.INSTANCE_NAME or 'Data Analyst Portal' }}{% endif %} {% if config.LOGO_SVG %}{{ config.LOGO_SVG | safe }}{% else %}{{ config.INSTANCE_NAME or 'Data Analyst Portal' }}{% endif %}
</a> </a>
<span class="app-header-subtitle">{{ config.INSTANCE_SUBTITLE or 'Data Analyst Portal' }}</span> {% if config.INSTANCE_SUBTITLE %}<span class="app-header-subtitle">{{ config.INSTANCE_SUBTITLE }}</span>{% endif %}
</div> </div>
<div class="app-header-right"> <div class="app-header-right">
{% set _path = request.url.path %} {% set _path = request.url.path %}

View file

@ -24,6 +24,7 @@
.home-mock * { box-sizing: border-box; } .home-mock * { box-sizing: border-box; }
.home-mock .install-hero { .home-mock .install-hero {
position: relative;
background: linear-gradient(135deg, #0073D1 0%, #0056A3 100%); background: linear-gradient(135deg, #0073D1 0%, #0056A3 100%);
color: white; color: white;
border-radius: 16px; border-radius: 16px;
@ -31,6 +32,51 @@
margin-bottom: 22px; margin-bottom: 22px;
box-shadow: 0 8px 24px rgba(0, 86, 163, 0.18); box-shadow: 0 8px 24px rgba(0, 86, 163, 0.18);
} }
.home-mock .install-hero-close {
position: absolute;
top: 14px;
right: 14px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.14);
color: white;
font-size: 18px;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
}
.home-mock .install-hero-close:hover,
.home-mock .install-hero-close:focus-visible {
background: rgba(255, 255, 255, 0.28);
outline: none;
}
.home-mock .offboard-strip {
margin: 0 0 22px;
padding: 10px 14px;
border: 1px solid var(--hp-border);
border-radius: 10px;
background: var(--hp-border-light);
color: var(--hp-text-secondary);
font-size: 13px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.home-mock .offboard-strip button {
border: 1px solid var(--hp-border);
background: white;
border-radius: 6px;
padding: 4px 10px;
font-size: 13px;
cursor: pointer;
}
.home-mock .offboard-strip button:hover { background: var(--hp-border-light); }
.home-mock .install-hero .eyebrow { .home-mock .install-hero .eyebrow {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@ -1154,29 +1200,22 @@
{% set display_name = (user.name or (user.email or "").split("@")[0] or "there") %} {% set display_name = (user.name or (user.email or "").split("@")[0] or "there") %}
{# Install-hero renders only for not-onboarded users. Once `agnes init`
POSTs /api/me/onboarded (or the user clicks the in-hero X) the hero
disappears entirely — the rest of /home (connector tiles, news,
etc.) stays. Offboarding escape hatch moved to a discrete strip
below; see `.offboard-strip`. #}
{% if not onboarded %}
<div class="install-hero"> <div class="install-hero">
{% if onboarded %} <button type="button" class="install-hero-close" id="installHeroClose"
<div class="eyebrow">Welcome back, {{ display_name }} — your workspace is ready</div> data-target-source="self_acknowledged"
<h1>You're set up — keep this page handy</h1> aria-label="I'm already set up — close this setup hero">&times;</button>
<p class="lead">
Your local {{ instance_brand }} install is confirmed. The steps below stay useful for <strong>adding another machine</strong>, <strong>connecting more services</strong>, or <strong>turning on auto-accept mode</strong>. Skip whatever you don't need; nothing here re-runs unless you click it.
</p>
{% else %}
<div class="eyebrow">Welcome, {{ display_name }} — let's get you set up</div> <div class="eyebrow">Welcome, {{ display_name }} — let's get you set up</div>
<h1>Connect Claude Code on your machine to your team's data</h1> <h1>Connect Claude Code on your machine to your team's data</h1>
<p class="lead"> <p class="lead">
{{ instance_brand }} gives <strong>Claude Code</strong> on your computer access to your team's <strong>curated data, plugins, and shared knowledge</strong> — so you can ask questions and get answers in plain language, right from your terminal. This page walks you through the <strong>one-time setup (~10 minutes)</strong>. Everything it installs lives in your home folder (<code style="background: rgba(255,255,255,0.12); padding: 1px 6px; border-radius: 4px; font-family: var(--hp-font-mono); font-size: 12.5px;">~/{{ workspace_dir }}</code>) and can be removed in one command. {{ instance_brand }} gives <strong>Claude Code</strong> on your computer access to your team's <strong>curated data, plugins, and shared knowledge</strong> — so you can ask questions and get answers in plain language, right from your terminal. This page walks you through the <strong>one-time setup (~10 minutes)</strong>. Everything it installs lives in your home folder (<code style="background: rgba(255,255,255,0.12); padding: 1px 6px; border-radius: 4px; font-family: var(--hp-font-mono); font-size: 12.5px;">~/{{ workspace_dir }}</code>) and can be removed in one command.
</p> </p>
{% endif %}
{% if onboarded %}
<div class="install-done" role="status" aria-live="polite">
<span class="check" aria-hidden="true">&#x2705;</span>
<span><strong>Steps 14 done</strong> — Claude Code installed, auto-mode set, workspace folder created, {{ instance_brand }} ready in <code>~/{{ workspace_dir }}</code>. The full install steps stay one click away under the offboard control below.</span>
</div>
{% endif %}
{% if not onboarded %}
<div class="install-block"> <div class="install-block">
<div class="label">Step 1 — install Claude Code</div> <div class="label">Step 1 — install Claude Code</div>
<div class="os-tabs" role="tablist" aria-label="Operating system"> <div class="os-tabs" role="tablist" aria-label="Operating system">
@ -1281,53 +1320,45 @@ Set-Location "$HOME\{{ workspace_dir }}"</span>
</details> </details>
</div> </div>
{% endif %}
{# P1-6 — auto-detect badge is the PRIMARY affordance after the {# P1-6 — auto-detect badge is the PRIMARY affordance after the
install-script copy: agnes-init's first POST to install-script copy: agnes-init's first POST to
/api/me/onboarded flips state automatically and the page /api/me/onboarded flips state automatically and the page
reloads. The manual "Mark me as onboarded" button below it reloads. The manual "Mark me as onboarded" button below it
stays as a fallback when auto-flip never lands. #} stays as a fallback when auto-flip never lands. #}
{% if not onboarded %}
<div class="auto-detect-badge" role="status" aria-live="polite"> <div class="auto-detect-badge" role="status" aria-live="polite">
<span class="pulse" aria-hidden="true"></span> <span class="pulse" aria-hidden="true"></span>
<span>Waiting for your first <code>agnes pull</code> — auto-detects within ~30 s of the setup script finishing.</span> <span>Waiting for your first <code>agnes pull</code> — auto-detects within ~30 s of the setup script finishing.</span>
</div> </div>
{% endif %}
{# Self-mark control lives inside the blue hero in both states. {# Self-mark fallback for the auto-flip. The hero's X close button
When onboarded, the install steps above are hidden so this is does the same thing more visibly; both target the not-onboarded
the only thing rendered below the lead paragraph. #} → onboarded direction. The onboarded → offboarded variant lives
below the hero (.offboard-strip) so it stays reachable once the
hero is gone. #}
<div class="self-mark"> <div class="self-mark">
{% if onboarded %}
Wiped your workspace or want the full setup view back?
<button id="self-mark-btn" type="button"
data-target-onboarded="false"
data-target-source="self_unmark">Mark me as offboarded</button>
{% else %}
Already set this up? Already set this up?
<button id="self-mark-btn" type="button" <button id="self-mark-btn" type="button"
data-target-onboarded="true" data-target-onboarded="true"
data-target-source="self_acknowledged">Mark me as onboarded</button> data-target-source="self_acknowledged">Mark me as onboarded</button>
{% endif %}
<span id="self-mark-status" class="status" role="status" aria-live="polite"></span> <span id="self-mark-status" class="status" role="status" aria-live="polite"></span>
</div> </div>
{% if onboarded %}
{# User-controlled minimize toggle for Connect-your-tools.
Default OFF (section renders flat). State persists in
localStorage so the choice is per-device. The agnes-init
auto-flip of users.onboarded never triggers a collapse on
its own — only an explicit click here does. The auto-mode
block used to be a peer collapsible (`step3`); it now lives
inside the install-hero as Step 2 and is not collapsible. #}
<div class="setup-minimize">
<button id="setupMinimizeToggle" type="button" aria-pressed="false">
Minimize setup view
</button>
</div>
{% endif %}
</div> </div>
{% endif %}
{% if onboarded %}
{# Offboarding escape hatch shown only after the hero has disappeared.
Lets the analyst (e.g. after wiping ~/FoundryAI) flip the
users.onboarded boolean back to false so the full install hero
renders again on next reload. Discrete by design — onboarded
users land on /home expecting the nav hub, not a setup screen. #}
<div class="offboard-strip">
<span>Workspace ready — wiped it and need the full setup view back?</span>
<button id="offboard-btn" type="button"
data-target-source="self_unmark">Mark me as offboarded</button>
<span id="offboard-status" class="status" role="status" aria-live="polite"></span>
</div>
{% endif %}
{# Auto-mode card used to live here as a `<details>` reference block; {# Auto-mode card used to live here as a `<details>` reference block;
moved into the install-hero as the new Step 2 so users enable it moved into the install-hero as the new Step 2 so users enable it
@ -1638,16 +1669,13 @@ Set-Location "$HOME\{{ workspace_dir }}"</span>
}); });
} }
var btn = document.getElementById('self-mark-btn'); // Shared poster for /api/me/onboarded — reused by every UI surface
var status = document.getElementById('self-mark-status'); // that flips users.onboarded (in-hero X close, "Mark me as onboarded"
if (!btn) return; // fallback button, the offboard strip). Reloads on success so the
btn.addEventListener('click', function () { // template re-renders with the new state.
// Direction comes from data-attrs the template sets per render — function postOnboarded(triggerBtn, statusEl, targetOnboarded, targetSource) {
// onboarded view → flip to FALSE (offboard), not-onboarded → flip to TRUE. if (triggerBtn) triggerBtn.disabled = true;
var targetOnboarded = btn.getAttribute('data-target-onboarded') === 'true'; if (statusEl) statusEl.textContent = targetOnboarded ? 'Marking…' : 'Resetting…';
var targetSource = btn.getAttribute('data-target-source') || 'self_acknowledged';
btn.disabled = true;
status.textContent = targetOnboarded ? 'Marking…' : 'Resetting…';
fetch('/api/me/onboarded', { fetch('/api/me/onboarded', {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@ -1655,17 +1683,59 @@ Set-Location "$HOME\{{ workspace_dir }}"</span>
body: JSON.stringify({ source: targetSource, onboarded: targetOnboarded }), body: JSON.stringify({ source: targetSource, onboarded: targetOnboarded }),
}).then(function (resp) { }).then(function (resp) {
if (resp.ok) { if (resp.ok) {
status.textContent = 'Done. Reloading…'; if (statusEl) statusEl.textContent = 'Done. Reloading…';
window.location.reload(); window.location.reload();
} else { } else {
status.textContent = 'Failed (' + resp.status + '). Try again.'; if (statusEl) statusEl.textContent = 'Failed (' + resp.status + '). Try again.';
btn.disabled = false; if (triggerBtn) triggerBtn.disabled = false;
} }
}).catch(function () { }).catch(function () {
status.textContent = 'Network error. Try again.'; if (statusEl) statusEl.textContent = 'Network error. Try again.';
btn.disabled = false; if (triggerBtn) triggerBtn.disabled = false;
}); });
}); }
// "Mark me as onboarded" fallback button inside the hero (rendered
// only when not-onboarded — the X close button is the primary path).
var btn = document.getElementById('self-mark-btn');
var status = document.getElementById('self-mark-status');
if (btn) {
btn.addEventListener('click', function () {
postOnboarded(
btn, status, true,
btn.getAttribute('data-target-source') || 'self_acknowledged'
);
});
}
// Hero X close — confirm first so a stray click doesn't flip state.
var heroClose = document.getElementById('installHeroClose');
if (heroClose) {
heroClose.addEventListener('click', function () {
var ok = window.confirm(
"Are you already onboarded? Closing this will mark your account as onboarded " +
"and hide the setup steps. You can revert later from the strip below the hero."
);
if (!ok) return;
postOnboarded(
heroClose, status, true,
heroClose.getAttribute('data-target-source') || 'self_acknowledged'
);
});
}
// Offboarding strip — only rendered when onboarded. Flips back to
// the install hero on next reload.
var offBtn = document.getElementById('offboard-btn');
var offStatus = document.getElementById('offboard-status');
if (offBtn) {
offBtn.addEventListener('click', function () {
postOnboarded(
offBtn, offStatus, false,
offBtn.getAttribute('data-target-source') || 'self_unmark'
);
});
}
// ── Minimize-setup toggle ──────────────────────────────────────── // ── Minimize-setup toggle ────────────────────────────────────────
// Default OFF: sections render flat (no <summary> visible). // Default OFF: sections render flat (no <summary> visible).

View file

@ -28,8 +28,14 @@ instance:
# "FoundryAI"). Set explicitly only if you want a folder # "FoundryAI"). Set explicitly only if you want a folder
# name that differs from the auto-derivation. Env override: # name that differs from the auto-derivation. Env override:
# AGNES_WORKSPACE_DIR_NAME. # AGNES_WORKSPACE_DIR_NAME.
# logo_svg: Full <svg> element for header logo (optional, default: Keboola logo) # logo_svg: | # Inline <svg> element rendered into the header brand slot.
# Example: '<svg width="120" height="30" viewBox="0 0 100 30" xmlns="http://www.w3.org/2000/svg"><text y="22" font-size="24" fill="#333">Logo</text></svg>' # <svg width="120" height="30" viewBox="0 0 100 30" xmlns="http://www.w3.org/2000/svg">
# <text y="22" font-size="24" fill="#333">Logo</text>
# </svg>
# # When set, the SVG replaces the text brand in the header.
# # `name` above still drives browser <title> text and page
# # headings — keep it populated. Env override:
# # AGNES_INSTANCE_LOGO_SVG.
# sync_interval: "1 hour" # Cadence shown in analyst CLAUDE.md (e.g., "1 hour", "30 minutes", "daily") # 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 # admin_email: "ops@acme.com" # Operator contact shown on /home GWS connector tile as
# an "Email admin" mailto button (analysts whose operator # an "Email admin" mailto button (analysts whose operator

View file

@ -1,6 +1,6 @@
[project] [project]
name = "agnes-the-ai-analyst" name = "agnes-the-ai-analyst"
version = "0.54.5" version = "0.54.6"
description = "Agnes — AI Data Analyst platform for AI analytical systems" description = "Agnes — AI Data Analyst platform for AI analytical systems"
requires-python = ">=3.11,<3.14" requires-python = ">=3.11,<3.14"
license = "MIT" license = "MIT"

View file

@ -75,11 +75,14 @@ def test_home_not_onboarded_user_sees_setup_view(fresh_db):
def test_home_onboarded_user_sees_nav_hub(fresh_db): def test_home_onboarded_user_sees_nav_hub(fresh_db):
"""A TRUE-onboarded user gets the post-onboarding view, identifiable by """A TRUE-onboarded user gets the post-onboarding view: the blue
the 'Welcome back' hero, the 'Step 1 & Step 2 done' completion badge, install-hero is gone entirely (no welcome banner, no completion
the offboard control, and the absence of the inline Step 1 / Step 2 badge, no inline step commands), the offboard escape strip is the
install commands. Step 3 (auto-mode), connectors, and the rest stay only setup-flow remnant rendered, and the rest of /home (connector
visible they remain useful after onboarding.""" tiles, news, etc.) stays. PR #289 collapsed the dual-state hero
into a single not-onboarded-only render pre-PR the onboarded
branch reused the same `.install-hero` shell with welcome copy
and a "Steps 14 done" badge."""
from src.db import get_system_db, close_system_db from src.db import get_system_db, close_system_db
conn = get_system_db() conn = get_system_db()
@ -93,13 +96,11 @@ def test_home_onboarded_user_sees_nav_hub(fresh_db):
resp = c.get("/home", cookies={"access_token": sess}) resp = c.get("/home", cookies={"access_token": sess})
assert resp.status_code == 200 assert resp.status_code == 200
body = resp.text body = resp.text
assert "Welcome back" in body # Install hero entirely absent for onboarded users.
# Banner copy updated when the explicit "create workspace folder" assert '<div class="install-hero">' not in body
# step was inserted between auto-mode and install-Agnes — completion # Offboard escape strip + its button replace the in-hero self-mark control.
# badge now spans Steps 1-4 (install Claude Code, auto-mode, mkdir assert '<div class="offboard-strip">' in body
# workspace, install Agnes from Claude Code). assert "Mark me as offboarded" in body
assert "Steps 1&#8211;4 done" in body or "Steps 14 done" in body
assert "Mark me as offboarded" in body # offboard control visible
# All four inline install-blocks are hidden post-onboarding — the # All four inline install-blocks are hidden post-onboarding — the
# labels rendered inside the install-block divs go away. # labels rendered inside the install-block divs go away.
assert "Step 1 — install Claude Code" not in body assert "Step 1 — install Claude Code" not in body
@ -152,37 +153,30 @@ def test_connectors_render_flat_when_onboarded_by_default(fresh_db):
assert 'class="home-mock"\n' in body or '<div class="home-mock">' in body assert 'class="home-mock"\n' in body or '<div class="home-mock">' in body
def test_minimize_toggle_visible_only_when_onboarded(fresh_db): def test_minimize_toggle_no_longer_rendered(fresh_db):
"""The "Minimize setup view" toggle markup is rendered for onboarded """The "Minimize setup view" toggle used to live inside the
users (so they can opt into the collapsed view) and absent for onboarded-branch of the install-hero. PR #289 hides the hero
not-onboarded users (where the install steps already dominate).""" entirely once `users.onboarded=true`, so the minimize toggle
has no rendering site anymore verify the markup is absent
from both states. (The localStorage `agnes_home_setup_minimized`
flag and its applyMinimize() JS handler stay in the page so a
stale flag from a pre-PR session no-ops cleanly.)"""
from src.db import get_system_db, close_system_db from src.db import get_system_db, close_system_db
# Not-onboarded → no toggle button. for onboarded in (False, True):
conn = get_system_db() conn = get_system_db()
try: try:
_, sess = _make_user_and_session(conn, onboarded=False) _, sess = _make_user_and_session(
finally: conn, email=f"user-{onboarded}@example.com", onboarded=onboarded
conn.close() )
close_system_db() finally:
c = _client() conn.close()
resp = c.get("/home", cookies={"access_token": sess}) close_system_db()
assert resp.status_code == 200 c = _client()
assert '<button id="setupMinimizeToggle"' not in resp.text resp = c.get("/home", cookies={"access_token": sess})
assert 'class="setup-minimize"' not in resp.text assert resp.status_code == 200
assert '<button id="setupMinimizeToggle"' not in resp.text
# Onboarded → toggle button rendered inside the install-hero. assert 'class="setup-minimize"' not in resp.text
conn = get_system_db()
try:
_, sess2 = _make_user_and_session(conn, email="b@example.com", onboarded=True)
finally:
conn.close()
close_system_db()
c2 = _client()
resp2 = c2.get("/home", cookies={"access_token": sess2})
assert resp2.status_code == 200
assert '<button id="setupMinimizeToggle"' in resp2.text
assert 'class="setup-minimize"' in resp2.text
def test_home_no_auto_transition_after_post_until_reload(fresh_db): def test_home_no_auto_transition_after_post_until_reload(fresh_db):
@ -213,7 +207,9 @@ def test_home_no_auto_transition_after_post_until_reload(fresh_db):
assert flip.status_code == 200 assert flip.status_code == 200
post = c.get("/home", cookies={"access_token": sess}) post = c.get("/home", cookies={"access_token": sess})
assert "Welcome back" in post.text # nav hub view # PR #289: hero disappears entirely; offboard strip is the
# only setup-flow remnant. Use either as the nav-hub view marker.
assert '<div class="offboard-strip">' in post.text
assert 'class="install-block"' not in post.text assert 'class="install-block"' not in post.text

View file

@ -24,7 +24,7 @@ wheels = [
[[package]] [[package]]
name = "agnes-the-ai-analyst" name = "agnes-the-ai-analyst"
version = "0.54.1" version = "0.54.6"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "a2wsgi" }, { name = "a2wsgi" },