docs/capture-pipeline.md
Capture Pipeline Technical Documentation
1. Overview
The capture pipeline is the core data ingestion path in Birdbrain. It captures web pages from a Chrome extension, transmits them over HTTP to a local Hono server running inside the Electron main process, stores the content on disk, records metadata in SQLite, runs post-capture analysis, and pushes real-time events to the renderer for observability.
The pipeline handles three capture sources (auto, manual, selector) through a single unified endpoint. All sources share the same storage, hashing, database insertion, and post-capture extraction logic, differing only in their validation rules and how they are triggered.
Key Components
| Component | Location | Role |
|---|---|---|
| Chrome extension background | extension/src/background.ts | Capture orchestration, dedup, connection management |
| Content script | extension/src/content.ts | Page extraction (freeze-dry), selector matching, highlighting |
| Extension HTTP client | extension/src/utils/api.ts | Typed API client for the Hono server |
| Toast system | extension/src/toast.ts | In-page user feedback via Shadow DOM toasts |
| Capture server | src/main/services/captureServer.ts | Hono HTTP server with unified capture endpoint |
| File storage | src/main/services/storage.ts | Writes HTML, PNG, and TXT files to disk |
| Hash service | src/main/services/hash.ts | SHA-256 content hashing and verification |
| IPC channels | src/shared/ipc.ts | Typed channel definitions for Electron IPC |
| Preload bridge | src/preload/index.ts | Exposes IPC to renderer via contextBridge |
| Zustand store | src/renderer/stores/appStore.ts | Capture events state (ring buffer, stats) |
| Server status hook | src/renderer/hooks/useServerStatus.ts | Subscribes to IPC events, updates store |
| Health dashboard | src/renderer/components/status/CaptureHealth.tsx | Pipeline observability UI |
| Shared types | src/shared/types.ts | CaptureEvent, CaptureSource, Capture, etc. |
2. Architecture Diagram
Full Capture Flow
| 1 | Chrome Extension Electron Main Process Renderer |
| 2 | ================ ===================== ======== |
| 3 | |
| 4 | background.ts captureServer.ts (Hono) appStore.ts |
| 5 | +-----------+ +------------------+ +-----------+ |
| 6 | | | POST /api/captures | | IPC: | | |
| 7 | | captureTab|--------------------->| Unified Endpoint | NEW_CAPTURE | Zustand | |
| 8 | | manual | | |---------------->| Store | |
| 9 | | selector | | 1. Validate | | | |
| 10 | | | | 2. Blacklist | IPC: | capture | |
| 11 | | | | 3. Source rules | CAPTURE_ | Events[] | |
| 12 | | | JSON response | 4. Hash | ACTIVITY | capture | |
| 13 | | |<---------------------| 5. Save files |---------------->| Stats | |
| 14 | | | | 6. DB insert | | | |
| 15 | +-----------+ | 7. IPC notify | +-----------+ |
| 16 | ^ | 8. Post-capture | | |
| 17 | | +------------------+ v |
| 18 | content.ts | CaptureHealth.tsx |
| 19 | +-----------+ | +-----------+ |
| 20 | | freeze-dry| storage.ts database.ts | Activity | |
| 21 | | text | +--------+ +----------+ | list | |
| 22 | | selectors | | HTML | | captures | | Stats | |
| 23 | +-----------+ | PNG | | entities | | Test btns | |
| 24 | | TXT | +----------+ +-----------+ |
| 25 | +--------+ |
Event Flow for Observability
| 1 | captureServer.ts preload/index.ts appStore.ts CaptureHealth.tsx |
| 2 | | | | | |
| 3 | | emitCaptureEvent() | | | |
| 4 | |---IPC:CAPTURE_ACTIVITY---------->| | | |
| 5 | | |--onCaptureActivity()----->| | |
| 6 | | | |--addCaptureEvent() | |
| 7 | | | | (ring buffer, 50) | |
| 8 | | | | | |
| 9 | | | |---re-render---------->| |
| 10 | | | | | |
3. Capture Sources
Auto Capture
Auto capture is session-based. It fires automatically when a page finishes loading while a capture session is active.
Trigger: chrome.tabs.onUpdated listener in extension/src/background.ts (line 363). When changeInfo.status === 'complete', the shouldCapture() function checks:
- Session is active and extension is connected
- URL is not in the default ignore list (
DEFAULT_IGNORE) - URL is not matched by user-configured ignored patterns
- URL has not been captured in the last 60 seconds (client-side dedup via
dedupeMap)
Behavior:
- Case ID is inferred from
state.activeCaseIdon the server -- the extension does not send acaseId - The server increments
state.captureCountfor auto captures only - The extension updates its badge count after each successful auto capture
- No toast feedback is shown for auto captures (they happen silently in the background)
Manual Capture
Manual capture is session-independent. It can be triggered at any time the extension is connected to the server, regardless of whether a session is active.
Triggers:
- Context menu: "Capture with Birdbrain" right-click menu item (
extension/src/background.ts, line 115) - Popup:
MANUAL_CAPTUREmessage from the extension popup (extension/src/background.ts, line 421)
Behavior:
- Requires an explicit
caseIdin the request body - The server validates the case exists and is not archived
- Shows a toast on the page: "Capturing..." while in progress, then updates to success/degraded/error
- Extraction (freeze-dry) and screenshot run in parallel via
Promise.all(extension/src/background.ts, line 221) - Reports the
degradedflag to distinguish freeze-dry success from fallback
Selector Capture
Selector capture is triggered when page text matches user-defined patterns (selectors) associated with cases. It operates in two modes controlled by autoCaptureMode:
Trigger: checkSelectorsOnTab() in extension/src/background.ts (line 310), called on page load completion when the session is active and selectors exist.
Modes:
auto: Automatically captures the page for each case with matching selectorsnotify: Shows a Chrome notification listing the matches; the user can then manually capture
Behavior:
- Requires
caseIdandmatchedSelectorsin the request body - Has its own dedup map (
selectorDedupeMap) keyed bycaseId:url - Matches are grouped by case, so a single page can trigger captures for multiple cases
- The content script highlights matching text on the page and shows a sidebar with match details
Source Comparison
| Aspect | Auto | Manual | Selector |
|---|---|---|---|
| Session required | Yes | No | Session must be active (for selector fetching) |
| Case ID | Inferred from active case | Explicit in request | Explicit in request |
| Dedup | 60s client-side per URL | None | 60s client-side per caseId:URL |
| Toast feedback | None | Yes (full lifecycle) | None |
| Screenshot | Yes | Yes (parallel) | Yes |
| Archived case check | No (active case assumed valid) | Yes | Yes |
4. The Unified Capture Endpoint
Endpoint: POST /api/captures
Defined in src/main/services/captureServer.ts, line 254. All three capture sources hit this single endpoint, discriminated by the source field.
Request Body
| 1 | { |
| 2 | source: 'auto' | 'manual' | 'selector' |
| 3 | caseId?: string // required for manual/selector |
| 4 | url: string // required |
| 5 | title: string |
| 6 | html: string // required |
| 7 | screenshot?: string // base64-encoded PNG |
| 8 | timestamp: string |
| 9 | textContent?: string |
| 10 | headers?: Record<string, string> |
| 11 | matchedSelectors?: SelectorMatchInfo[] // required for selector |
| 12 | } |
Validation Pipeline
The endpoint processes requests in a strict order:
Step 1 -- Common validation (line 265):
| 1 | if (!url || !html) → 400 "Missing required fields: url, html" |
Step 2 -- URL blacklist (line 270):
The server checks the URL against settings.ignoredUrlPatterns using isUrlBlacklisted(). This is a defense-in-depth check -- the extension also filters URLs client-side, but the server enforces it independently.
| 1 | if blacklisted → 403 "URL blocked by ignored pattern" |
| 2 | + emits CaptureEvent { type: 'skipped', skipReason: "Blacklisted: {pattern}" } |
Step 3 -- Source-specific validation (line 278):
| Source | Validation |
|---|---|
auto | state.sessionActive must be true (400), state.activeCaseId must exist (400) |
manual | body.caseId required (400), case must exist (404), case must not be archived (400) |
selector | body.caseId required (400), matchedSelectors required (400), case must exist (404), case must not be archived (400) |
| Unknown | 400 "Invalid source: {source}" |
Step 4 -- Shared pipeline (line 319 onward):
- Emit
receivedevent - Compute SHA-256 hash of the HTML content
- Decode base64 screenshot to Buffer (if present)
- Generate a UUID for the capture (
crypto.randomUUID()) - Save files to disk via
saveCapture() - Insert capture record into SQLite via
db.insertCapture() - Increment
state.captureCount(auto source only) - Schedule post-capture work via
setImmediate() - Send
NEW_CAPTUREIPC event to the renderer - Emit
storedevent with duration measurement - Return
{ captureId, hash, status: 'ok', source }
Step 5 -- Error handling (line 355):
On any exception in the shared pipeline:
- Logs the error to console
- Emits a
failedCaptureEvent - Returns 500 "Failed to process capture"
Response
| 1 | { captureId: string, hash: string, status: 'ok', source: string } |
5. Page Extraction
Page extraction runs in the content script (extension/src/content.ts) and is triggered by the EXTRACT_PAGE message from the background script.
freeze-dry
The freeze-dry library serializes the live DOM into a self-contained HTML string. It inlines CSS, images, fonts, and other resources as data URIs, producing a snapshot that can be rendered offline without any external dependencies.
Configuration (line 265):
| 1 | freezeDry(document, { |
| 2 | timeout: 10000, // 10-second timeout to prevent hangs on complex pages |
| 3 | addMetadata: true // adds freeze-dry metadata to the output |
| 4 | }) |
Degraded Fallback
If freeze-dry fails (throws an error), the content script falls back to raw document.documentElement.outerHTML and sets degraded: true in the response (line 272-278). This ensures a capture always succeeds, even if it lacks inlined resources.
The degraded flag propagates to the background script, which uses it to show an amber "Captured (basic snapshot)" toast instead of the green "Page captured" toast for manual captures.
Text Content Extraction
Alongside the HTML, the content script extracts document.body.innerText as plain text (line 263). This text content is:
- Saved as a
.txtfile alongside the HTML - Used for entity extraction (regex pattern matching)
- Used for selector matching (pattern detection)
Screenshot Capture
Screenshots are captured via the Chrome extension API in the background script:
| 1 | chrome.tabs.captureVisibleTab({ format: 'png' }) |
The result is a data URI that gets stripped of its data:image/png;base64, prefix before being sent to the server. Screenshot capture can fail silently (e.g., on restricted pages like chrome:// URLs), in which case the capture proceeds without a screenshot.
Parallel Extraction (Manual Capture)
For manual captures, freeze-dry and screenshot run in parallel via Promise.all (extension/src/background.ts, line 221):
| 1 | const [pageData, rawScreenshot] = await Promise.all([ |
| 2 | chrome.tabs.sendMessage(tabId, { type: 'EXTRACT_PAGE' }), |
| 3 | chrome.tabs.captureVisibleTab({ format: 'png' }).catch(() => undefined) |
| 4 | ]) |
This minimizes total capture latency. For auto captures, the extraction and screenshot are sequential (lines 178-189).
6. Storage & Integrity
File Organization
All capture files are stored under a configurable storage root directory, organized by case ID:
| 1 | {storageRoot}/ |
| 2 | {caseId}/ |
| 3 | {captureId}.html -- freeze-dried (or raw fallback) HTML |
| 4 | {captureId}.png -- viewport screenshot (if available) |
| 5 | {captureId}.txt -- plain text content (if available) |
The saveCapture() function in src/main/services/storage.ts (line 26) handles directory creation and file writes:
- Creates the case directory if it does not exist (
mkdirSyncwithrecursive: true) - Writes HTML as UTF-8 text
- Writes screenshot as raw binary (from a Node.js
Buffer) - Writes text content as UTF-8 text
- Returns relative paths (e.g.,
{caseId}/{captureId}.html) for storage in the database
SHA-256 Hash Computation
Every capture's HTML content is hashed using SHA-256 (src/main/services/hash.ts):
| 1 | export function hashContent(content: string): string { |
| 2 | return createHash('sha256').update(content, 'utf-8').digest('hex') |
| 3 | } |
The hash is:
- Computed at capture time from the HTML string
- Stored in the
capturestablehashcolumn - Returned to the extension in the capture response
- Used by the test pipeline to verify round-trip integrity
Hash Verification
The verifyHash() function allows checking whether stored content matches its expected hash:
| 1 | export function verifyHash(content: string, expectedHash: string): boolean { |
| 2 | return hashContent(content) === expectedHash |
| 3 | } |
This supports the integrity verification feature used in evidence export and the HashVerification type in src/shared/types.ts (line 124), which reports verified, tampered, or missing status per capture.
7. Observability
CaptureEvent Types
Defined in src/shared/types.ts (line 176):
| 1 | export interface CaptureEvent { |
| 2 | type: 'received' | 'stored' | 'failed' | 'skipped' | 'extraction_done' |
| 3 | captureId?: string |
| 4 | source: CaptureSource |
| 5 | url: string |
| 6 | timestamp: string |
| 7 | error?: string // only for 'failed' |
| 8 | skipReason?: string // only for 'skipped' |
| 9 | durationMs?: number // only for 'stored' |
| 10 | } |
| Event Type | Meaning | Emitted When |
|---|---|---|
received | Capture request accepted for processing | After source-specific validation passes |
stored | Files written and DB record created | After saveCapture() + insertCapture() succeed |
failed | Capture processing failed | On any exception in the shared pipeline |
skipped | Capture rejected before processing | URL matches a blacklist pattern |
extraction_done | Post-capture analysis complete | After entity extraction and selector matching finish |
Event Flow
| 1 | captureServer.ts Electron IPC preload/index.ts useServerStatus.ts appStore.ts |
| 2 | | | | | | |
| 3 | |--emitCaptureEvent()--->| | | | |
| 4 | | mainWindow.webContents| | | | |
| 5 | | .send(CAPTURE_ACTIVITY| | | | |
| 6 | | , event) | | | | |
| 7 | | |--ipcRenderer.on()---->| | | |
| 8 | | | |--onCaptureActivity-->| | |
| 9 | | | | (callback) |--addCaptureEvent-->| |
| 10 | | | | | | |
The emitCaptureEvent() function (src/main/services/captureServer.ts, line 139) sends events via the CAPTURE_ACTIVITY IPC channel. The preload bridge (src/preload/index.ts, line 132) exposes onCaptureActivity() which the useServerStatus hook subscribes to. The hook calls addCaptureEvent() on the Zustand store.
Health Dashboard
The CaptureHealth component (src/renderer/components/status/CaptureHealth.tsx) is mounted in the TopBar (src/renderer/components/layout/TopBar.tsx) and provides:
Trigger button: A pill-shaped button showing success count and failure count. Color-coded:
- Emerald: captures succeeding, no failures
- Red: failures detected
- Slate: no activity yet
Popover panel (toggled on click):
- Status indicators: Extension connection state, session recording state
- Aggregate stats:
{N}ok / {N}fail / {N}skip - Last error: Most recent failure message and timestamp (if any)
- Activity list: Scrollable list of last 50 events, each showing:
- Status icon (green check, red X, amber warning, blue spinner, indigo zap)
- Truncated URL (40 chars)
- Source type, timestamp, duration (for
storedevents) - Error message (for
failedevents) - Skip reason (for
skippedevents)
- Test buttons: "Test Pipeline" and "Test HTTP" (see Section 12)
Extension Toast Feedback
The toast system (extension/src/toast.ts) renders feedback directly on the captured page using Shadow DOM isolation (to avoid style conflicts with page CSS).
Toast lifecycle for manual captures:
- Background sends
SHOW_CAPTURE_TOAST-- content script shows spinner + "Capturing page..." - After capture completes, background sends
UPDATE_CAPTURE_TOASTwith the outcome:
| Status | Icon | Message | Auto-dismiss |
|---|---|---|---|
capturing | Spinner | "Capturing page..." | No (stays until update) |
success | Checkmark | "Page captured" | 3 seconds |
degraded | Warning | "Captured (basic snapshot)" | 5 seconds |
error | X mark | "Capture failed" (or custom message) | 5 seconds |
skipped | Info | "Already captured" | 2 seconds |
Error Categorization
When a manual capture fails, the background script categorizes the error by HTTP status (extension/src/background.ts, line 252):
| HTTP Status | User-Facing Message |
|---|---|
| 400 | "Capture rejected: {server detail}" |
| 403 | "URL is blacklisted" |
| 404 | "Case not found" |
| 500 | "Server error -- check Birdbrain app" |
TypeError (network) | "Can't reach Birdbrain -- is it running?" |
The ApiError class in extension/src/utils/api.ts (line 3) carries the HTTP status and parsed error detail from the server response body.
8. Post-Capture Processing
After a capture is stored, the server schedules asynchronous work via schedulePostCaptureWork() (src/main/services/captureServer.ts, line 115):
| 1 | function schedulePostCaptureWork( |
| 2 | captureId: string, |
| 3 | caseId: string, |
| 4 | source: CaptureSource, |
| 5 | url: string, |
| 6 | textContent: string | undefined |
| 7 | ): void { |
| 8 | setImmediate(() => { |
| 9 | // 1. Rule-based entity extraction |
| 10 | // 2. Selector matching |
| 11 | // 3. Emit extraction_done event |
| 12 | }) |
| 13 | } |
Rule-Based Entity Extraction
runRuleBasedExtraction() (line 75):
- Skips if
textContentis empty/whitespace - Loads enabled entity types from settings (cached after first load in
cachedEnabledEntityTypes) - Calls
extractEntitiesRuleBased(textContent, enabledTypes)-- regex + NLP extraction - Batch-inserts discovered entities into the
entitiestable viadb.insertEntitiesBatch() - Sends an
EXTRACTION_COMPLETEIPC event to the renderer with the entity count - Errors are caught and logged, never propagated -- extraction failure does not fail the capture
Selector Matching
After entity extraction, the server runs db.matchSelectorsForCapture(captureId, caseId, textContent) (line 124). This checks the capture's text content against all active selectors for the case and records matches in the database.
Scheduling via setImmediate
Post-capture work uses setImmediate() rather than await for two reasons:
- The HTTP response returns immediately after storage -- the client does not wait for extraction
- Extraction errors cannot cause the capture to appear failed
Each task (extraction, selector matching) is wrapped in its own try/catch so that a failure in one does not prevent the other from running. After both complete, an extraction_done event is emitted.
9. Connection Management
Extension to Server Connection
The Chrome extension maintains a connection to the Hono server through status polling.
Polling interval (extension/src/background.ts, line 97):
| 1 | setInterval(() => { checkStatus() }, connected ? 30_000 : 5_000) |
- When disconnected: polls every 5 seconds (aggressive reconnect)
- When connected: polls every 30 seconds (keepalive)
- Initial check runs immediately on extension load
Status endpoint: GET /api/status returns server state including running, activeCase, sessionActive, captureCount, autoCaptureMode, cases, and ignoredUrlPatterns.
Server-Side Connection Tracking
The server tracks when the extension was last seen via state.extensionLastSeen (updated on every /api/status hit). A 5-second interval check (startExtensionConnectionCheck(), line 478) detects disconnection:
| 1 | if (Date.now() - extensionLastSeen > 10000) → extension disconnected |
When connection state changes, the server sends EXTENSION_CONNECTION IPC events to the renderer.
Badge States
The extension icon and badge reflect connection state (extension/src/background.ts, line 381):
| State | Icon | Badge Text | Badge Color |
|---|---|---|---|
disconnected | Default | ! | Gray (#6b7280) |
connected | Default | (empty) | -- |
active (session recording) | Active variant | Capture count | Red (#ef4444) |
| Selector match | -- | Match count | Blue (#3b82f6) |
10. Design Decisions
Why a Unified Endpoint Over Separate Endpoints
The previous design had three nearly-identical endpoints (/api/captures, /api/captures/manual, /api/captures/selector) duplicating ~80% of their logic. The unified endpoint with a source discriminator:
- Eliminates code duplication for the shared pipeline (hash, storage, DB, events)
- Makes it impossible for source-specific endpoints to drift out of sync
- Simplifies the extension API from 4 capture functions to 1
- Makes the validation rules per source explicit in a single decision tree
Why Single-Phase Capture Over Two-Phase
The previous two-phase manual capture showed "Captured!" immediately (Phase 1: raw HTML), then ran freeze-dry in the background (Phase 2). Phase 2 failures were silent -- the user saw success but got a degraded capture.
The single-phase design:
- Waits for freeze-dry (with 10s timeout) before responding
- Reports actual outcome: success, degraded, or failure
- Eliminates the
PATCH /api/captures/:id/htmlendpoint andEXTRACT_PAGE_FASTcontent script handler - Trade-off: manual capture latency increases from ~100ms to ~500ms-2s, but feedback is honest
Why Client-Side Dedup Instead of Server-Side
Auto-capture deduplication (60-second window per URL) runs in the extension's background.ts via dedupeMap. The server does not perform dedup because:
- The extension already knows recent URLs it has captured -- no HTTP round-trip needed for dedup checks
- Server-side dedup would require maintaining URL state or querying the database on every request
- The
skippedevent is never emitted from the server for dedup -- the extension simply does not send the request - Selector captures have their own dedup keyed by
caseId:url(different URLs can match the same selectors for different cases)
Why Fire-and-Forget for Entity Extraction
Post-capture entity extraction runs via setImmediate() and its results are not included in the HTTP response because:
- Extraction can take variable time depending on text length and enabled entity types
- A failed extraction should not invalidate a successful capture
- The renderer is notified asynchronously via
EXTRACTION_COMPLETEIPC when extraction finishes - The
extraction_doneCaptureEvent provides observability into extraction completion
Why Ring Buffer (50 Events) for the Activity List
The Zustand store caps capture events at 50 (appStore.ts, line 106):
| 1 | const events = [event, ...s.captureEvents].slice(0, 50) |
- Prevents unbounded memory growth during long recording sessions
- 50 events provides sufficient recent history for debugging
- Events are prepended (newest first) for display in the health dashboard
- Aggregate stats (
successCount,failCount,skipCount) accumulate independently and are not lost when events rotate out
11. URL Filtering
Blacklist Pattern Types
The isUrlBlacklisted() function in src/main/services/captureServer.ts (line 56) and the equivalent isIgnoredByUser() in extension/src/background.ts (line 139) support three pattern formats:
- Regex: Patterns wrapped in
/delimiters (e.g.,/^https:\/\/mail\.google\.com/i). Parsed withnew RegExp(). - Glob: Patterns containing
*or?wildcards (e.g.,*.google.com/mail/*). Converted to regex viaglobToRegex(). - Literal substring: Plain strings (e.g.,
facebook.com). Matched viaurl.includes(pattern).
Default Ignore Patterns (Extension)
The extension has hardcoded patterns in DEFAULT_IGNORE (extension/src/background.ts, line 9):
| 1 | const DEFAULT_IGNORE = [ |
| 2 | /^chrome:\/\//, |
| 3 | /^chrome-extension:\/\//, |
| 4 | /^about:/, |
| 5 | /^devtools:/, |
| 6 | /^data:/, |
| 7 | /^file:/, |
| 8 | /localhost:19845/ // the capture server itself |
| 9 | ] |
These are applied client-side before any request is sent. They filter out browser internal pages and prevent recursive captures of the Birdbrain server.
User-Configured Patterns
Users can add custom ignored URL patterns via settings (BirdbrainSettings.ignoredUrlPatterns). These are:
- Fetched by the extension on every status poll and stored in
userIgnoredPatterns - Applied client-side in the extension via
isIgnoredByUser()before sending captures - Applied server-side in
isUrlBlacklisted()as a defense-in-depth check -- if the extension is out of date or a direct HTTP request bypasses the extension, the server still rejects blacklisted URLs
Dual-Layer Filtering
URL filtering happens at two layers:
| 1 | Extension (client-side) Server (defense-in-depth) |
| 2 | ======================== ========================= |
| 3 | DEFAULT_IGNORE (regex) settings.ignoredUrlPatterns |
| 4 | + userIgnoredPatterns via isUrlBlacklisted() |
| 5 | via shouldCapture() / isIgnoredByUser() → 403 with pattern name |
| 6 | → request never sent + CaptureEvent { type: 'skipped' } |
12. Testing
Test Pipeline Endpoint
GET /api/captures/test (src/main/services/captureServer.ts, line 369)
This endpoint exercises the full capture pipeline without requiring the Chrome extension:
- Finds the first available case in the database (fails if no cases exist)
- Creates test HTML:
<html><body>Birdbrain pipeline test {timestamp}</body></html> - Computes SHA-256 hash of the test HTML
- Emits a
receivedCaptureEvent - Saves the test capture to disk via
saveCapture() - Inserts it into the database via
db.insertCapture() - Reads it back and verifies the hash matches
- Emits a
storedCaptureEvent - Cleans up: deletes the test capture from the database in a
finallyblock
Returns: { success: boolean, durationMs: number, error?: string }
HTTP Test
The HTTP test (captures:testHttp IPC channel, src/main/ipcHandlers.ts, line 94) verifies the Hono server is reachable by making a fetch() call to GET /api/status from the main process. This tests the HTTP listener independently of the extension.
Health Dashboard Test Buttons
Both tests are accessible from the CaptureHealth popover:
- "Test Pipeline": Invokes
window.birdbrain.testPipeline()-> IPCcaptures:testPipeline->GET /api/captures/teston the local server - "Test HTTP": Invokes
window.birdbrain.testHttp()-> IPCcaptures:testHttp->GET /api/statuson the local server
Results are displayed inline: "Pipeline OK -- verified in {N}ms" (green) or "Pipeline FAILED: {error}" (red).
Unit Test Coverage
The capture server endpoint is covered by tests in tests/main/services/captureServer.test.ts. Key test scenarios include:
- Unified endpoint validation per source type
- URL blacklist rejection
- Event emission at each pipeline stage
- Test endpoint round-trip verification