feat: Added GET /api/v1/stats endpoint returning live technique_count (…
- "backend/routers/stats.py" - "backend/main.py" GSD-Task: S03/T01
This commit is contained in:
parent
9f0b0922b0
commit
1f783c4216
10 changed files with 504 additions and 2 deletions
|
|
@ -31,3 +31,4 @@
|
|||
| D023 | M012/S01 | architecture | Qdrant embedding text enrichment strategy | Prepend creator_name and join topic_tags into embedding text for technique pages and key moments. Batch-resolve creator names at stage 6 start. | Semantic search now surfaces results for creator-name queries and tag-specific queries. Batch resolution avoids N+1 lookups during embedding. Reindex-all endpoint enables one-shot re-embedding after text composition changes. | Yes | agent |
|
||||
| D024 | M014/S01 | architecture | Content model for sections with subsections | Sections with subsections use empty-string content field; substance lives in subsections | Avoids duplication between section-level content and subsection content. The section heading serves as H2 container; all prose lives in subsection content fields. Sections without subsections use the section content field directly. | Yes | agent |
|
||||
| D025 | M015 | architecture | Search query storage and popular searches architecture | PostgreSQL search_log table + Redis read-through cache with 5-min TTL | PostgreSQL gives full historical data for future analytics (zero-result queries, time-of-day patterns). Redis cache prevents DB query on every homepage load. 5-min TTL balances freshness with load. Volume is tiny at current scale. | Yes | collaborative |
|
||||
| D026 | | requirement | R033 | validated | Creators browse page shows "Last updated: Apr 2/3" per creator with techniques, omits for 0-technique creators. Homepage recently-added cards show subtle date stamps. Both verified live on ub01:8096. | Yes | agent |
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Add social proof and freshness signals to the public site: search query logging
|
|||
| ID | Slice | Risk | Depends | Done | After this |
|
||||
|----|-------|------|---------|------|------------|
|
||||
| S01 | Search Query Logging + Popular Searches API | medium | — | ✅ | GET /api/v1/search/popular returns top 10 queries from last 7 days, cached in Redis. search_log table has rows from real searches. |
|
||||
| S02 | Creator Freshness + Homepage Card Dates | low | — | ⬜ | Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date. |
|
||||
| S02 | Creator Freshness + Homepage Card Dates | low | — | ✅ | Creators browse page shows 'Last updated: Apr 3' per creator. Homepage recently-added cards show a subtle date. |
|
||||
| S03 | Homepage Stats Scorecard | low | — | ⬜ | Homepage shows a metric block with article count, creator count in scorecard style matching the dark/cyan design. |
|
||||
| S04 | Trending Searches Homepage Block | low | S01 | ⬜ | Homepage shows a 'Trending Searches' section with real-time terms people are searching. |
|
||||
| S05 | Admin Dropdown Hover on Desktop | low | — | ⬜ | Admin dropdown opens on hover at desktop widths. On mobile, opens on tap. |
|
||||
|
|
|
|||
92
.gsd/milestones/M015/slices/S02/S02-SUMMARY.md
Normal file
92
.gsd/milestones/M015/slices/S02/S02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
id: S02
|
||||
parent: M015
|
||||
milestone: M015
|
||||
provides:
|
||||
- last_technique_at field on /api/v1/creators response
|
||||
- Creator freshness dates on browse page
|
||||
- Date stamps on homepage recently-added cards
|
||||
requires:
|
||||
[]
|
||||
affects:
|
||||
[]
|
||||
key_files:
|
||||
- backend/schemas.py
|
||||
- backend/routers/creators.py
|
||||
- frontend/src/api/public-client.ts
|
||||
- frontend/src/pages/CreatorsBrowse.tsx
|
||||
- frontend/src/pages/Home.tsx
|
||||
- frontend/src/App.css
|
||||
key_decisions:
|
||||
- Used correlated scalar subquery for last_technique_at to preserve existing query structure
|
||||
- Used opacity-reduced --color-text-secondary for muted date styling
|
||||
patterns_established:
|
||||
- (none)
|
||||
observability_surfaces:
|
||||
- none
|
||||
drill_down_paths:
|
||||
- .gsd/milestones/M015/slices/S02/tasks/T01-SUMMARY.md
|
||||
- .gsd/milestones/M015/slices/S02/tasks/T02-SUMMARY.md
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-03T04:21:37.819Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# S02: Creator Freshness + Homepage Card Dates
|
||||
|
||||
**Added last_technique_at to the creators API and rendered freshness dates on both the creators browse page and homepage recently-added cards.**
|
||||
|
||||
## What Happened
|
||||
|
||||
Two tasks delivered this slice end-to-end.
|
||||
|
||||
T01 added a correlated scalar subquery (`MAX(technique_pages.created_at)`) to the `list_creators` endpoint, exposed via a new `last_technique_at` field on the `CreatorBrowseItem` schema. The subquery preserves the existing query structure — no JOINs or GROUP BY changes. Deployed and verified on ub01: all 5 creators with techniques return datetime values, Caracal Project (0 techniques) returns null.
|
||||
|
||||
T02 added the field to the frontend TypeScript interface, rendered "Last updated: Mon D" on creator browse rows (only when non-null), and added `.recent-card__date` elements on homepage recently-added cards with muted styling (opacity-reduced secondary text). Rebuilt and deployed the web container. Browser assertions confirmed dates appear in short format (Apr 2, Apr 3) on both pages.
|
||||
|
||||
## Verification
|
||||
|
||||
Slice-level verification on live deployment (ub01:8096):
|
||||
|
||||
1. **API field presence:** `curl /api/v1/creators?limit=5 | grep last_technique_at` — field present on all items, non-null datetimes for creators with techniques.
|
||||
2. **Creators browse page:** browser_assert confirmed "Last updated" text visible and `.creator-row__updated` selector present. Caracal Project (0 techniques) has no date element.
|
||||
3. **Homepage cards:** browser_assert confirmed `.recent-card__date` selector visible, "Apr 3" and "Apr 2" text visible in recently-added section.
|
||||
4. **Date format:** All dates use short format (Mon D), not full dates or ISO strings.
|
||||
|
||||
## Requirements Advanced
|
||||
|
||||
- R033 — Validated — creators browse shows last updated dates, homepage cards show date stamps, both verified live
|
||||
|
||||
## Requirements Validated
|
||||
|
||||
- R033 — Browser assertions on ub01:8096 confirm dates on creators page and homepage cards in short Mon D format
|
||||
|
||||
## New Requirements Surfaced
|
||||
|
||||
None.
|
||||
|
||||
## Requirements Invalidated or Re-scoped
|
||||
|
||||
None.
|
||||
|
||||
## Deviations
|
||||
|
||||
Docker Compose service name is chrysopedia-web, not chrysopedia-web-8096 as stated in the plan. SPA curl verification replaced with browser_assert since HTML is rendered client-side.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
None.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `backend/schemas.py` — Added last_technique_at: datetime | None to CreatorBrowseItem
|
||||
- `backend/routers/creators.py` — Added correlated MAX(created_at) subquery and row unpacking for last_technique_at
|
||||
- `frontend/src/api/public-client.ts` — Added last_technique_at to CreatorBrowseItem interface
|
||||
- `frontend/src/pages/CreatorsBrowse.tsx` — Render Last updated: Mon D on creator rows when non-null
|
||||
- `frontend/src/pages/Home.tsx` — Render .recent-card__date on recently-added cards
|
||||
- `frontend/src/App.css` — Added .creator-row__updated and .recent-card__date styles
|
||||
46
.gsd/milestones/M015/slices/S02/S02-UAT.md
Normal file
46
.gsd/milestones/M015/slices/S02/S02-UAT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# S02: Creator Freshness + Homepage Card Dates — UAT
|
||||
|
||||
**Milestone:** M015
|
||||
**Written:** 2026-04-03T04:21:37.819Z
|
||||
|
||||
## UAT: Creator Freshness + Homepage Card Dates
|
||||
|
||||
### Preconditions
|
||||
- Chrysopedia running on ub01:8096
|
||||
- At least one creator with 0 techniques (Caracal Project)
|
||||
- At least one creator with techniques (COPYCATT, Skope, etc.)
|
||||
|
||||
### Test Cases
|
||||
|
||||
#### TC1: API returns last_technique_at field
|
||||
1. `curl -s http://ub01:8096/api/v1/creators?limit=6 | python3 -m json.tool`
|
||||
2. **Expected:** Every item in the response has a `last_technique_at` field
|
||||
3. Creators with techniques: field is an ISO datetime string (e.g., `2026-04-02T23:19:16.258484`)
|
||||
4. Caracal Project (0 techniques): field is `null`
|
||||
|
||||
#### TC2: Creators browse page shows freshness dates
|
||||
1. Navigate to http://ub01:8096/creators
|
||||
2. **Expected:** Each creator row with techniques shows "Last updated: Mon D" (e.g., "Last updated: Apr 2")
|
||||
3. Caracal Project row shows stats but NO "Last updated" text
|
||||
|
||||
#### TC3: Creators browse page date format
|
||||
1. Navigate to http://ub01:8096/creators
|
||||
2. **Expected:** Dates use short format: "Apr 2", "Apr 3" — NOT "April 2, 2026" or "2026-04-02"
|
||||
|
||||
#### TC4: Homepage recently-added cards show dates
|
||||
1. Navigate to http://ub01:8096
|
||||
2. Scroll to "Recently Added" section
|
||||
3. **Expected:** Each card shows a small muted date next to or near the creator name
|
||||
4. Dates use short format (e.g., "Apr 3")
|
||||
|
||||
#### TC5: Date styling is muted
|
||||
1. Navigate to http://ub01:8096/creators
|
||||
2. **Expected:** "Last updated" text is visually muted (smaller, lighter) compared to primary stats text
|
||||
3. Navigate to http://ub01:8096
|
||||
4. **Expected:** Card dates are subtle — not competing with title or creator name for visual hierarchy
|
||||
|
||||
#### TC6: Pagination and sorting unaffected
|
||||
1. `curl -s 'http://ub01:8096/api/v1/creators?limit=2&offset=2&sort=views'`
|
||||
2. **Expected:** Returns 2 creators, both with `last_technique_at` field. Pagination works correctly.
|
||||
3. `curl -s 'http://ub01:8096/api/v1/creators?sort=alpha'`
|
||||
4. **Expected:** Alphabetical sort still works. `last_technique_at` present on all items.
|
||||
9
.gsd/milestones/M015/slices/S02/tasks/T02-VERIFY.json
Normal file
9
.gsd/milestones/M015/slices/S02/tasks/T02-VERIFY.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T02",
|
||||
"unitId": "M015/S02/T02",
|
||||
"timestamp": 1775189992286,
|
||||
"passed": true,
|
||||
"discoverySource": "none",
|
||||
"checks": []
|
||||
}
|
||||
|
|
@ -1,6 +1,74 @@
|
|||
# S03: Homepage Stats Scorecard
|
||||
|
||||
**Goal:** Add a stats API endpoint and render a scorecard block on the homepage.
|
||||
**Goal:** Homepage shows a stats scorecard with live article count and creator count from the API.
|
||||
**Demo:** After this: Homepage shows a metric block with article count, creator count in scorecard style matching the dark/cyan design.
|
||||
|
||||
## Tasks
|
||||
- [x] **T01: Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL** — Create a new stats router with a single endpoint that returns technique_count and creator_count. Register it in main.py.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `backend/routers/stats.py` with a `GET /stats` endpoint:
|
||||
- Import `APIRouter`, `Depends`, `AsyncSession` from FastAPI/SQLAlchemy
|
||||
- Import `get_session` from `database`
|
||||
- Import `TechniquePage` and `Creator` from `models`
|
||||
- Use `select(func.count()).select_from(TechniquePage)` and same for `Creator`
|
||||
- Return a dict `{"technique_count": N, "creator_count": N}`
|
||||
- Use `tags=["stats"]` on the router
|
||||
2. Edit `backend/main.py` to add `from routers import stats` and `app.include_router(stats.router, prefix="/api/v1")` following the existing pattern (after the last `include_router` call around line 88).
|
||||
3. Rebuild and restart the API container on ub01:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api'
|
||||
```
|
||||
4. Verify: `curl http://ub01:8096/api/v1/stats` returns JSON with both counts > 0.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] Endpoint returns `{"technique_count": int, "creator_count": int}`
|
||||
- [ ] Both counts are > 0 against the live database
|
||||
- [ ] Router registered with `/api/v1` prefix
|
||||
|
||||
## Verification
|
||||
|
||||
- `curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0, f'Bad counts: {d}'"` exits 0
|
||||
- `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` returns 200
|
||||
- Estimate: 20m
|
||||
- Files: backend/routers/stats.py, backend/main.py
|
||||
- Verify: curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0"
|
||||
- [ ] **T02: Add homepage stats scorecard section with live counts from /api/v1/stats** — Wire up the frontend to fetch stats from the new endpoint and render a scorecard section on the homepage matching the dark/cyan design.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Edit `frontend/src/api/public-client.ts`:
|
||||
- Add `StatsResponse` interface: `{ technique_count: number; creator_count: number }`
|
||||
- Add `fetchStats()` function: `return request<StatsResponse>(\`${BASE}/stats\`)` — follows the exact pattern of `fetchTopics()` at line 311.
|
||||
2. Edit `frontend/src/pages/Home.tsx`:
|
||||
- Import `fetchStats` and `StatsResponse` (if exported as type) from `public-client`
|
||||
- Add state: `const [stats, setStats] = useState<{technique_count: number; creator_count: number} | null>(null)`
|
||||
- Add a `useEffect` (with cancellation flag pattern matching existing effects in the file) that calls `fetchStats()` and sets state.
|
||||
- Render a `<section className="home-stats">` between the nav-cards section (ends ~line 177) and the random technique button (starts ~line 180). Show two metric items: technique count labeled "Articles" and creator count labeled "Creators". Use `card-stagger` animation class. Only render when stats is non-null.
|
||||
3. Edit `frontend/src/App.css`:
|
||||
- Add `.home-stats` section styles: centered flex row, gap, subtle background using `var(--surface-1)`, border using `var(--border)`.
|
||||
- `.home-stats__metric`: large font for the number (use `var(--accent)` cyan color), small label below in `var(--text-secondary)`.
|
||||
- Keep it subtle — communicating volume without being boastful per R034.
|
||||
4. Verify `npm run build` compiles cleanly.
|
||||
5. Rebuild and restart the web container on ub01:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096'
|
||||
```
|
||||
6. Verify the homepage at http://ub01:8096 shows the scorecard with real counts.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] `fetchStats()` function in public-client.ts
|
||||
- [ ] Homepage renders scorecard section with technique count and creator count
|
||||
- [ ] Scorecard uses existing CSS custom properties (--accent, --surface-1, --border, --text-secondary)
|
||||
- [ ] `npm run build` succeeds
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd frontend && npm run build` exits 0
|
||||
- Homepage at http://ub01:8096 displays scorecard with two metrics showing numbers > 0
|
||||
- Estimate: 30m
|
||||
- Files: frontend/src/api/public-client.ts, frontend/src/pages/Home.tsx, frontend/src/App.css
|
||||
- Verify: cd frontend && npm run build
|
||||
|
|
|
|||
99
.gsd/milestones/M015/slices/S03/S03-RESEARCH.md
Normal file
99
.gsd/milestones/M015/slices/S03/S03-RESEARCH.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# S03 Research — Homepage Stats Scorecard
|
||||
|
||||
**Depth:** Light — straightforward UI addition using existing data patterns.
|
||||
|
||||
**Target Requirement:** R034 — Homepage Stats Scorecard (active)
|
||||
|
||||
## Summary
|
||||
|
||||
Add a visual stats scorecard to the homepage showing article (technique) count and creator count. No new models or migrations needed — just a lightweight backend stats endpoint and a new frontend section.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Add a `GET /api/v1/stats` endpoint returning `{technique_count, creator_count}`. Render a scorecard section on `Home.tsx` between the nav-cards and the random-technique button (or between featured and recently-added — planner decides placement). Style with existing dark/cyan design tokens.
|
||||
|
||||
## Implementation Landscape
|
||||
|
||||
### Backend — New Stats Endpoint
|
||||
|
||||
**File:** `backend/routers/stats.py` (new router, ~30 lines)
|
||||
|
||||
Create a simple router with one endpoint:
|
||||
```
|
||||
GET /api/v1/stats → { technique_count: int, creator_count: int }
|
||||
```
|
||||
|
||||
Implementation: Two `SELECT COUNT(*)` queries against `TechniquePage` and `Creator` tables. Both models already exist in `backend/models.py`.
|
||||
|
||||
**Registration:** Add `stats.router` to `backend/main.py` alongside existing routers (pattern: `app.include_router(stats.router, prefix="/api/v1")`).
|
||||
|
||||
Existing router registration pattern in `main.py`:
|
||||
```python
|
||||
app.include_router(techniques.router, prefix="/api/v1")
|
||||
app.include_router(creators.router, prefix="/api/v1")
|
||||
# ... etc
|
||||
```
|
||||
|
||||
**Alternative considered:** Reusing existing list endpoints with `limit=0` to extract `total` — rejected because it runs full query pipelines (joins, subqueries for counts per item) when we only need two simple COUNTs.
|
||||
|
||||
### Frontend — API Client Addition
|
||||
|
||||
**File:** `frontend/src/api/public-client.ts`
|
||||
|
||||
Add:
|
||||
- `StatsResponse` interface: `{ technique_count: number; creator_count: number }`
|
||||
- `fetchStats()` function: `GET ${BASE}/stats`
|
||||
|
||||
Pattern matches existing functions like `fetchTopics()`, `fetchRandomTechnique()`.
|
||||
|
||||
### Frontend — Homepage Scorecard Section
|
||||
|
||||
**File:** `frontend/src/pages/Home.tsx`
|
||||
|
||||
Add a new `useEffect` + state for stats (`useState<StatsResponse | null>(null)`). Render a scorecard section. Placement: between the nav-cards section and the random-technique button — this puts it in the "discovery" zone of the page.
|
||||
|
||||
The scorecard should show two metrics: technique count and creator count. Each metric: large number + label below. Use `card-stagger` animation class already present on other homepage sections.
|
||||
|
||||
### Frontend — CSS Styling
|
||||
|
||||
**File:** `frontend/src/App.css`
|
||||
|
||||
Add `.home-stats` section styles. Design tokens already in use:
|
||||
- Background: `var(--surface-1)` or similar dark surface
|
||||
- Accent: cyan (`var(--accent)` / `#00e5ff` range used throughout)
|
||||
- Text: `var(--text-primary)` / `var(--text-secondary)`
|
||||
- Border: `1px solid var(--border)` pattern used on cards
|
||||
|
||||
The scorecard should be subtle — communicating volume without being boastful (per R034 requirement). Two numbers in a horizontal row with small labels, possibly with a subtle border or background.
|
||||
|
||||
Existing homepage section naming pattern: `.home-hero`, `.home-featured`, `.home-popular-topics`, `.home-how-it-works`.
|
||||
|
||||
### Existing Patterns to Follow
|
||||
|
||||
- **Router pattern:** See `backend/routers/creators.py` lines 24-106 for the simplest existing router
|
||||
- **Frontend fetch pattern:** See `fetchTopics()` in `public-client.ts` (line 311) — same shape needed
|
||||
- **Homepage section pattern:** Each section in `Home.tsx` has its own `useEffect` with cancellation flag, own state, and a `<section>` block with BEM-style classes
|
||||
- **Stagger animation:** `card-stagger` class + `--stagger-index` CSS variable already used on nav-cards and recent-cards
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Action | Notes |
|
||||
|------|--------|-------|
|
||||
| `backend/routers/stats.py` | Create | New router, ~30 lines |
|
||||
| `backend/main.py` | Edit | Register stats router |
|
||||
| `frontend/src/api/public-client.ts` | Edit | Add StatsResponse + fetchStats |
|
||||
| `frontend/src/pages/Home.tsx` | Edit | Add stats state + scorecard section |
|
||||
| `frontend/src/App.css` | Edit | Add .home-stats styles |
|
||||
|
||||
### Natural Task Seams
|
||||
|
||||
1. **Backend stats endpoint** (stats.py + main.py registration) — verify with curl
|
||||
2. **Frontend scorecard** (public-client.ts + Home.tsx + App.css) — verify with browser
|
||||
|
||||
Two tasks. Backend first since frontend depends on it. Both are small.
|
||||
|
||||
### Verification
|
||||
|
||||
- `curl http://ub01:8096/api/v1/stats` returns `{"technique_count": N, "creator_count": N}` with N > 0
|
||||
- Homepage at `http://ub01:8096` shows scorecard block with real counts
|
||||
- `npm run build` succeeds (frontend compiles)
|
||||
51
.gsd/milestones/M015/slices/S03/tasks/T01-PLAN.md
Normal file
51
.gsd/milestones/M015/slices/S03/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
estimated_steps: 22
|
||||
estimated_files: 2
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T01: Add GET /api/v1/stats endpoint returning technique and creator counts
|
||||
|
||||
Create a new stats router with a single endpoint that returns technique_count and creator_count. Register it in main.py.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `backend/routers/stats.py` with a `GET /stats` endpoint:
|
||||
- Import `APIRouter`, `Depends`, `AsyncSession` from FastAPI/SQLAlchemy
|
||||
- Import `get_session` from `database`
|
||||
- Import `TechniquePage` and `Creator` from `models`
|
||||
- Use `select(func.count()).select_from(TechniquePage)` and same for `Creator`
|
||||
- Return a dict `{"technique_count": N, "creator_count": N}`
|
||||
- Use `tags=["stats"]` on the router
|
||||
2. Edit `backend/main.py` to add `from routers import stats` and `app.include_router(stats.router, prefix="/api/v1")` following the existing pattern (after the last `include_router` call around line 88).
|
||||
3. Rebuild and restart the API container on ub01:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-api && docker compose up -d chrysopedia-api'
|
||||
```
|
||||
4. Verify: `curl http://ub01:8096/api/v1/stats` returns JSON with both counts > 0.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] Endpoint returns `{"technique_count": int, "creator_count": int}`
|
||||
- [ ] Both counts are > 0 against the live database
|
||||
- [ ] Router registered with `/api/v1` prefix
|
||||
|
||||
## Verification
|
||||
|
||||
- `curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0, f'Bad counts: {d}'"` exits 0
|
||||
- `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` returns 200
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``backend/models.py` — TechniquePage and Creator model definitions`
|
||||
- ``backend/database.py` — get_session dependency`
|
||||
- ``backend/main.py` — router registration pattern (lines 78-88)`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``backend/routers/stats.py` — new stats router with GET /stats endpoint`
|
||||
- ``backend/main.py` — edited to register stats router`
|
||||
|
||||
## Verification
|
||||
|
||||
curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0"
|
||||
77
.gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md
Normal file
77
.gsd/milestones/M015/slices/S03/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S03
|
||||
milestone: M015
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["backend/routers/stats.py", "backend/main.py"]
|
||||
key_decisions: ["Used `or 0` fallback on scalar results for safety against empty tables"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Both slice-level verification commands pass: JSON assertion confirms technique_count=21 and creator_count=7 (both >0), HTTP status returns 200."
|
||||
completed_at: 2026-04-03T04:25:55.041Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL
|
||||
|
||||
> Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T01
|
||||
parent: S03
|
||||
milestone: M015
|
||||
key_files:
|
||||
- backend/routers/stats.py
|
||||
- backend/main.py
|
||||
key_decisions:
|
||||
- Used `or 0` fallback on scalar results for safety against empty tables
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-03T04:25:55.042Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL
|
||||
|
||||
**Added GET /api/v1/stats endpoint returning live technique_count (21) and creator_count (7) from PostgreSQL**
|
||||
|
||||
## What Happened
|
||||
|
||||
Created backend/routers/stats.py with an async endpoint running COUNT(*) queries against TechniquePage and Creator tables. Registered the router in main.py with /api/v1 prefix. Pushed to GitHub, rebuilt the API container on ub01, and verified both counts are positive against the live database.
|
||||
|
||||
## Verification
|
||||
|
||||
Both slice-level verification commands pass: JSON assertion confirms technique_count=21 and creator_count=7 (both >0), HTTP status returns 200.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `curl -s http://ub01:8096/api/v1/stats | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['technique_count']>0 and d['creator_count']>0"` | 0 | ✅ pass | 1000ms |
|
||||
| 2 | `curl -s -o /dev/null -w '%{http_code}' http://ub01:8096/api/v1/stats` | 0 | ✅ pass (200) | 1000ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `backend/routers/stats.py`
|
||||
- `backend/main.py`
|
||||
|
||||
|
||||
## Deviations
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
59
.gsd/milestones/M015/slices/S03/tasks/T02-PLAN.md
Normal file
59
.gsd/milestones/M015/slices/S03/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
estimated_steps: 28
|
||||
estimated_files: 3
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T02: Add homepage stats scorecard section with live counts from /api/v1/stats
|
||||
|
||||
Wire up the frontend to fetch stats from the new endpoint and render a scorecard section on the homepage matching the dark/cyan design.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Edit `frontend/src/api/public-client.ts`:
|
||||
- Add `StatsResponse` interface: `{ technique_count: number; creator_count: number }`
|
||||
- Add `fetchStats()` function: `return request<StatsResponse>(\`${BASE}/stats\`)` — follows the exact pattern of `fetchTopics()` at line 311.
|
||||
2. Edit `frontend/src/pages/Home.tsx`:
|
||||
- Import `fetchStats` and `StatsResponse` (if exported as type) from `public-client`
|
||||
- Add state: `const [stats, setStats] = useState<{technique_count: number; creator_count: number} | null>(null)`
|
||||
- Add a `useEffect` (with cancellation flag pattern matching existing effects in the file) that calls `fetchStats()` and sets state.
|
||||
- Render a `<section className="home-stats">` between the nav-cards section (ends ~line 177) and the random technique button (starts ~line 180). Show two metric items: technique count labeled "Articles" and creator count labeled "Creators". Use `card-stagger` animation class. Only render when stats is non-null.
|
||||
3. Edit `frontend/src/App.css`:
|
||||
- Add `.home-stats` section styles: centered flex row, gap, subtle background using `var(--surface-1)`, border using `var(--border)`.
|
||||
- `.home-stats__metric`: large font for the number (use `var(--accent)` cyan color), small label below in `var(--text-secondary)`.
|
||||
- Keep it subtle — communicating volume without being boastful per R034.
|
||||
4. Verify `npm run build` compiles cleanly.
|
||||
5. Rebuild and restart the web container on ub01:
|
||||
```
|
||||
ssh ub01 'cd /vmPool/r/repos/xpltdco/chrysopedia && git pull && docker compose build chrysopedia-web-8096 && docker compose up -d chrysopedia-web-8096'
|
||||
```
|
||||
6. Verify the homepage at http://ub01:8096 shows the scorecard with real counts.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] `fetchStats()` function in public-client.ts
|
||||
- [ ] Homepage renders scorecard section with technique count and creator count
|
||||
- [ ] Scorecard uses existing CSS custom properties (--accent, --surface-1, --border, --text-secondary)
|
||||
- [ ] `npm run build` succeeds
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd frontend && npm run build` exits 0
|
||||
- Homepage at http://ub01:8096 displays scorecard with two metrics showing numbers > 0
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``backend/routers/stats.py` — stats endpoint created in T01`
|
||||
- ``frontend/src/api/public-client.ts` — existing fetch pattern to follow`
|
||||
- ``frontend/src/pages/Home.tsx` — homepage component to add scorecard section`
|
||||
- ``frontend/src/App.css` — stylesheet for scorecard styles`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``frontend/src/api/public-client.ts` — StatsResponse interface and fetchStats() added`
|
||||
- ``frontend/src/pages/Home.tsx` — stats scorecard section rendered between nav-cards and random button`
|
||||
- ``frontend/src/App.css` — .home-stats section styles added`
|
||||
|
||||
## Verification
|
||||
|
||||
cd frontend && npm run build
|
||||
Loading…
Add table
Reference in a new issue