feat: dashboard landing, auto-ingestion, stale article fixes#9
feat: dashboard landing, auto-ingestion, stale article fixes#9jeremylongshore merged 3 commits intomainfrom
Conversation
…tale articles Post-login now lands on /dashboard instead of the feed. Dashboard auto-triggers ingestion once per browser session (sessionStorage guard) so the feed stays fresh without manual clicks. IngestionButton picks up auto-triggered runs to show progress. Articles query now filters to last 48 hours, and the backend is_within_time_window() returns False on date parse failure instead of True (defense-in-depth). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review Summary by QodoDashboard landing, auto-ingestion, and stale article filtering
WalkthroughsDescription• Redirect login to /dashboard instead of articles feed • Auto-trigger ingestion on dashboard mount once per session • Filter articles feed to last 48 hours only • Listen for auto-ingestion events in IngestionButton component • Fix backend date parsing to reject unparseable dates safely Diagramflowchart LR
Login["Login Page"] -- "navigate to /dashboard" --> Dashboard["Dashboard Page"]
Dashboard -- "useAutoIngestion hook" --> AutoIngest["Auto-trigger Ingestion"]
AutoIngest -- "POST /trigger/ingestion" --> Backend["Backend Service"]
AutoIngest -- "dispatch custom event" --> IngestionBtn["IngestionButton"]
IngestionBtn -- "listen & poll" --> Backend
Articles["Articles Feed"] -- "48h time filter" --> Firestore["Firestore Query"]
Backend -- "is_within_time_window" --> DateCheck["Date Validation"]
File Changes1. dashboard/src/pages/Login.tsx
|
Summary of ChangesHello @jeremylongshore, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the user experience by streamlining navigation, automating content updates, and improving data integrity. It ensures users land on a more functional dashboard, receive up-to-date information without manual refreshes, and see a more relevant article feed, while also hardening the backend against malformed date data. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a once-per-session auto-ingestion hook that POSTs to MCP and emits an Changes
Sequence DiagramsequenceDiagram
rect rgba(200,200,255,0.5)
participant Dashboard as Dashboard Component
participant Hook as useAutoIngestion Hook
participant Storage as sessionStorage
participant MCP as MCP Endpoint
participant Window as window (event)
participant Button as IngestionButton
end
Dashboard->>Hook: mount -> call useAutoIngestion()
Hook->>Storage: read auto-ingest flag
alt flag absent
Hook->>MCP: POST /trigger/ingestion (time_window_hours=24, max_items=50)
MCP-->>Hook: 202 Accepted + { run_id }
Hook->>Hook: parse run_id, show toast
Hook->>Window: dispatch 'auto-ingestion-started' { run_id }
Hook->>Storage: set auto-ingest flag
else flag present
Hook-->>Hook: skip trigger
end
Note over Button: listens for 'auto-ingestion-started'
Window->>Button: event { run_id }
Button->>Button: if phase == idle -> set starting, record start time
Button->>Button: call pollStatus(run_id)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
The pull request introduces several key improvements to the dashboard experience, including auto-ingestion on mount and a 48-hour filter for the articles feed. The backend fix for date parsing in the time window check is also a good robustness improvement. I have identified a few areas for improvement regarding configuration management and a potential runtime issue with Firestore queries that require composite indexes.
| import { useEffect } from 'react' | ||
| import { toast } from 'sonner' | ||
|
|
||
| const MCP_URL = 'https://perception-mcp-w53xszfqnq-uc.a.run.app' |
There was a problem hiding this comment.
The MCP_URL is hardcoded to a specific production endpoint and is duplicated in IngestionButton.tsx. It is better to use an environment variable to allow for different environments (local, staging, production) and to centralize the configuration.
| const MCP_URL = 'https://perception-mcp-w53xszfqnq-uc.a.run.app' | |
| const MCP_URL = import.meta.env.VITE_MCP_URL || 'https://perception-mcp-w53xszfqnq-uc.a.run.app' |
| q = query( | ||
| articlesRef, | ||
| where('category', '==', selectedCategory), | ||
| where('published_at', '>=', cutoff), | ||
| orderBy('published_at', 'desc'), | ||
| limit(100) |
There was a problem hiding this comment.
This query combines an equality filter (category) with a range filter and ordering on another field (published_at). In Firestore, this specific combination requires a composite index. If this index hasn't been created yet in the Firebase Console, this query will fail at runtime with a failed-precondition error. Please ensure the index for category: ASC, published_at: DESC is deployed.
|
| Filename | Overview |
|---|---|
| perception_app/mcp_service/routers/rss.py | Changed is_within_time_window to return False on parse failure instead of True, but this breaks existing test expectations |
| dashboard/src/hooks/useAutoIngestion.ts | New hook to auto-trigger ingestion on dashboard mount with sessionStorage deduplication, fire-and-forget pattern |
| dashboard/src/pages/Articles.tsx | Added 48-hour filter to article queries with where('published_at', '>=', cutoff), may need composite Firestore index |
Sequence Diagram
sequenceDiagram
participant User
participant Login
participant Dashboard
participant useAutoIngestion
participant MCP as MCP Service
participant IngestionButton
participant Articles
participant Firestore
User->>Login: Enter credentials
Login->>Login: Authenticate
Login->>Dashboard: navigate('/dashboard')
Dashboard->>useAutoIngestion: Mount (call hook)
useAutoIngestion->>useAutoIngestion: Check sessionStorage
alt First visit this session
useAutoIngestion->>useAutoIngestion: Set sessionStorage flag
useAutoIngestion->>MCP: POST /trigger/ingestion
MCP-->>useAutoIngestion: 202 {run_id}
useAutoIngestion->>User: toast.info('Refreshing feed...')
useAutoIngestion->>IngestionButton: dispatch 'auto-ingestion-started' event
IngestionButton->>IngestionButton: setPhase('starting')
IngestionButton->>MCP: Poll GET /trigger/ingestion/{run_id}
MCP-->>IngestionButton: Status updates
IngestionButton->>User: Show progress
else Already fired
useAutoIngestion->>useAutoIngestion: Skip (no-op)
end
User->>Articles: Navigate to feed
Articles->>Firestore: Query articles (48h filter)
Note over Articles,Firestore: where('published_at', '>=', cutoff)<br/>orderBy('published_at', 'desc')
Firestore-->>Articles: Return filtered articles
Articles->>User: Display articles
Last reviewed commit: dadde4d
| } else { | ||
| q = query( | ||
| articlesRef, | ||
| where('category', '==', selectedCategory), | ||
| where('published_at', '>=', cutoff), | ||
| orderBy('published_at', 'desc'), | ||
| limit(100) | ||
| ) |
There was a problem hiding this comment.
Verify that this composite query works without additional Firestore indexes. The query combines category ==, published_at >=, and orderBy published_at desc. While the existing index (category ASC, published_at DESC) should handle this, test in production to confirm no index errors occur.
Additional Comments (1)
|
Code Review by Qodo
1. useAutoIngestion() swallows fetch errors
|
| fetch(`${MCP_URL}/trigger/ingestion`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| trigger: 'auto', | ||
| time_window_hours: 24, | ||
| max_items_per_source: 50, | ||
| }), | ||
| }) | ||
| .then(async (res) => { | ||
| if (res.status === 202) { | ||
| const data = await res.json() | ||
| toast.info('Refreshing your feed...', { | ||
| description: `Run ID: ${data.run_id}`, | ||
| }) | ||
| window.dispatchEvent( | ||
| new CustomEvent('auto-ingestion-started', { | ||
| detail: { run_id: data.run_id }, | ||
| }) | ||
| ) | ||
| } | ||
| // 409 = already running, silently ignore | ||
| }) | ||
| .catch(() => { | ||
| // Fire-and-forget — don't block dashboard load | ||
| }) |
There was a problem hiding this comment.
1. useautoingestion() swallows fetch errors 📘 Rule violation ⛯ Reliability
The new auto-ingestion request silently ignores network/JSON/server failures and unexpected HTTP statuses, making production issues hard to detect and debug. This can leave users with a stale dashboard with no actionable feedback or telemetry.
Agent Prompt
## Issue description
`useAutoIngestion()` currently ignores non-`202` HTTP responses and swallows all fetch/parse errors in an empty `.catch()`, creating silent failures.
## Issue Context
Auto-ingestion is a critical freshness mechanism for the dashboard. When it fails, we need at least (a) safe user-facing feedback (generic), and (b) internal diagnostics (logging/telemetry) with enough context (status code, request path, correlation/run id if available).
## Fix Focus Areas
- dashboard/src/hooks/useAutoIngestion.ts[13-38]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Listen for auto-ingestion started by useAutoIngestion hook | ||
| useEffect(() => { | ||
| const handler = (e: Event) => { | ||
| const runId = (e as CustomEvent<{ run_id: string }>).detail.run_id | ||
| if (phase === 'idle' && runId) { | ||
| setPhase('starting') | ||
| startTimeRef.current = Date.now() | ||
| pollStatus(runId) | ||
| } | ||
| } | ||
| window.addEventListener('auto-ingestion-started', handler) | ||
| return () => window.removeEventListener('auto-ingestion-started', handler) | ||
| }, [phase, pollStatus]) |
There was a problem hiding this comment.
2. Missed auto-ingestion event 🐞 Bug ⛯ Reliability
Auto-ingestion progress can be missed because a one-shot CustomEvent is dispatched from one component effect while the listener is registered in another component effect, with no guaranteed ordering. This can leave the IngestionButton idle even though an ingestion run is active (more likely under React StrictMode dev double-mount).
Agent Prompt
### Issue description
Auto-ingestion communicates the run id via a one-shot window event. Because listener registration happens in a different component effect, the event can be dispatched before the listener exists, so the UI never starts polling.
### Issue Context
- `useAutoIngestion()` dispatches `auto-ingestion-started` after receiving 202.
- `IngestionButton` only polls if it receives that event.
- There is no fallback to discover an active run if the event is missed.
### Fix Focus Areas
- dashboard/src/hooks/useAutoIngestion.ts[7-38]
- dashboard/src/components/IngestionButton.tsx[148-160]
- dashboard/src/pages/Dashboard.tsx[35-56]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (sessionStorage.getItem(SESSION_KEY)) return | ||
|
|
||
| sessionStorage.setItem(SESSION_KEY, '1') | ||
|
|
||
| fetch(`${MCP_URL}/trigger/ingestion`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| trigger: 'auto', | ||
| time_window_hours: 24, | ||
| max_items_per_source: 50, | ||
| }), | ||
| }) | ||
| .then(async (res) => { | ||
| if (res.status === 202) { | ||
| const data = await res.json() | ||
| toast.info('Refreshing your feed...', { | ||
| description: `Run ID: ${data.run_id}`, | ||
| }) | ||
| window.dispatchEvent( | ||
| new CustomEvent('auto-ingestion-started', { | ||
| detail: { run_id: data.run_id }, | ||
| }) | ||
| ) | ||
| } | ||
| // 409 = already running, silently ignore | ||
| }) | ||
| .catch(() => { | ||
| // Fire-and-forget — don't block dashboard load | ||
| }) |
There was a problem hiding this comment.
3. Session guard blocks retry 🐞 Bug ⛯ Reliability
The auto-ingestion sessionStorage guard is set before the POST succeeds and errors are swallowed. If the request fails, auto-ingestion won’t retry for the rest of the browser session, leaving the feed stale.
Agent Prompt
### Issue description
Auto-ingestion sets the session guard before the network request completes. If the request fails, the guard remains and prevents retries for the whole session.
### Issue Context
Current implementation intentionally swallows errors, but should not permanently disable auto-ingestion due to transient failures.
### Fix Focus Areas
- dashboard/src/hooks/useAutoIngestion.ts[8-38]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
dashboard/src/hooks/useAutoIngestion.ts (1)
4-4:MCP_URLis duplicated — extract to a shared constants module.
IngestionButton.tsx(line 5) defines the exact same constant. Any URL change would need to be made in two places.♻️ Suggested refactor
Create
dashboard/src/config/constants.ts:+export const MCP_URL = 'https://perception-mcp-w53xszfqnq-uc.a.run.app'Then in both
useAutoIngestion.tsandIngestionButton.tsx:-const MCP_URL = 'https://perception-mcp-w53xszfqnq-uc.a.run.app' +import { MCP_URL } from '../config/constants'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dashboard/src/hooks/useAutoIngestion.ts` at line 4, MCP_URL is duplicated between useAutoIngestion.ts and IngestionButton.tsx; extract it into a shared constant (e.g., export const MCP_URL) in a new module like dashboard/src/config/constants.ts, then replace the local MCP_URL definitions in useAutoIngestion.ts and IngestionButton.tsx with imports from that constants module and remove the duplicated declarations so both files reference the single exported MCP_URL symbol.perception_app/mcp_service/routers/rss.py (1)
131-133: Defensive logic is correct; align warning format with the file's structured logging convention.The logic change (
return False) is the right call. However, the newlogger.warningon line 132 uses a plain f-string, while every other log statement in this file useslogger.info/error(json.dumps({...}))for structured output. The pipeline lint/test run also surfaces this line as a warning. Keeping the format consistent makes logs easier to parse downstream.♻️ Suggested fix
- except Exception as e: - logger.warning(f"is_within_time_window: failed to parse '{published_at}': {e}") - return False # Exclude if we can't parse date + except Exception as e: + logger.warning(json.dumps({ + "severity": "WARNING", + "message": "is_within_time_window: failed to parse published_at", + "published_at": published_at, + "error": str(e), + })) + return False # Exclude if we can't parse date🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@perception_app/mcp_service/routers/rss.py` around lines 131 - 133, The warning in is_within_time_window uses a plain f-string; change it to the file's structured logging style by logging a JSON object (e.g., via logger.warning(json.dumps({...}))) that includes a descriptive message, the published_at value and the exception details (e) so it matches other logger.info/error calls in the module; keep the subsequent return False unchanged.dashboard/src/components/IngestionButton.tsx (1)
149-160:phasein the dependency array causes the listener to be torn down and re-registered on every phase transition.During an active ingestion run,
phasecycles through several values (starting→initializing→loading_sources→ … →idle). Includingphasein the deps means theauto-ingestion-startedlistener is removed and re-added on each transition. While thephase === 'idle'guard prevents double-triggering and the event only fires once per session, the repeated re-registrations are unnecessary. The standard fix is aphaseRefso the handler doesn't need to close overphase:♻️ Suggested refactor
+ const phaseRef = useRef(phase) + useEffect(() => { phaseRef.current = phase }, [phase]) // Listen for auto-ingestion started by useAutoIngestion hook useEffect(() => { const handler = (e: Event) => { const runId = (e as CustomEvent<{ run_id: string }>).detail.run_id - if (phase === 'idle' && runId) { + if (phaseRef.current === 'idle' && runId) { setPhase('starting') startTimeRef.current = Date.now() pollStatus(runId) } } window.addEventListener('auto-ingestion-started', handler) return () => window.removeEventListener('auto-ingestion-started', handler) - }, [phase, pollStatus]) + }, [pollStatus])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dashboard/src/components/IngestionButton.tsx` around lines 149 - 160, The effect registers an `auto-ingestion-started` handler that closes over `phase`, causing the listener to be removed/re-added on every phase change; instead, make `phase` a ref (e.g., `phaseRef`) that you update whenever `phase` changes and have the `handler` read `phaseRef.current` so the effect can omit `phase` from its dependency array; keep `pollStatus` stable (or include it if not memoized) and keep `startTimeRef`, `setPhase`, and `pollStatus` referenced by name so you update `startTimeRef.current`, call `setPhase('starting')`, and invoke `pollStatus(runId)` inside the handler using the refs/stable callbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dashboard/src/hooks/useAutoIngestion.ts`:
- Around line 22-27: The response from res.json() is untyped so data.run_id may
be undefined and toast could show "Run ID: undefined"; in useAutoIngestion,
define an interface (e.g., AutoIngestionResponse { run_id?: string }) or cast
the JSON to that type, then guard or normalize the value before passing to toast
(e.g., const runId = data?.run_id ?? 'unknown' or only include the Run ID
description when present) so the toast always receives a safe, meaningful string
and you avoid rendering "undefined".
---
Nitpick comments:
In `@dashboard/src/components/IngestionButton.tsx`:
- Around line 149-160: The effect registers an `auto-ingestion-started` handler
that closes over `phase`, causing the listener to be removed/re-added on every
phase change; instead, make `phase` a ref (e.g., `phaseRef`) that you update
whenever `phase` changes and have the `handler` read `phaseRef.current` so the
effect can omit `phase` from its dependency array; keep `pollStatus` stable (or
include it if not memoized) and keep `startTimeRef`, `setPhase`, and
`pollStatus` referenced by name so you update `startTimeRef.current`, call
`setPhase('starting')`, and invoke `pollStatus(runId)` inside the handler using
the refs/stable callbacks.
In `@dashboard/src/hooks/useAutoIngestion.ts`:
- Line 4: MCP_URL is duplicated between useAutoIngestion.ts and
IngestionButton.tsx; extract it into a shared constant (e.g., export const
MCP_URL) in a new module like dashboard/src/config/constants.ts, then replace
the local MCP_URL definitions in useAutoIngestion.ts and IngestionButton.tsx
with imports from that constants module and remove the duplicated declarations
so both files reference the single exported MCP_URL symbol.
In `@perception_app/mcp_service/routers/rss.py`:
- Around line 131-133: The warning in is_within_time_window uses a plain
f-string; change it to the file's structured logging style by logging a JSON
object (e.g., via logger.warning(json.dumps({...}))) that includes a descriptive
message, the published_at value and the exception details (e) so it matches
other logger.info/error calls in the module; keep the subsequent return False
unchanged.
| .then(async (res) => { | ||
| if (res.status === 202) { | ||
| const data = await res.json() | ||
| toast.info('Refreshing your feed...', { | ||
| description: `Run ID: ${data.run_id}`, | ||
| }) |
There was a problem hiding this comment.
data.run_id is untyped; toast can render "Run ID: undefined" if the response shape changes.
res.json() returns any, so data.run_id silently falls through as undefined if the API response doesn't include that field.
🛡️ Proposed fix
- if (res.status === 202) {
- const data = await res.json()
- toast.info('Refreshing your feed...', {
- description: `Run ID: ${data.run_id}`,
- })
- window.dispatchEvent(
- new CustomEvent('auto-ingestion-started', {
- detail: { run_id: data.run_id },
- })
- )
- }
+ if (res.status === 202) {
+ const data = await res.json() as { run_id?: string }
+ if (data.run_id) {
+ toast.info('Refreshing your feed...', {
+ description: `Run ID: ${data.run_id}`,
+ })
+ window.dispatchEvent(
+ new CustomEvent('auto-ingestion-started', {
+ detail: { run_id: data.run_id },
+ })
+ )
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@dashboard/src/hooks/useAutoIngestion.ts` around lines 22 - 27, The response
from res.json() is untyped so data.run_id may be undefined and toast could show
"Run ID: undefined"; in useAutoIngestion, define an interface (e.g.,
AutoIngestionResponse { run_id?: string }) or cast the JSON to that type, then
guard or normalize the value before passing to toast (e.g., const runId =
data?.run_id ?? 'unknown' or only include the Run ID description when present)
so the toast always receives a safe, meaningful string and you avoid rendering
"undefined".
Add dual entrypoint docs, codebase gotchas section, dashboard dev commands, functions directory, pytest markers, and fix agent count. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
is_within_time_window returns False for unparseable dates (excludes garbage articles). Test was incorrectly asserting True. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
CLAUDE.md (1)
193-202: Consider documenting VERTEX_SEARCH_DATA_STORE_ID.The environment variables section lists the core Vertex AI variables, but based on learnings,
VERTEX_SEARCH_DATA_STORE_IDmay also be needed for RAG search functionality. Verify if this should be documented here.Based on learnings, the environment variable
VERTEX_SEARCH_DATA_STORE_IDis needed to enable Vertex-managed sessions and RAG search.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLAUDE.md` around lines 193 - 202, Add documentation for VERTEX_SEARCH_DATA_STORE_ID under the "Environment Variables" section alongside VERTEX_PROJECT_ID, VERTEX_LOCATION, and VERTEX_AGENT_ENGINE_ID: state that VERTEX_SEARCH_DATA_STORE_ID is required for Vertex-managed sessions and RAG search functionality, provide the expected format/example value, and note any defaults or where to find/create the datastore in Vertex AI so readers can configure it correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@CLAUDE.md`:
- Around line 193-202: Add documentation for VERTEX_SEARCH_DATA_STORE_ID under
the "Environment Variables" section alongside VERTEX_PROJECT_ID,
VERTEX_LOCATION, and VERTEX_AGENT_ENGINE_ID: state that
VERTEX_SEARCH_DATA_STORE_ID is required for Vertex-managed sessions and RAG
search functionality, provide the expected format/example value, and note any
defaults or where to find/create the datastore in Vertex AI so readers can
configure it correctly.
Summary
/dashboardinstead of/(Articles feed) so users land on the command centerPOST /trigger/ingestiononce per browser session (sessionStorage guard), so the feed stays fresh without manual clicksauto-ingestion-startedcustom event, showing real-time progresswhere("published_at", ">=", cutoff)on both the "all" and per-category queriesis_within_time_window()returnsFalseon date parse failure instead ofTrue(defense-in-depth; the exception path is unreachable in practice sincenormalize_published_date()falls back todatetime.now())Files Changed
dashboard/src/pages/Login.tsxnavigate("/")→navigate("/dashboard")dashboard/src/hooks/useAutoIngestion.tsdashboard/src/pages/Dashboard.tsxuseAutoIngestion()hookdashboard/src/components/IngestionButton.tsxauto-ingestion-startedeventdashboard/src/pages/Articles.tsxpublished_atrange filter to Firestore queriesperception_app/mcp_service/routers/rss.pyis_within_time_window()to reject unparseable datesTest plan
npx tsc --noEmit— compiles cleannpx vite build— production build succeedspytest tests/mcp/ -v --no-cov— 39/39 passed/dashboard, ingestion auto-starts🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Documentation
Tests