Files

129 lines
5.4 KiB
Markdown

---
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<String> 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<String>` 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<String>` 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