* feat(home+news): state-aware /home + /news + admin-edited news section
Squash of the vr/home-page feature work for clean rebase onto main.
Original 18-commit history preserved in branch backup/vr-home-page-pre-rebase.
What's in this PR:
**State-aware /home page**
- New `/home` route with hero + auto-mode + connectors (Asana / GWS /
Atlassian) + lookarounds. Onboarded vs not-onboarded state-machine
branches a single template (`home_not_onboarded.html`); the install
steps, "Setup a new Claude Code" CTA (90-day PAT mint), and per-
connector setup prompts hide once `users.onboarded=TRUE`. A
completion badge replaces them.
- "Mark me as offboarded" button reverses the flag without an SQL UPDATE.
- `users.onboarded BOOLEAN` column added; default FALSE; flipped by the
CLI's `agnes init` post-success POST and the `/admin/users` API.
- Connector setup prompts pre-check whether the tool is already
installed/connected before re-running setup.
- GWS scope set widened to include Google Chat (`chat.spaces`,
`chat.messages`).
**Single template + design tokens**
- `dashboard.html` now extends `base.html` via the new
`{% block layout %}` opt-out (full-width pages skip the 800px
`.container`). Net: every page shares one shell.
- `style-custom.css` `:root` extended with `--space-{7,9,10,12}`,
`--radius-2xl`, `--shadow-{card,elevated}`, `--text-{muted,disabled}`,
`--focus-ring`, `--transition-*`, `--width-{narrow,app,wide}` so
inline page styles can migrate incrementally.
**Auth redirects honor AGNES_HOME_ROUTE**
- `safe_next_path` resolves the configured home route when no `default=`
is passed; OAuth callbacks, magic-link clicks, password form, and
LOCAL_DEV_MODE shortcuts now land on `/home` (or whatever the operator
picked) instead of always /dashboard.
**News section + /news permalink + /admin/news editor**
- Schema-bumped `news_template` table (single versioned entity, draft +
publish gate). `published BOOLEAN` distinguishes draft from public;
monotonically-increasing `version` per save; rows >30d pruned on
save except the currently-displayed published version.
- `/home` bottom-of-page renders the latest published intro with a
"Read more →" link to `/news` (which renders the full body).
- `/admin/news` editor with sandboxed live preview, versions table,
per-row Unpublish, Format-help cheatsheet.
- `agnes admin news show / draft / edit / publish / unpublish /
versions / export` (CLI). Talks to the live server via the
`/api/admin/news/*` endpoints (PAT-authed) — no direct DB access
so it coexists with a running uvicorn.
- **Optimistic-lock guard**: `agnes admin news publish --version N` and
PUT/PATCH endpoints accept `expected_version` and 409 with structured
`{error: "version_conflict", expected, actual, actual_by}` when a
concurrent admin replaced the draft. Edit refuses to overwrite a
draft authored by someone else without `--force` or
`--expect-version`.
- nh3 (Rust-backed ammonia) HTML sanitizer; iframe pre-pass strips
any iframe whose src is not on the YouTube/Vimeo/Loom allowlist;
javascript:/data: schemes blocked everywhere.
- Author CSS vocabulary: `.news-hero` (blue gradient hero block),
`.callout`/`.callout-{info,warn,success,danger}`,
`.video-embed`, `.news-section`, `.news-grid-{2,3}`, `.news-cta` —
all consolidated in `style-custom.css` under "News content
vocabulary (shared)" so /home perex, /news body, and /admin/news
preview share one source of styling.
- Code-inside-`<pre>` contrast fix (was unreadable amber-on-silver).
- `.news-content` table styling (border, header band, row-hover).
**`scripts/dev/run-local.sh`** — local uvicorn launcher. Pulls Google
OAuth client id/secret from GCP Secret Manager
(`AGNES_OAUTH_GCP_PROJECT`-driven, no vendor defaults), points
`AGNES_CLI_DIST_DIR` at `./dist` so the wheel endpoint resolves, and
`--dev` flips `LOCAL_DEV_MODE=1` + `AGNES_HOME_ROUTE=/home` for one-
command iteration. `LOCAL_DEV_MODE=1` also enables the FastAPI debug
toolbar.
**CLAUDE.md "Run tests before every push" section** codifies
`pytest tests/ -n auto -q` as non-negotiable before each push.
**Tests**: 51 + 14 + 8 = 73 new tests across news-template repo,
sanitizer, API, web, CLI; plus updated home/auth/template tests for
the new shared-shell architecture.
Origin docs (gitignored, customer-fork content):
docs/brainstorms/home-page-requirements.md,
docs/plans/2026-05-07-001-feat-home-page-plan.md.
* feat(cli): agnes onboarded {on,off,status} — self-scoped flag toggle
User-facing equivalent of the in-page "Mark me as (off)boarded" button
on /home. POSTs /api/me/onboarded with {onboarded, source}; --source
overrides the audit-log marker so flips made from the CLI vs the web
button vs agnes init automation stay distinguishable.
`status` reads via /api/me/profile (when present); falls back to a
quick body-marker scan of /home so the read path doesn't write an
audit_log row. PAT-authed via cli.client.api_post — same convention
as agnes admin news / agnes admin add-user etc.
Tests: 5 covering on/off/status round-trip, idempotency, and
audit-log source recording. Full suite holds at 12 pre-existing
failures (same set as before).
* ui(nav+home): primary nav reorg + green What's new band + /marketplace link fix
Primary nav (post-rebase audit + per-user feedback):
- Items: Home → Marketplace → Data Packages → Memory. Admin dropdown
for admins only. The "Dashboard" label was renamed Home — point still
resolves through `home_route` so customer instances on /dashboard
still land there.
- Activity Center moved into the Admin dropdown. Per-team adoption
analytics is admin-consumed in practice; the route still allows
any authed user for direct deep-links so existing /home tile +
bookmarks keep working.
- Memory link added (→ /corporate-memory) — was previously buried in
the /home "Look around" tiles.
- Setup local agent + My Stack dropped from main nav. Setup is the
/home install flow's home now; My Stack lives as a tab inside
/marketplace.
/home tweaks:
- Plugin marketplace tile now points at /marketplace (was /store —
legacy from before the marketplace rebrand landed in #230).
- "What's new" section header gets a green band (success-flavored
D1FAE5 background, A7F3D0 border, darker green title) so the
bottom-of-page news block visibly distinguishes from the blue
install-hero at the top. Header strip only — body stays white.
Test fix: test_home_route_resolution renamed `dashboard_link_uses_home_route`
→ `home_link_uses_home_route` and asserts `href="/home">Home` instead
of `href="/home">Dashboard` after the label change.
* fix(home): decouple Step 3 + Connect-tools collapse from server onboarded flag
The server-side `users.onboarded` flip happens through two paths:
1. Explicit user click on "Mark me as onboarded" or `agnes onboarded on`.
2. Implicit `agnes init` POST → /api/me/onboarded on success.
Path 2 produced a UX surprise: an analyst running `agnes init` mid-flow
reloaded /home and saw Step 3 (auto-mode) + Connect-your-tools auto-
collapse to summary bars. They were actively working through those
sections — the install POST never signalled "I'm done with the rest
of setup", just "Agnes itself is installed".
Decouple the section-collapse decision from the server flag:
- Step 1 + Step 2 install blocks: still hidden on `onboarded=TRUE`
(their completion is a hard server signal — Agnes IS installed).
- Step 3 + Connect-your-tools: render flat by default in BOTH states.
Wrapped in `<details class="setup-collapsible" open>` so the
browser's native disclosure handles per-section toggle without JS,
but the `<summary>` is CSS-hidden until the page-level
`data-setup-minimized="1"` attribute is set on `.home-mock`.
- New "Minimize setup view" toggle inside the blue install-hero,
rendered only when onboarded. Click flips the data-attr on
`.home-mock` AND removes the `open` attribute from each
`<details>`. State persists in `localStorage["agnes_home_setup_minimized"]`
so the choice survives reloads but is per-device.
- "Show full setup view" (the same button when minimized) re-opens
both `<details>` and clears localStorage.
When minimized, each `<details>` still has its own native expand/
collapse — click the gray summary bar to peek at one section without
toggling the page-level minimize off.
Tests:
- test_step3_and_connectors_render_flat_when_onboarded_by_default —
asserts `<details class="setup-collapsible" ... open>` for both
sections post-onboarding and the absence of any server-rendered
`data-setup-minimized` attribute on the `.home-mock` root.
- test_minimize_toggle_visible_only_when_onboarded — toggle button
rendered only when onboarded.
Full pytest holds at 12 pre-existing failures (same set).
7.9 KiB
News content authoring guide
This page documents the HTML vocabulary the /home news perex and /news permalink page recognise. Authors should write content using only these allowed tags, attributes, and class names — anything outside the list is silently stripped on save.
Where the content lives
A single news entity (intro + content) is stored in DuckDB table news_template. Every save creates / updates a row with a monotonically increasing version. The latest row with published = TRUE is what /home and /news render. The admin can roll back by unpublishing a newer version (web falls back to the next-highest published version automatically).
Drafts older than 30 days that were never published, and superseded published versions older than 30 days, are pruned on save. The currently-displayed published version is never pruned regardless of age.
How to author
Two equivalent surfaces:
- Web admin UI:
/admin/news— two textareas (intro + full content) with a sandboxed live preview, a "Format help" cheatsheet, and a versions table withUnpublishactions. - CLI:
agnes admin news show # current published agnes admin news draft # active draft (or none) agnes admin news edit \ --intro '<p>Short HTML perex.</p>' \ --content '<h1>Title</h1><p>Body.</p>' agnes admin news edit --from news.yaml # YAML/JSON {intro, content} agnes admin news publish # flip active draft → published agnes admin news unpublish 5 # roll back v5; web shows next-highest published agnes admin news versions agnes admin news export news.yaml # round-trip to a file
Both surfaces sanitize on save through src/sanitize_news.py (Rust-backed nh3).
Allowed tags
p, br, hr,
h1, h2, h3, h4, h5, h6,
ul, ol, li,
strong, em, b, i, u, s,
code, pre, blockquote,
a, img,
span, div, section,
table, thead, tbody, tr, th, td,
details, summary,
figure, figcaption,
iframe (only with allowlisted src — see below)
Anything else (<script>, <style>, <object>, <embed>, <base>, <link>, <meta>, <form>, <input>, all event-handler attributes like onclick/onerror/onload) is stripped.
URL schemes
<a href>,<img src>:http://,https://,mailto:, or relative paths (no scheme).javascript:anddata:are blocked.<a target="_blank">: the sanitizer auto-injectsrel="noopener noreferrer"so target-blank links can't pivot the parent window.<iframe src>: must start with one of the video-host prefixes. Any other src strips the entire iframe element:https://www.youtube.com/embed/…,https://youtube.com/embed/…,https://www.youtube-nocookie.com/embed/…https://player.vimeo.com/video/…https://www.loom.com/embed/…,https://www.loom.com/share/…
Allowed attributes
| Tag | Attributes |
|---|---|
a |
href, title, target, class (rel is auto-managed by the sanitizer; do not set it manually) |
img |
src, alt, width, height |
iframe |
src, title, width, height, allow, allowfullscreen, frameborder |
span, div, section, p, h1–h6, table, td, th, blockquote |
class |
The class attribute is permitted only on the structural tags listed; it's used for the documented vocabulary below. Custom classes that aren't in the documented list will pass the sanitizer but won't render any styling — there's no CSS for them.
Documented class vocabulary
Use these classes via copy-and-edit. They render consistently on /home (perex) and /news (full body). The CSS lives in one place: app/web/static/style-custom.css under "News content vocabulary (shared)" — editing it there updates /home, /news, and the /admin/news preview pane together.
Hero block — landing-page-style highlight
Blue-gradient block with eyebrow line, title, and a one-line lead. Same vocabulary the /home install hero uses; surfaced as div.news-hero so news authors can recreate the look without reaching into /home-specific selectors. Use sparingly — at most one per news entity, usually at the top.
<section class="news-hero">
<div class="eyebrow">Release · v0.40</div>
<h2>News section is live</h2>
<p class="lead">A reusable hero block for landing-page-style highlights — eyebrow + heading + one-line lead in the brand-blue gradient.</p>
</section>
Callouts — boxed notice with colored left border
<div class="callout">
<strong>Note:</strong> A neutral callout.
</div>
<div class="callout callout-info">
<strong>FYI:</strong> Informational, blue.
</div>
<div class="callout callout-warn">
<strong>Heads up:</strong> Yellow — needs attention.
</div>
<div class="callout callout-success">
<strong>Done:</strong> Green — successful release / completed.
</div>
<div class="callout callout-danger">
<strong>Important:</strong> Red — breaking change / required action.
</div>
Video embed — 16:9 wrapper
Wrap the iframe so it scales fluidly on narrow viewports:
<div class="video-embed">
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="Walkthrough video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
Replace the src with the YouTube / Vimeo / Loom embed URL — anything else gets the iframe stripped on save.
Sections — visual breaks between topics
<section class="news-section">
<h2>Big release this week</h2>
<p>Body of this section.</p>
</section>
Two- or three-column grid
<div class="news-grid-2">
<div>
<h3>Left column</h3>
<p>…</p>
</div>
<div>
<h3>Right column</h3>
<p>…</p>
</div>
</div>
news-grid-3 works the same with three children. Both collapse to a single column below 720px.
Call-to-action button
<a class="news-cta" href="/setup-advanced">Open advanced setup</a>
Example: a release note
<!-- intro (shown on /home) -->
<p><strong>Agnes 0.40 is live.</strong> Highlights: new <code>agnes admin news</code>
command, marketplace plugin discovery, and a 3× faster <code>/catalog</code>.</p>
<!-- content (shown on /news) -->
<section class="news-section">
<h2>What changed</h2>
<ul>
<li><strong>News editor.</strong> Admins can publish updates to <code>/home</code>
+ <code>/news</code> from the web UI or CLI. See
<a href="/admin/news">/admin/news</a>.</li>
<li><strong>Marketplace.</strong> The Plugin Store now surfaces newly-released
community plugins on the homepage.</li>
<li><strong>Catalog speed-ups.</strong> Schema lookups cache locally; first-page
load drops from ~800ms to ~250ms.</li>
</ul>
</section>
<div class="callout callout-warn">
<strong>Action needed:</strong> If you're on a custom plugin that pinned to
<code>0.39</code>, bump the pin to <code>0.40</code> after upgrading.
</div>
<section class="news-section">
<h2>Walkthrough</h2>
<div class="video-embed">
<iframe src="https://www.loom.com/embed/example-loom-id"
allowfullscreen></iframe>
</div>
</section>
<a class="news-cta" href="/setup-advanced">Open advanced setup</a>
What gets stripped
If you find something missing from the published render, it almost certainly fell into one of these buckets:
- A non-allowlisted tag (
<script>,<style>,<object>,<form>, etc.) → stripped. - An event handler (
onclick,onerror,onload,onmouseover, …) → stripped. - A
javascript:ordata:URL onhref/src→ URL stripped (element kept, attribute removed). - An iframe whose
srcis not on the YouTube / Vimeo / Loom allowlist → entire iframe element stripped. - A
classattribute on a tag that isn't in the structural list →classstripped.
When in doubt, paste the candidate HTML into the /admin/news preview pane — the sandboxed iframe shows exactly what users will see.