diff --git a/.planning/M016-ux-brand-reading-experience.md b/.planning/M016-ux-brand-reading-experience.md new file mode 100644 index 0000000..5402224 --- /dev/null +++ b/.planning/M016-ux-brand-reading-experience.md @@ -0,0 +1,142 @@ +# M016: UX Polish, Brand & Reading Experience + +> **Stream:** Frontend — intended for a dedicated GSD milestone instance +> **Conflict zone:** `frontend/src/` only — no backend Python changes +> **Deploy cadence:** commit-build-redeploy after each slice completion + +--- + +## Goal + +Modernize the public site's visual identity and reading experience, fix pipeline admin UI bugs, and establish a brand baseline (logo, favicon, OG tags). Every change in this milestone lives entirely in the frontend — CSS, React components, and static assets. + +--- + +## Slice Breakdown + +### S01: Landing Page Visual Fixes (Quick Wins) +**Risk:** Low | **Effort:** Small | **Files:** `App.css`, `Home.tsx` + +Research found 5 concrete bugs/inconsistencies on the homepage: + +| # | Issue | Fix | +|---|-------|-----| +| 1 | Duplicate `.btn` rule at App.css:3185 overrides CTA sizing (renders 131x38 instead of ~195x48) | Remove or merge the duplicate `.btn` block | +| 2 | `.home-featured` uses `border-image` which kills `border-radius` — card renders square | Replace with pseudo-element gradient border technique | +| 3 | Three different `max-width` tracks (36rem, 42rem, none) create jagged center column | Unify to 42rem for all content sections | +| 4 | Vertical spacing irregular — Random→Featured gap is only 8px vs 24px elsewhere | Normalize section margins to 1.5rem | +| 5 | Two `border-radius` values (0.5rem vs 0.625rem) on home cards | Unify to 0.625rem | + +**Verification:** Visual screenshot comparison before/after on desktop (1280px) and mobile (375px). + +--- + +### S02: Pipeline Admin UI Fixes +**Risk:** Low | **Effort:** Small-Medium | **Files:** `AdminPipeline.tsx`, `App.css` + +Four issues identified with root causes already diagnosed: + +| # | Issue | Root Cause | Fix | +|---|-------|-----------|-----| +| 1 | Most-recent run won't collapse / flickers | `expandedRunId` in `load()` useCallback dependency array (line 729) causes race condition — collapsing sets null, which triggers load recreation, which re-expands | Remove `expandedRunId` from dependency array + use `useRef` for initial-load tracking | +| 2 | Mobile job cards show vertical text ("C h e e") | `.pipeline-video__creator` missing overflow rules (App.css:4477) | Add `overflow: hidden; text-overflow: ellipsis; white-space: nowrap` (matches `.pipeline-video__filename` pattern) | +| 3 | No stage direction chevrons | Pipeline stages listed without visual flow indicator | Add CSS chevron/arrow between stage indicators using `::after` pseudo-elements or inline SVG | +| 4 | Filter text box should be replaced with button group | Current text input for status filter; should be "ALL \| Not Started \| In Progress \| Complete" buttons, end-aligned | Replace `` with `
` flexbox, `justify-content: flex-end`, verify vertical alignment against adjacent elements | + +**Verification:** Test collapse toggle on most-recent run, resize to 375px and check creator name truncation, confirm chevrons render between stages, confirm filter buttons align right and sit level with row. + +--- + +### S03: Brand Minimum (Favicon, OG Tags, Logo) +**Risk:** Low | **Effort:** Small | **Files:** `index.html`, `App.tsx`, `App.css`, new static assets + +The site currently has: +- No favicon (browser default icon) +- No OG meta tags (no preview image when sharing URL via text/Discord) +- No logo next to "Chrysopedia" in the header + +Tasks: +1. **Design a simple logo** — something that fits the dark theme + cyan accent aesthetic. Could be a stylized book/page icon, a knowledge/crystal motif matching the "chryso-" (gold) prefix, or an abstract mark. Generate an SVG. +2. **Add favicon** — export logo as favicon.ico + apple-touch-icon + 192/512 PNG for PWA manifest +3. **Add OG meta tags** — `og:title`, `og:description`, `og:image`, `og:url`, `twitter:card` in index.html. Create a 1200x630 OG image using the logo + brand colors. +4. **Place logo in header** — render the SVG inline next to "Chrysopedia" text with appropriate sizing + +**Verification:** Share URL in Discord/iMessage and confirm preview card renders. Check favicon in browser tab. Visual check logo in header. + +--- + +### S04: ToC Modernization +**Risk:** Medium | **Effort:** Medium | **Files:** `TableOfContents.tsx`, `App.css`, `TechniquePage.tsx` + +Research identified these dated elements: +- CSS counter numbering ("1.", "1.2") — biggest offender +- Boxed card container with solid border +- Uppercase "CONTENTS" label +- No active-section highlighting +- Underline-only hover states + +Modernization plan: +1. **Remove numbered counters** — switch to unordered list with clean indentation +2. **Replace box border** with left accent bar (`border-left: 2px solid var(--color-accent)`) +3. **Change heading** from "CONTENTS" to "On this page" in sentence case +4. **Add hover background** (`rgba(34, 211, 238, 0.08)`) instead of underline +5. **Add IntersectionObserver** — track which section heading is in the viewport, highlight the corresponding ToC entry with accent left-border + brighter text color +6. **Make ToC sticky** — position it at the top of the existing sidebar (above Key Moments), `position: sticky; top: 1.5rem` + +**Verification:** Navigate to a technique page with 4+ sections. Scroll through — ToC should highlight current section. ToC stays visible in sidebar while scrolling. Hover states work. No numbering visible. + +--- + +### S05: Sticky Reading Header +**Risk:** Medium | **Effort:** Medium | **Files:** new `ReadingHeader.tsx`, `App.css`, `TechniquePage.tsx` + +New component that slides in when user scrolls past the article title: +- Shows: article title (truncated) + current section name +- `position: sticky; top: 0; z-index: 50` +- Thin bar (~40px height), `var(--color-bg-header)` background, subtle bottom border +- Hidden by default, slides in via `transform: translateY(-100%)` → `translateY(0)` transition +- Uses IntersectionObserver on the technique header element as show/hide trigger +- Shares the section-tracking observer from S04's ToC work +- On mobile: compact single-line with optional dropdown for section jump +- Update `scroll-margin-top` values on section anchors to account for new header height + +**Verification:** Open long technique page. Scroll past title — reading header appears. Correct section name updates as you scroll. Works on mobile (375px). Doesn't break existing header. + +--- + +### S06: Landing Page Personality Pass +**Risk:** Low | **Effort:** Small | **Files:** `Home.tsx`, `App.css` + +After S01 fixes the bugs, this slice adds polish: +1. **Hero tightening** — reduce hero bottom padding and how-it-works top margin to get content above the fold faster (~40-50px reclaim) +2. **Stats scorecard enhancement** — animated count-up on first view (simple `requestAnimationFrame` counter), subtle glow on numbers +3. **Random button treatment** — wrap in a small card with "Feeling adventurous?" tagline, or embed as secondary action inside Trending Searches +4. **Section heading standardization** — pick one treatment (title-case with left accent bar) and apply consistently to "Recently Added", "Trending Searches", "Popular Topics" +5. **Header brand accent** — apply `color: var(--color-accent)` or subtle gradient to "Chrysopedia" text (pairs with S03 logo) + +**Verification:** Visual check desktop + mobile. Stats animate on page load. Section headings consistent. Content peeks above fold on standard viewport. + +--- + +## Dependency Graph + +``` +S01 (landing fixes) ──→ S06 (personality pass) +S02 (pipeline fixes) [independent] +S03 (brand minimum) ──→ S06 (header accent uses logo) +S04 (ToC modern) ──→ S05 (reading header shares IntersectionObserver pattern) +``` + +S01, S02, S03, S04 can all start in parallel. S05 depends on S04. S06 depends on S01 + S03. + +Recommended execution order: **S01 → S02 → S03 → S04 → S05 → S06** + +--- + +## Out of Scope (for this milestone) + +- Creator landing page redesign (depends on backend social links API — see M017) +- Auto-avatar images (backend-gated — see M017) +- Embed tab (needs backend investigation first — see M017) +- Any backend Python changes +- M015 S04/S05 leftovers (trending searches block, admin dropdown hover) — should be completed by M015's own GSD session first diff --git a/.planning/M017-backend-perf-creator-features.md b/.planning/M017-backend-perf-creator-features.md new file mode 100644 index 0000000..3089d44 --- /dev/null +++ b/.planning/M017-backend-perf-creator-features.md @@ -0,0 +1,164 @@ +# M017: Backend Performance & Creator Features + +> **Stream:** Backend — intended for a separate Claude Code session (not GSD) +> **Conflict zone:** `backend/` only + one frontend API client fix +> **Deploy cadence:** commit-build-redeploy after each task group + +--- + +## Goal + +Fix critical performance bottlenecks in the admin pipeline API, implement auto-avatar fetching for creators, and lay the backend groundwork for creator landing page improvements. Almost entirely backend Python — minimal frontend changes. + +--- + +## Task Groups (execute sequentially) + +### 1. Critical Fixes (do first, immediate impact) + +#### 1a. Fix `worker-status` async event loop blocking +**File:** `backend/routers/pipeline.py` lines 1266-1313 +**Problem:** The endpoint is `async def` but calls three synchronous Celery inspect methods (`inspector.active()`, `.reserved()`, `.stats()`), each with a 1-second timeout. This blocks the entire uvicorn event loop for ~3 seconds, stalling ALL concurrent API requests. +**Evidence:** Every parallel API call during page load takes ~3,024ms instead of their natural 6-38ms. +**Fix:** +```python +import asyncio + +active = await asyncio.to_thread(inspector.active) or {} +reserved = await asyncio.to_thread(inspector.reserved) or {} +stats = await asyncio.to_thread(inspector.stats) or {} +``` +Also consider: reduce inspect timeout to 0.5s, add Redis cache with 10-15s TTL to avoid repeated slow calls. +**Impact:** Page load drops from ~3s to ~50ms. + +#### 1b. Fix creators endpoint 422 error +**File (frontend):** `frontend/src/pages/AdminPipeline.tsx` line 1126 +**File (backend):** `backend/routers/creators.py` line 28 +**Problem:** Frontend requests `fetchCreators({ limit: 200 })` but backend validates `le=100`. Returns 422 on every pipeline page load — creator filter dropdown never populates. +**Fix:** Change frontend call to `limit: 100`. (This is the ONE frontend file touch in this stream.) + +--- + +### 2. Pipeline API Performance + +#### 2a. Rewrite `stale-pages` to eliminate N+1 queries +**File:** `backend/routers/pipeline.py` lines 906-973 +**Problem:** Loads ALL technique pages, then runs a separate query per page for latest version + another for creator name. Currently 44 extra queries for 22 pages. Fast today (~30ms) but degrades linearly. +**Fix:** Single query joining `technique_pages` with a lateral/window subquery for latest version + join to creators. + +#### 2b. Add pagination to videos endpoint +**File:** `backend/routers/pipeline.py` lines 72-188 +**Problem:** Returns all 43 videos (23KB) with no offset/limit. Client-side filtering only. +**Fix:** Add `offset`, `limit`, `status`, `creator_id` query params. Return paginated response with `total` count. Frontend can adopt server-side filtering later (or the M016 frontend stream can wire it up). + +#### 2c. Optimize `_find_dynamic_related` for technique pages +**File:** `backend/routers/techniques.py` lines 33-111 +**Problem:** Loads ALL technique pages into memory to score relatedness in Python. O(n) in total page count. +**Fix:** Move scoring to SQL (keyword overlap via `ts_rank` or simple tag intersection) or cache related links per technique page with invalidation on new page creation. + +--- + +### 3. Auto-Avatar Integration (TheAudioDB) + +Research concluded: **TheAudioDB is the best first source** — free, no OAuth, no caching restrictions, decent coverage for established artists. + +#### 3a. Database migration +Add to `Creator` model: +- `avatar_url: String | None` — stored image URL or local path +- `avatar_source: String` — enum: `"generated"`, `"theaudiodb"`, `"manual"` +- `avatar_fetched_at: DateTime | None` — for cache invalidation + +Alembic migration (will be 014 or later depending on M015 state). + +#### 3b. TheAudioDB lookup service +New file: `backend/services/avatar.py` +- `async def fetch_avatar(creator_name: str, creator_genres: list[str]) -> AvatarResult | None` +- Calls `https://www.theaudiodb.com/api/v1/json/{key}/search.php?s={name}` +- Confidence scoring: name match via `thefuzz.fuzz.token_sort_ratio` (threshold ≥ 85%), genre overlap as tiebreaker +- Returns `strArtistThumb` URL if confident match, None otherwise +- Handle: no results, multiple results, missing image fields + +#### 3c. Celery worker task +New task: `tasks.fetch_creator_avatar` +- Called on creator creation or manually via admin endpoint +- Runs TheAudioDB lookup → downloads image → stores locally (or stores URL) +- Updates `Creator.avatar_url`, `avatar_source`, `avatar_fetched_at` +- Falls back gracefully — if no match, leaves fields null (frontend already renders generated SVG as fallback) + +#### 3d. Admin endpoint for manual trigger +`POST /admin/pipeline/creators/{id}/fetch-avatar` — triggers the worker task for a specific creator. +`POST /admin/pipeline/creators/fetch-all-avatars` — batch trigger for all creators missing avatars. + +#### 3e. Wire avatar_url into creators API responses +Add `avatar_url` to `CreatorBrowseItem` and `CreatorDetail` schemas. The frontend `CreatorAvatar` component already accepts an `imageUrl` prop — it will just work once the API returns it. + +--- + +### 4. Creator Landing Page API Groundwork + +#### 4a. Social links model + migration +Add to `Creator` model: +- `social_links: JSON | None` — structured as `{"spotify": "url", "instagram": "url", "bandcamp": "url", "website": "url", ...}` +- `bio: Text | None` — short creator bio/description +- `featured: Boolean` — flag for homepage featuring + +#### 4b. Creator detail endpoint enhancement +Expand `GET /api/v1/creators/{slug}` to return: +- `social_links` +- `bio` +- `avatar_url` +- `technique_count` +- Full technique list with titles, slugs, created_at +- Genre breakdown + +#### 4c. Admin endpoint for creator profile editing +`PUT /admin/pipeline/creators/{id}` — update `bio`, `social_links`, `featured` flag, manually set `avatar_url`. + +--- + +### 5. Embed Tab Investigation +The "Embed" tab under pipeline jobs is non-functional. Before building, need to: +- Read the existing frontend component to understand what it expects +- Determine what "Embed" should show (embedding vectors? embed codes? embedded content?) +- If it's about Qdrant vector embeddings: add an endpoint to query embedding status per technique page +- If it's about iframe embed codes: generate shareable snippet per technique + +**This task starts as investigation — scope will be defined after reading the code.** + +--- + +## General Load Time Optimization (apply throughout) + +As each endpoint is touched, also consider: +- Add `Cache-Control` headers for public GET endpoints (technique pages, creators, search suggestions) +- Add Redis caching (30s-5min TTL) for expensive or frequently-hit endpoints +- Ensure database indexes exist on commonly filtered/sorted columns +- Consider adding `select_in_loading` for SQLAlchemy relationships to avoid implicit lazy loads + +--- + +## Key Files + +| File | What changes | +|------|-------------| +| `backend/routers/pipeline.py` | worker-status async fix, stale-pages rewrite, videos pagination, avatar endpoints | +| `backend/routers/creators.py` | Creator detail expansion, social links | +| `backend/routers/techniques.py` | Related techniques optimization | +| `backend/models.py` | Creator model additions (avatar, social_links, bio) | +| `backend/schemas.py` | New response schemas | +| `backend/services/avatar.py` | New — TheAudioDB integration | +| `backend/tasks.py` | New avatar fetch task | +| `alembic/versions/014_*.py` | Migration for creator columns | +| `frontend/src/pages/AdminPipeline.tsx` | Line 1126 only — fix limit: 200 → 100 | + +--- + +## Merge Coordination with M016 + +These two streams are designed to have minimal file overlap: +- **M016 touches:** `App.css`, `Home.tsx`, `TechniquePage.tsx`, `TableOfContents.tsx`, `AdminPipeline.tsx` (CSS/JSX only), new frontend components, static assets +- **M017 touches:** `backend/` (routers, models, schemas, services, tasks), `alembic/`, one line in `AdminPipeline.tsx` + +The single conflict point is `AdminPipeline.tsx` — M017's creators limit fix (line 1126) vs M016's pipeline UI fixes (collapse bug at line 729, filter buttons, chevrons). Resolve by merging M017's one-liner first, then M016's broader changes on top. + +For the avatar/social-links frontend wiring: M017 ships the API, M016 (or a follow-up) consumes it. No conflict — just sequencing. diff --git a/frontend/src/App.css b/frontend/src/App.css index 25eeb32..c69ad74 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1028,6 +1028,16 @@ a.app-footer__repo:hover { padding: 0.375rem 0; } +/* Bridge the gap between trigger and menu so hover doesn't break */ +.admin-dropdown__menu::before { + content: ''; + position: absolute; + top: -0.5rem; + right: 0; + width: 100%; + height: 0.5rem; +} + .admin-dropdown__item { display: block; padding: 0.5rem 1rem; @@ -5097,13 +5107,9 @@ a.app-footer__repo:hover { } .recent-card__creator { - display: inline-flex; - align-items: center; - gap: 0.3rem; font-size: 0.8rem; color: var(--color-text-secondary); white-space: nowrap; - flex-shrink: 0; } .recent-card__header-right { @@ -5114,11 +5120,22 @@ a.app-footer__repo:hover { flex-shrink: 0; } +.recent-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-top: 0.25rem; + padding-top: 0.375rem; + border-top: 1px solid var(--color-border); +} + .recent-card__date { font-size: 0.7rem; color: var(--color-text-secondary); opacity: 0.65; white-space: nowrap; + margin-left: auto; } /* ── Search result card creator ───────────────────────────────────────── */ diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 6449c63..cdf247a 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -310,19 +310,7 @@ export default function Home() { className="recent-card card-stagger" style={{ '--stagger-index': i } as React.CSSProperties} > - - {t.title} - - {t.creator_name && ( - {t.creator_name} - )} - {t.created_at && ( - - {new Date(t.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - - )} - - + {t.title} {t.topic_category} @@ -337,11 +325,21 @@ export default function Home() { : t.summary} )} + + + + {t.creator_name || ''} + {t.key_moment_count > 0 && ( {t.key_moment_count} moment{t.key_moment_count !== 1 ? 's' : ''} )} + {t.created_at && ( + + {new Date(t.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} + + )} ))}