--- phase: quick-3 plan: 01 subsystem: navigation tags: [remote-links, http, security, whitelist, history] dependency_graph: requires: [quick-1, quick-2] provides: [remote-markdown-fetching, domain-whitelist] affects: [app-navigation, renderer-styling, vault-fetching, config] tech_stack: added: [ureq 2.12 (synchronous HTTP client)] patterns: [domain whitelist check, content-type validation, 5MB body cap, no-history-on-error] key_files: created: [] modified: - Cargo.toml - src/config.rs - src/vault.rs - src/app.rs - src/renderer.rs decisions: - ureq chosen for synchronous HTTP (no async runtime, consistent with existing architecture) - Subdomain matching: sub.example.com matches whitelist entry example.com via suffix check - Content-type validation accepts text/markdown, text/plain, text/x-markdown, and any URL ending in .md - Error cases (domain-not-allowed, fetch-error, not-markdown) do NOT push history entry - Back/forward re-fetch remote pages from network (consistent with re-load-from-disk pattern) - current_url: Option field distinguishes remote vs local page context across resize/reload metrics: duration: "4 min" completed: "2026-03-01T12:15:30Z" tasks_completed: 2 files_modified: 5 --- # Quick Task 3: Remote Markdown Page Linking Summary **One-liner:** Domain-whitelisted HTTP/HTTPS link following with ureq, content-type validation, BBS error screens, and browser history integration. ## What Was Built Users can now follow HTTP/HTTPS links in markdown documents. When a link is activated: 1. The domain is extracted from the URL and checked against `allowed_remote_domains` in `bbs.toml`. 2. If allowed, ureq fetches the URL with a 10-second timeout. 3. The Content-Type header is validated (accepts `text/markdown`, `text/plain`, `text/x-markdown`, or any `.md` URL). 4. The response body is read with a 5 MB cap. 5. The fetched markdown is rendered in the TUI exactly like a local page. 6. Remote pages participate in browser history (Backspace/Alt+Left goes back; re-fetches on return). Error cases show BBS-themed error screens (no history entry pushed): - **Domain not whitelisted**: shows domain name and config hint - **Fetch error** (network, timeout, non-2xx): shows HTTP status or error message - **Not markdown** (HTML, binary): shows Content-Type received HTTP/HTTPS links are styled in `LightMagenta` to visually distinguish them from local `LightCyan` links. ## Tasks Completed | Task | Name | Commit | Key Files | |------|------|--------|-----------| | 1 | Add config field, ureq dependency, and remote fetch function | 5759ec8 | Cargo.toml, src/config.rs, src/vault.rs | | 2 | Wire remote link navigation into app event loop with history and error screens | c8d4754 | src/app.rs, src/renderer.rs | ## Key Changes ### Cargo.toml - Added `ureq = "2.12"` dependency ### src/config.rs - Added `allowed_remote_domains: Vec` field with `#[serde(default)]` - Updated `Default` impl to include empty Vec ### src/vault.rs - Added `RemoteDocument` enum (`Loaded`, `DomainNotAllowed`, `FetchError`, `NotMarkdown`) - Added `extract_domain()` — strips scheme, path, port from URL - Added `domain_is_allowed()` — exact + subdomain matching (case-insensitive) - Added `fetch_remote_markdown()` — whitelist check, 10s timeout, content-type validation, 5MB cap ### src/renderer.rs - HTTP/HTTPS links styled `LightMagenta` in `Event::Start(Tag::Link)` handler (non-wiki, non-local branch) ### src/app.rs - Added `current_url: Option` field (Some = remote page, None = local page) - Added `navigate_to_remote()` method — saves history, fetches, renders, pushes entry on success - Updated `follow_selected_link()` to detect HTTP/HTTPS and dispatch to `navigate_to_remote()` - Updated `navigate_back()` and `navigate_forward()` to re-fetch remote URLs from history - Updated `handle_resize()` to pass `None` vault_path for remote pages - Updated `reload_current_document()` to skip live-reload for remote pages - Updated `navigate_to()` to clear `current_url` when going to a local page ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Fixed clippy lint: manual `split_once` pattern** - **Found during:** Task 2 verification (cargo clippy) - **Issue:** `url.splitn(2, "://").nth(1)` and `resp.status_text().to_string()` triggered clippy warnings - **Fix:** Replaced with `url.split_once("://")?.1` and removed redundant `.to_string()` in format! - **Files modified:** src/vault.rs, src/app.rs - **Commit:** c8d4754 ### Out-of-scope items (deferred) None discovered. ## Verification Results - `cargo build` passes with no errors (3 pre-existing warnings, unrelated to this task) - `cargo clippy` passes with warnings only (no new errors introduced) - HTTP links styled LightMagenta (verified in renderer.rs code path) - Domain whitelist check with exact and subdomain matching implemented - All 4 RemoteDocument variants handled with appropriate error screens - History integration tested via code review (back/forward re-fetch remote URLs) - handle_resize passes None vault_path for remote pages - reload_current_document returns early for remote pages ## Self-Check: PASSED Files exist: - Cargo.toml: FOUND (ureq = "2.12") - src/config.rs: FOUND (allowed_remote_domains field) - src/vault.rs: FOUND (fetch_remote_markdown function) - src/app.rs: FOUND (navigate_to_remote method, current_url field) - src/renderer.rs: FOUND (LightMagenta branch) Commits exist: - 5759ec8: Task 1 commit - c8d4754: Task 2 commit