# Fetch Web Content - Implementation Plan ## Phase 1: Foundation - Data Model & Interface **Goal:** Add database field and create pluggable interface for web content fetching. **Verification:** Run `mvn test` - all existing tests pass; new migration applied. ### Tasks #### 1.1 Add Readability4J dependency - Add to `pom.xml`: ```xml net.dankito.readability4j readability4j 1.0.8 ``` - Run `mvn compile` to verify dependency resolution #### 1.2 Create database migration - File: `src/main/resources/db/migrations/V12__add_web_content_to_news_items.sql` - Content: ```sql ALTER TABLE news_items ADD COLUMN web_content TEXT; ``` #### 1.3 Update NewsItem entity - File: `core/model/NewsItem.java` - Add field: ```java @Column(name = "web_content", columnDefinition = "TEXT") private String webContent; ``` #### 1.4 Create WebContentFetcher interface - File: `provider/webfetch/WebContentFetcher.java` - Interface with single method: `Optional fetch(String url)` #### 1.5 Create NoOpWebFetcher - File: `provider/webfetch/NoOpWebFetcher.java` - Annotate with `@ConditionalOnProperty(prefix = "application.web-fetch", name = "enabled", havingValue = "false")` - Returns `Optional.empty()` always - **Unit test:** `NoOpWebFetcherTest` - verify returns empty for any URL --- ## Phase 2: HttpClient Implementation **Goal:** Implement web fetching with content extraction using Readability4J. **Verification:** Run `mvn test` - new unit tests pass for HttpClientWebFetcher. ### Tasks #### 2.1 Add configuration properties - File: `src/main/resources/application.properties` - Add: ```properties # Web Content Fetching application.web-fetch.enabled=true application.web-fetch.timeout-seconds=15 application.web-fetch.user-agent=DayBrief/1.0 ``` - File: `src/main/resources/application-local.properties` - Add same properties #### 2.2 Create HttpClientWebFetcher - File: `provider/webfetch/HttpClientWebFetcher.java` - Annotate with `@ConditionalOnProperty(prefix = "application.web-fetch", name = "enabled", havingValue = "true", matchIfMissing = true)` - Inject `HtmlToMarkdownConverter` - Implement: - `isValidUrl()` - validate HTTP/HTTPS, reject private IPs (SSRF prevention) - `fetchHtml()` - use Java HttpClient with timeout - `extractArticle()` - use Readability4J - `fetch()` - orchestrate and return markdown - **Unit tests:** `HttpClientWebFetcherTest` - `shouldReturnEmptyForNullUrl` - `shouldReturnEmptyForMailtoUrl` - `shouldReturnEmptyForPrivateIp` - `shouldReturnEmptyForLocalhost` - `shouldExtractContentFromValidHtml` (mock HTTP) #### 2.3 Create URL validation utility - In `HttpClientWebFetcher`: - Check protocol is http/https - Parse host, reject: `localhost`, `127.0.0.1`, `10.*`, `172.16-31.*`, `192.168.*`, `169.254.*` - Log rejected URLs at DEBUG level --- ## Phase 3: Processing Integration **Goal:** Integrate web fetching into news item processing pipeline. **Verification:** Run `mvn test` - ProcessingWorker tests pass with mocked WebContentFetcher. ### Tasks #### 3.1 Update AiService interface - File: `provider/ai/AiService.java` - Add overloaded method: ```java EnrichmentWithScoreResult enrichWithScore(String title, String content, String webContent, String briefingCriteria); ``` #### 3.2 Update SpringAiService - File: `provider/ai/SpringAiService.java` - Implement new method with `buildEffectiveContent()` helper: - If webContent null/blank, use original content - If original < 500 chars, combine: "Original snippet:\n{original}\n\nFull article:\n{webContent}" - Otherwise use webContent only - Delegate existing 3-arg method to new 4-arg method with null webContent #### 3.3 Update MockAiService - File: `provider/ai/MockAiService.java` - Add overloaded method (delegate to existing or return mock) #### 3.4 Modify ProcessingWorker - File: `core/queue/ProcessingWorker.java` - Inject `WebContentFetcher` - Add `shouldFetchWebContent(NewsItem)`: - Return false if link null or not http/https - Return false if cleanContent > 2000 chars - Return true otherwise - In `doProcess()`: - Call `shouldFetchWebContent()` - If true, call `webContentFetcher.fetch(link)` - Store result in `item.setWebContent()` - Pass webContent to `aiService.enrichWithScore()` - **Unit tests:** `ProcessingWorkerTest` (mock WebContentFetcher, AiService, Repository) - `shouldFetchWebContentWhenLinkPresent` - `shouldSkipFetchWhenContentOver2000Chars` - `shouldSkipFetchForNonHttpLink` - `shouldProceedWhenFetchFails` - `shouldStoreWebContentWhenFetched` --- ## Phase 4: DTO Content Selection **Goal:** Return best available content to frontend via DTO mapping. **Verification:** Run `mvn test` - NewsItemService tests verify content selection. ### Tasks #### 4.1 Update NewsItemService - File: `core/service/NewsItemService.java` - Add private method: ```java private String selectBestContent(NewsItem item) { if (item.getWebContent() != null && !item.getWebContent().isBlank()) { return item.getWebContent(); } return item.getCleanContent(); } ``` - Update `mapToDTO()` to use `selectBestContent(item)` instead of `item.getCleanContent()` - **Unit tests:** `NewsItemServiceTest` - `shouldReturnWebContentWhenAvailable` - `shouldFallbackToCleanContentWhenWebContentNull` - `shouldFallbackToCleanContentWhenWebContentBlank` --- ## Phase 5: Integration Testing **Goal:** Verify end-to-end web fetching with real HTTP calls (mocked server). **Verification:** Run `mvn verify` - integration tests pass. ### Tasks #### 5.1 Create WebFetchIT integration test - File: `src/test/java/be/transcode/daybrief/server/provider/webfetch/WebFetchIT.java` - Use WireMock to mock HTTP responses - Tests: - `shouldFetchAndExtractArticleContent` - return HTML, verify markdown output - `shouldHandleRedirects` - 301/302 response - `shouldHandleTimeouts` - delayed response - `shouldHandle404` - return empty - `shouldHandle500` - return empty #### 5.2 Manual verification - Start application locally: `mvn spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=local"` - Add RSS source with truncated content (e.g., Hacker News RSS) - Wait for processing - Verify in logs: web content fetch attempts - Check database: `webContent` field populated - View news item in UI: full article content displayed --- ## File Summary | Phase | File | Action | |-------|------|--------| | 1 | `pom.xml` | Add Readability4J dependency | | 1 | `db/migrations/V12__add_web_content_to_news_items.sql` | Create | | 1 | `core/model/NewsItem.java` | Add webContent field | | 1 | `provider/webfetch/WebContentFetcher.java` | Create interface | | 1 | `provider/webfetch/NoOpWebFetcher.java` | Create | | 2 | `application.properties` | Add web-fetch config | | 2 | `application-local.properties` | Add web-fetch config | | 2 | `provider/webfetch/HttpClientWebFetcher.java` | Create | | 3 | `provider/ai/AiService.java` | Add overloaded method | | 3 | `provider/ai/SpringAiService.java` | Implement new method | | 3 | `provider/ai/MockAiService.java` | Add overloaded method | | 3 | `core/queue/ProcessingWorker.java` | Add web fetch logic | | 4 | `core/service/NewsItemService.java` | Add selectBestContent | | 5 | `provider/webfetch/WebFetchIT.java` | Create integration test | ## Test Summary | Phase | Test File | Tests | |-------|-----------|-------| | 1 | `NoOpWebFetcherTest.java` | 1 test | | 2 | `HttpClientWebFetcherTest.java` | 5 tests | | 3 | `ProcessingWorkerTest.java` | 5 tests | | 4 | `NewsItemServiceTest.java` | 3 tests | | 5 | `WebFetchIT.java` | 5 tests |