. #}
{# page-specific markup, verbatim (minus the _app_header include —
base.html includes it already). The attribute, if
any, moves to a {% block body_attrs %}data-x="…"{% endblock %} #}
{% endblock %}
{% block scripts %}
{% endblock %}
```
### Deletions per page
- `` + `` + `` (base.html provides)
- `` + `` (base.html provides)
- ``, `` (base.html provides)
- Font preconnect block — `base.html` doesn't ship it today, so this is a small **behavior change**: pages will lose the explicit Inter preconnect. Mitigation: add the preconnect once to base.html's `{% block head_extra %}` parent (or to `base.html` itself above the stylesheet link). See Step 1.
- `` if any (base.html provides)
- `{% include '_theme.html' %}` (base.html provides)
- `` opening tag (base.html provides; attrs go to `{% block body_attrs %}`)
- `{% include '_app_header.html' %}` at start of body (base.html includes it)
- `` + closing tags
### Preserved per page
- `` text → `{% block title %}`
- All inline `{% endblock %}`
- All page markup → `{% block content %}` or `{% block layout %}`
- All inline `{% endblock %}`
- Page-specific JS variable usage (e.g. `data-source-type` on body) → `{% block body_attrs %}`
---
## Task 0: Setup + verify base.html blocks + add body_attrs slot
**Files:**
- Modify: `app/web/templates/base.html`
- [ ] **Step 1: Verify dashboard.html / home_*.html already extend base.html.**
```bash
grep -l "extends.*base\.html" app/web/templates/*.html
```
Expected output includes `dashboard.html`, `home_onboarded.html`, `home_not_onboarded.html`. Confirms our scope is exactly the 5 standalones, not more.
- [ ] **Step 2: Add `{% block body_attrs %}` to base.html.**
The `admin_tables.html` template currently renders ` `; its inline JS reads that attribute. We must preserve the attribute.
Read `base.html`'s `` line, then change it from:
```html
```
to:
```html
```
The default empty block keeps non-admin_tables pages unchanged.
- [ ] **Step 3: Add Inter font preconnect to base.html.**
Currently `base.html` ships only the stylesheet + `_theme.html` include. The 4 standalone pages that ship their own font preconnect (catalog, corporate_memory*, install) would lose the optimization after migration. Add to `base.html` `` right BEFORE the stylesheet link:
```html
{% if not config.THEME_FONT_URL %}
{% endif %}
```
This change benefits ALL base.html consumers — admin pages currently rely on the system Inter being present. With this addition, base.html-extended pages always have the canonical font loaded.
- [ ] **Step 4: Confirm test pass + render check before any per-page migration.**
```bash
.venv/bin/python -m pytest tests/test_web_ui.py tests/test_web_home_page.py tests/test_design_system_contract.py -q
```
Expected: green. Captures the baseline before migrations begin.
- [ ] **Step 5: Commit.**
```bash
git add app/web/templates/base.html
git commit -m "feat(base): body_attrs block + Inter font preconnect
Adds a body_attrs Jinja block (default empty) so pages that extend
base.html can carry their own attributes — admin_tables.html
needs data-source-type for its JS reading.
Hoists the Inter font preconnect + stylesheet link into base.html's
so every page that extends base gets the same font load. The
5 standalone pages about to be migrated each had this block inline;
centralising it here means future changes (e.g. self-hosting the
font) land in one place."
```
---
## Task 1: Migrate install.html (smallest pilot)
**Why install first**: smallest of the 5 (1097 lines), simplest layout, no admin gating, well-contained. Validates the recipe before tackling the big templates.
**Files:**
- Modify: `app/web/templates/install.html`
- [ ] **Step 1: Read full install.html.**
```bash
wc -l app/web/templates/install.html
sed -n '1,30p' app/web/templates/install.html # head (lines 1-30)
sed -n '640,660p' app/web/templates/install.html # + boundary
sed -n '935,945p' app/web/templates/install.html # ", re.IGNORECASE | re.DOTALL)),
("style", re.compile(r"", re.IGNORECASE | re.DOTALL)),
]
# Concatenate matches found in head, in their original source position.
matches = []
for kind, pat in HEAD_PATTERNS:
for m in pat.finditer(head):
matches.append((m.start(), kind, m.group(0)))
matches.sort()
for _, kind, raw in matches:
# Skip duplicate style-custom.css link (base.html already provides).
if kind == "link" and "style-custom.css" in raw:
continue
# Skip —
# we keep the inline preconnect block per page to preserve current
# behaviour; do NOT dedupe against base.html in this pass.
head_assets.append(raw)
head_extra = "\n".join(head_assets)
# 4) Inside : pull out every ", re.IGNORECASE | re.DOTALL)
for m in SCRIPT_BODY_RE.finditer(body):
script_blocks.append(m.group(0))
body_no_scripts = SCRIPT_BODY_RE.sub("", body)
# Strip leading _app_header.html include (with optional surrounding
# whitespace and HTML comments).
body_no_header = re.sub(
r"^\s*(?:\s*)*\{%\s*include\s+['\"]_app_header\.html['\"]\s*%\}\s*",
"", body_no_scripts, count=1
)
# 5) Compose output.
layout_indent = " "
out = ['{% extends "base.html" %}', "", f"{{% block title %}}{title}{{% endblock %}}"]
if body_attrs:
out += ["", f"{{% block body_attrs %}}{body_attrs}{{% endblock %}}"]
if head_extra.strip():
out += ["", "{% block head_extra %}", head_extra, "{% endblock %}"]
out += [
"",
"{% block layout %}",
"{% include '_app_header.html' %}",
body_no_header.rstrip(),
"{% endblock %}",
]
if script_blocks:
out += ["", "{% block scripts %}"]
out += script_blocks
out += ["{% endblock %}"]
out.append("")
new_text = "\n".join(out)
path.write_text(new_text, encoding="utf-8")
return {
"name": filename,
"old_lines": len(text.splitlines()),
"new_lines": len(new_text.splitlines()),
"head_assets": len(head_assets),
"script_blocks": len(script_blocks),
"body_attrs": bool(body_attrs),
}
result = migrate("install.html")
print(result)
```
- [ ] **Step 3: Decide content vs layout block.**
`install.html`'s top-level wrapper after the header — read it. If it's just standard content, `{% block content %}` is right (base.html wraps it in `
`). If it has its own `` or full-bleed elements, switch to `{% block layout %}` (which overrides base.html's container entirely) and manually re-add the `_app_header.html` include and `` wrapper. **Default to `content`; switch only if the page looks broken in browser.**
- [ ] **Step 4: Smoke test in dev server.**
```bash
LOCAL_DEV_MODE=1 DATA_DIR=/tmp/agnes-design-pass-data .venv/bin/uvicorn app.main:app --port 8765 > /tmp/uv-install.log 2>&1 &
sleep 4
agent-browser open http://localhost:8765/install --wait-until networkidle
agent-browser screenshot /tmp/install-after.png --full
# Click Admin dropdown
agent-browser snapshot -i | grep -E "Admin|Hide"
# Verify install page's own behavior (whatever it does — copy buttons, accordions, etc.)
pkill -f "uvicorn.*8765"
```
Compare `install-after.png` against the baseline from `/tmp/design-pass-baseline/`.
- [ ] **Step 5: Run tests + commit.**
```bash
.venv/bin/python -m pytest tests/ -k "install or web_ui" -q
git add app/web/templates/install.html
git commit -m "refactor(install): extend base.html instead of standalone
Pilots the 5-page standalone→framework migration. install.html now
inherits , , font preconnect, theme include, app-header,
and the app.js script tag from base.html. Page-specific styles and
scripts kept verbatim inside head_extra + scripts blocks. -1097 line
template becomes ~+200 less (head/body scaffolding deleted)."
```
---
## Task 2: Migrate corporate_memory.html
**Files:** `app/web/templates/corporate_memory.html`
Same recipe as Task 1. Note: page is admin-gated (`{% if session.user %}` checks in markup). The wrapping logic isn't part of the migration — base.html's `_app_header.html` already handles the auth check.
- [ ] **Step 1–5: Apply Task-1 recipe verbatim.** Commit as `refactor(memory): extend base.html`.
- [ ] **Step 6: Verify memory-page-specific JS** (knowledge filter, voting, sync status) still works on `/corporate-memory`. Browser test: click a knowledge item, click upvote, click filter pill.
---
## Task 3: Migrate corporate_memory_admin.html
**Files:** `app/web/templates/corporate_memory_admin.html`
Same recipe. Admin-only curation page; modals + accordion behavior to verify.
- [ ] Apply recipe + commit `refactor(memory-admin): extend base.html`.
- [ ] Browser test: open a knowledge item modal, edit, save (don't commit DB; just verify modal opens and closes).
---
## Task 4: Migrate catalog.html
**Files:** `app/web/templates/catalog.html`
The biggest of the four "memory + catalog + install" group (2524 lines). Has source-cards, accordion, profiler overlay, two inline `