Engineering-Specs/10-frontend-discovery.md
Crawler Factory: Spec 10: Frontend Discovery Flow Implementation Plan
wzxhzdk:34 For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Ship the four discovery-phase UI components: RollingLog, DomainBadge, DiscoveryPanel, CategoryReview: that drive the first half of the Crawler Factory user flow. Together they let an operator paste a URL, watch streamed discovery events, and select pathGroups to configure.
Architecture: Pure presentation + form components. No backend calls live in these components: they consume props/hooks already built by Spec 09 (useDiscoveryStream). The shell page (AdminFactory.tsx) and configurator drawer (Spec 11) are out of scope. Visual conventions match the existing /admin aesthetic: dark indigo background, framer-motion fade-up, mono fonts for IDs, color-coded badges. shadcn primitives (scroll-area, card, checkbox, input, button, form, badge) are the building blocks: no custom CSS beyond Tailwind utility classes.
Tech Stack: React 18 (function components, hooks), TypeScript strict, framer-motion, react-hook-form 7 + @hookform/resolvers/zod, zod 3.25, shadcn UI (Radix primitives), Tailwind, lucide-react icons, vitest + @testing-library/react + jsdom.
Depends on:
- Spec 01 (ContentDomain type from lib/factory/types.ts)
- Spec 09 (useDiscoveryStream hook + LogEntry shape from lib/factory/types.ts)
- Existing shadcn components in src/components/ui/
Consumed by:
- Spec 11 (CategoryConfigurator reads selectedPathGroupIds from CategoryReview.onConfigureSelected)
- Spec 12 (LiveCrawlMonitor reuses RollingLog as a read-only persistent log)
- Spec 13 (smoke test verifies the discovery flow end-to-end)
Plan source: Projects/Crawler-Factory/00-Plan.md §5 (UI specs: page layout, panels), §7 Tasks 24, 25, 26. §19c for traffic-light thresholds. Visual conventions referenced from src/pages/Admin.tsx and src/components/admin/HealthHUD.tsx.
File structure
| Path | Responsibility |
|---|---|
src/components/admin/factory/RollingLog.tsx |
Auto-scrolling structured log viewer (shadcn ScrollArea). Stateless presentation only. |
src/components/admin/factory/DomainBadge.tsx |
Pill badge: content domain name, confidence percent, traffic-light dot. Stateless. |
src/components/admin/factory/DiscoveryPanel.tsx |
Step 1 panel: URL input form (react-hook-form + zod), submit, status line, embedded RollingLog. |
src/components/admin/factory/CategoryReview.tsx |
Step 2 panel: pathGroup tree grouped by content domain, checkboxes, manual reclassify dropdown, "Configure Selected (N)" CTA. |
src/components/admin/factory/__tests__/RollingLog.test.tsx |
Auto-scroll + level-color tests. |
src/components/admin/factory/__tests__/DomainBadge.test.tsx |
Domain color + traffic-light threshold tests. |
src/components/admin/factory/__tests__/DiscoveryPanel.test.tsx |
Form validation + submit tests (mocked hook). |
src/components/admin/factory/__tests__/CategoryReview.test.tsx |
Grouping + selection + CTA tests. |
SOP rules applied (non-negotiable; from CodingSOPs.md, TestingSOPs.md, WritingSOPs.md)
Every file in this spec MUST follow these rules. They are not optional.
- Two-layer type safety. zod for runtime form validation;
tsc --noEmitstrict for write-time. Noany. UseT | null, notT | undefined, when the absence is meaningful (e.g.,detectedDomain: ContentDomain | nullper Spec 01PathGroupSchema). - No
console.log. Top of every component file:import { createLogger } from '@/lib/utils/logger';andconst log = createLogger('factory:ui:rolling-log');(or appropriate context). Wrap any error path withlog.error({ event, ... }, 'short_msg'): never rawconsole.errorfrom training priors. The logger module is already shipped (src/lib/utils/logger.tsvia path alias@/lib/utils/logger): confirm path before importing; if the alias resolves tolib/utils/logger.tsuse the relative path that works in your codebase build. - Try/catch every fallible call inside event handlers. Form submit handlers wrap
discover()in try/catch and log on rejection. - Functions ≤20 lines, ≤3 params. More than 3 params → use a typed config object. Components receive a single typed
Propsobject. - Docstring on every exported component. Single line above the export explaining purpose. Comment WHY, not WHAT, for non-obvious logic (e.g., the auto-scroll ref pattern, the traffic-light threshold math).
- No raw dicts crossing module boundaries. Props are typed interfaces; arrays of
LogEntryandPathGroupcome from the Spec 01 zod-validated types. - TDD cadence. Test first → run, expect FAIL → implement → run, expect PASS → commit.
- No live network in unit tests.
useDiscoveryStreamis mocked viavi.mock. Real SSE lives behinduseDiscoveryStreamin Spec 09 and is integration-tested there. - React rules.
- Functional components only.
- Hooks at top level, never inside conditionals.
-
useEffectdeps arrays explicit; ESLintreact-hooks/exhaustive-depsmust stay clean. - No inlineany. Noas unknown as Xcasts. - Each component file exports exactly one component (plus any helper types): no barrel files. - Naming. TypeScript:
camelCasefor vars/functions,PascalCasefor components/types,UPPER_SNAKE_CASEfor module constants. Files:PascalCase.tsxfor components (matches existingHealthHUD.tsx,BugTracker.tsxconvention). Tests: same name with.test.tsx. - Conventional commits.
feat(factory):,test(factory):,chore(factory):, etc. One logical change per commit. - Accessibility. All form controls have associated
<label>(use shadcnFormLabel). Buttons disabled when invalid. Live regions (aria-live="polite") on the status line + log container so screen readers announce updates. - No frontend may render an emoji. Use
lucide-reacticons (consistent withAdmin.tsx).
Visual convention reminders (from src/pages/Admin.tsx + src/components/admin/HealthHUD.tsx)
- Background:
bg-[#0a0a0f], text white, bordersborder-white/10. - Section headers:
text-3xl font-black uppercase tracking-tighterwith indigo accent badge (bg-indigo-500/10 text-indigo-400 border border-indigo-500/30). - Section subheaders:
text-sm font-bold text-white/40 uppercase tracking-widest, flanked byh-px bg-white/10 flex-1dividers. - Cards:
bg-white/5 border border-white/10 rounded-2xl backdrop-blur-md. - Mono font:
font-monofor URLs, IDs, timestamps. - Badges:
bg-{color}-500/20 text-{color}-400 border border-{color}-500/30 rounded-md px-2 py-1 text-[10px] font-bold uppercase tracking-widest. - Animations:
framer-motioninitial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}on top-level sections.
Task 24: RollingLog: auto-scrolling structured log viewer
Files:
- Create: src/components/admin/factory/RollingLog.tsx
- Create: src/components/admin/factory/__tests__/RollingLog.test.tsx
RollingLog is a pure presentation component: takes an array of LogEntry, renders them inside shadcn ScrollArea, and auto-scrolls to the bottom whenever the list grows. Each entry shows a monospace timestamp, a colored level badge, and the message. It is reused by DiscoveryPanel (Step 1) and later by LiveCrawlMonitor (Spec 12).
LogEntry is imported from lib/factory/types.ts (Spec 01):
## Executive Team — [Company]
| Name | Title |
|---|---|
| Jane Smith | CEO |
- [ ] Step 24.1: Write failing test for empty-state render
Create src/components/admin/factory/__tests__/RollingLog.test.tsx:
User: /scout-leadership apple.com
│
▼
[1] Health check — GET /health
→ if DOWN: print startup command, exit
│
▼
[2] Map domain — POST /map {"url": "https://apple.com", "max_pages": 200}
→ get urls[] list (may take 10–45s)
│
▼
[3] Filter urls[] for leadership patterns
→ rank: /leadership > /executives > /team > /management > /about/* variants
→ if no match: log "no leadership URL found", proceed to fallback
│
▼
[4a] Scrape best-match URL — POST /scrape {"url": "...", "use_js": true}
→ if markdown word_count < 100: go to [4b]
→ if scrape fails (success=false): go to fallback
│
[4b] Retry with raw_html — POST /scrape {"url": "...", "use_js": true, "formats": ["raw_html"]}
→ parse HTML for card patterns (h3/h4 names, role/title spans)
│
▼
[5] Structure output
→ Build markdown table: | Name | Title | Source |
→ If LinkedIn links found in ScrapeResponse.links: add | LinkedIn |
→ Attach source label per row
│
▼
[6] Write file: ./{company-slug}-leadership.md
│
▼
[7] Print to chat:
## Leadership Team — Apple (apple.com)
| Name | Title | Source |
|------|-------|--------|
| Tim Cook | CEO | apple.com/leadership |
...
Source: https://www.apple.com/leadership/
Scraped: 2026-05-05T14:32:00Z | Duration: 38s
- [ ] Step 24.2: Run, expect FAIL
[3] Filter returns no match
│
▼
[F1] Attempt common fallbacks: /about, /company, /about-us
→ scrape and scan for executive mentions
│
▼
[F2] If execs found in fallback:
→ return table with WARNING banner: "Data from [fallback URL], not a dedicated leadership page"
│
▼
[F3] If nothing found:
→ return: "No leadership page found for apple.com. URLs checked: [list]. Try: apple.com/leadership manually."
Expected: FAIL: Cannot find module '../RollingLog'.
- [ ] Step 24.3: Implement
RollingLog.tsxwith empty state
Create src/components/admin/factory/RollingLog.tsx:
<
- [ ] Step 24.4: Run empty-state test, expect PASS
wzxhzdk:4
Expected: 1 test pass.
- [ ] Step 24.5: Add tests for level colors + auto-scroll
Append to src/components/admin/factory/__tests__/RollingLog.test.tsx:
wzxhzdk:5
- [ ] Step 24.6: Run all RollingLog tests, expect PASS
wzxhzdk:6
Expected: 7 tests pass.
- [ ] Step 24.7: Run
tsc --noEmit
wzxhzdk:7
Expected: clean.
- [ ] Step 24.8: Commit
wzxhzdk:8
Task 25: DomainBadge: color-coded pill with confidence + traffic light
Files:
- Create: src/components/admin/factory/DomainBadge.tsx
- Create: src/components/admin/factory/__tests__/DomainBadge.test.tsx
DomainBadge shows the detected content domain on a pill, the confidence as a small monospace percentage, and a traffic-light dot keyed to §19c thresholds:
| Confidence | Dot color | Class |
|---|---|---|
| ≥ 0.85 | green | bg-emerald-400 |
| 0.65–0.84 | yellow | bg-yellow-400 |
| < 0.65 | amber | bg-amber-500 |
Per-domain pill colors (from §7 Task 24):
| Domain | Color |
|---|---|
| marketing | indigo |
| support | red |
| education | emerald |
| technical | blue |
| customer-stories | purple |
| product-catalog | amber |
| events | pink |
| legal | slate |
| social | fuchsia |
- [ ] Step 25.1: Write failing tests
Create src/components/admin/factory/__tests__/DomainBadge.test.tsx:
wzxhzdk:9
- [ ] Step 25.2: Run, expect FAIL
wzxhzdk:10
Expected: FAIL: Cannot find module '../DomainBadge'.
- [ ] Step 25.3: Implement
DomainBadge.tsx
Create src/components/admin/factory/DomainBadge.tsx:
wzxhzdk:11
- [ ] Step 25.4: Run all DomainBadge tests, expect PASS
wzxhzdk:12
Expected: 12 tests pass.
- [ ] Step 25.5: Run
tsc --noEmit
wzxhzdk:13
Expected: clean.
- [ ] Step 25.6: Commit
wzxhzdk:14
Task 26: DiscoveryPanel: URL form + status line + embedded log
Files:
- Create: src/components/admin/factory/DiscoveryPanel.tsx
- Create: src/components/admin/factory/__tests__/DiscoveryPanel.test.tsx
DiscoveryPanel is the Step 1 layout (per §5b page diagram): a 40/60 two-column card. Panel A holds the URL input, Submit, and a status line. Panel B holds an embedded RollingLog driven by useDiscoveryStream from Spec 09.
Hook contract assumed from Spec 09 (already shipped: do NOT redefine):
wzxhzdk:15
If Spec 09 has not yet shipped, the implementation below mocks the hook in tests and the runtime imports remain valid because the import path resolves to the (separately built) Spec 09 module. Do not write Spec 09's body inside this spec.
- [ ] Step 26.1: Write failing test for URL validation
Create src/components/admin/factory/__tests__/DiscoveryPanel.test.tsx:
wzxhzdk:16
- [ ] Step 26.2: Run, expect FAIL
wzxhzdk:17
Expected: FAIL: Cannot find module '../DiscoveryPanel'.
- [ ] Step 26.3: Implement
DiscoveryPanel.tsx
Create src/components/admin/factory/DiscoveryPanel.tsx:
wzxhzdk:18
- [ ] Step 26.4: Run all DiscoveryPanel tests, expect PASS
wzxhzdk:19
Expected: 8 tests pass.
- [ ] Step 26.5: Run
tsc --noEmit
wzxhzdk:20
Expected: clean. If useDiscoveryStream is not yet shipped (Spec 09 pending), tsc will fail on the import: block this task on Spec 09 completion.
- [ ] Step 26.6: Commit
wzxhzdk:21
Task 27: CategoryReview: pathGroup tree, selection, manual reclassify, CTA
Files:
- Create: src/components/admin/factory/CategoryReview.tsx
- Create: src/components/admin/factory/__tests__/CategoryReview.test.tsx
CategoryReview renders the post-discovery tree (per §5b second diagram). Top-level rows are content domains. Each row is collapsible (chevron icon flips). Inside, each pathGroup row contains:
- shadcn
Checkbox(controlled) for selection - path pattern (mono font)
- URL count
DomainBadgewith confidence- "show samples" toggle (revealing up to 3 sample URLs)
- "reclassify" dropdown (shadcn
Select) listing all 9 content domains for manual override
The bottom CTA Configure Selected (N) is enabled only when ≥1 row is selected. Clicking calls props.onConfigureSelected(selectedPathGroupIds).
klzzwxh:0080 shape (already shipped from Spec 01 klzzwxh:0081):
wzxhzdk:22
This component owns local UI state for selection and manual domain override: both are edited optimistically, then bubbled up via onSelectionChange and onDomainOverride so the parent (Spec 13 CrawlerFactory.tsx) can persist into the session. The pathGroups prop remains the source of truth between renders.
- [ ] Step 27.1: Write failing tests
Create src/components/admin/factory/__tests__/CategoryReview.test.tsx:
wzxhzdk:23
- [ ] Step 27.2: Run, expect FAIL
wzxhzdk:24
Expected: FAIL: Cannot find module '../CategoryReview'.
- [ ] Step 27.3: Implement
CategoryReview.tsx
Create src/components/admin/factory/CategoryReview.tsx:
wzxhzdk:25
- [ ] Step 27.4: Run all CategoryReview tests, expect PASS
wzxhzdk:26
Expected: 7 tests pass.
- [ ] Step 27.5: Run
tsc --noEmit
wzxhzdk:27
Expected: clean.
- [ ] Step 27.6: Commit
wzxhzdk:28
Task 28: Cluster validation: full regression
Files: - (Read-only verification)
- [ ] Step 28.1: Run the full project test suite
wzxhzdk:29
Expected: existing baseline tests + 4 new factory UI test files (~34 new test cases), all GREEN.
- [ ] Step 28.2: Run full tsc
wzxhzdk:30
Expected: clean.
- [ ] Step 28.3: Run lint
wzxhzdk:31
Expected: clean. ESLint react-hooks/exhaustive-deps and @typescript-eslint/no-explicit-any must pass.
- [ ] Step 28.4: Visual smoke (manual, post-merge)
Boot the dev shell:
wzxhzdk:32
Navigate to /admin/factory once Spec 13 (AdminFactory.tsx) is mounted. Confirm:
- The two-column DiscoveryPanel renders with the dark indigo aesthetic.
- The URL input rejects non-https values and shows the inline error.
- An empty RollingLog shows the "no events yet" hint.
- A populated CategoryReview groups pathGroups by content domain, the chevron flips on collapse, and the "Configure Selected (0)" button is disabled.
Note: this manual smoke is documented here for completeness; it depends on Spec 09 + Spec 13. Do not block the commit on it.
- [ ] Step 28.5: Mark Spec 10 done in vault
Status.md
In the vault Projects/Crawler-Factory/Status.md, change:
wzxhzdk:33
Acceptance criteria: Spec 10 done means:
src/components/admin/factory/RollingLog.tsxexists, exportsRollingLog, accepts{ entries, maxHeight? }, auto-scrolls on new entry, color-codes level badges (info=indigo, warn=amber, error=red), and renders monospace timestamps.src/components/admin/factory/DomainBadge.tsxexists, exportsDomainBadge, accepts{ contentDomain, confidence }, renders one of nine domain colors, monospace integer percentage, and a green/yellow/amber traffic-light dot per §19c thresholds (≥0.85 / 0.65–0.84 / wzxhzdk:50.65).src/components/admin/factory/DiscoveryPanel.tsxexists, exportsDiscoveryPanel, takes no required props, owns a react-hook-form + zod URL form (https only), invokesuseDiscoveryStream.discover(url)on submit, shows status line ("Awaiting URL." → "Discovering... N URLs found, M categorized." → "Error: …"), and embedsRollingLog.src/components/admin/factory/CategoryReview.tsxexists, exportsCategoryReview, accepts{ pathGroups, onConfigureSelected, onDomainOverride? }, groups rows by detected content domain (collapsible per group), renders shadcnCheckbox,DomainBadge, sample-URL toggle (≤3 URLs), reclassify dropdown (9 options), and a "Configure Selected (N)" CTA enabled only when ≥1 row is selected.- All four components apply the
/adminvisual conventions:bg-[#0a0a0f]-compatible cards (bg-white/5 border-white/10 rounded-2xl backdrop-blur-md), uppercase tracked-widest section headers, mono fonts for IDs/URLs/timestamps, indigo accent for primary actions. - ~34 new vitest cases pass, full project test suite stays GREEN,
tsc --noEmitclean,npm run lintclean. - Per CodingSOPs: every component file has a logger, every public component has a docstring, no
any, noconsole.log, all hooks at top-level with explicit deps. All form controls have associated<label>. All live regions usearia-live="polite".
Out of scope (handled by other specs)
- The
useDiscoveryStreamhook implementation: Spec 09 (mocked here). - The
useFactorySession+useCrawlProgresshooks: Spec 09. CategoryConfiguratordrawer (Step 3: Configure): Spec 11.LiveCrawlMonitor(Step 4: Live crawl): Spec 12 (but it reusesRollingLogfrom this spec).CrawlerFactory.tsxorchestrator + FSM driver: Spec 13.AdminFactory.tsxpage shell + route registration: Spec 13.- Backend SSE endpoints (
/api/factory/discover, etc.): Specs 06–08. - Persisting
selectedPathGroupIdsto the session record: Spec 13 orchestrator. - Persisting manual reclassify overrides into the session: Spec 13 orchestrator wires
onDomainOverridetouseFactorySession.update. - Visual smoke against a live API: Spec 13 manual verification.