1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

docs: remove completed planning docs and stale superpowers plan

- Delete NEXT.md (all six tracks implemented)
- Delete docs/superpowers/plans/track1 plan (track complete)
- Trim ARCHIVR-MENTAL-MODEL.md Key Documents table to only living files
This commit is contained in:
TheGeneralist 2026-06-23 17:17:31 +02:00
parent 2d7a4f1766
commit 17d116655a
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
3 changed files with 7 additions and 961 deletions

View file

@ -2,6 +2,13 @@
This document explains the current project shape after the workspace refactor.
## Key Documents
| Document | Role |
|---|---|
| `ARCHIVR-MENTAL-MODEL.md` | **This file.** Current architecture, data flows, and where to edit. |
| `docs/README.md` | User-facing docs: how to run the tool, supported inputs, environment variables. |
## The Big Model
Archivr is now a Rust workspace with three crates:

386
NEXT.md
View file

@ -1,386 +0,0 @@
# Archivr Next Work Decision Handoff
> **For future agentic workers:** Start by reading `ARCHIVR-MENTAL-MODEL.md`, then this file. If the user chooses one track below, create a task-level implementation plan before touching code. Use `superpowers:writing-plans` for the chosen track, then implement with `superpowers:subagent-driven-development` or `superpowers:executing-plans`.
**Goal:** Capture the next plausible product and engineering directions after the database, workspace, Nix packaging, and initial multi-archive web UI foundation.
**Current Architecture:** Archivr is a Rust workspace. `archivr-core` owns archive behavior and SQLite access. `archivr-cli` writes archives. `archivr-server` reads mounted archive databases and serves the static browser UI.
**Tech Stack:** Rust, SQLite via `rusqlite`, Axum, static HTML/CSS/JavaScript, Nix flakes.
---
## Current State
The project now has three crates:
| Crate | Role |
|---|---|
| `crates/archivr-core` | Archive/domain logic, database schema, archive queries, downloader helpers |
| `crates/archivr-cli` | Terminal interface for `archivr init` and `archivr archive` |
| `crates/archivr-server` | Web server, mounted archive registry, JSON API, static web UI |
The browser UI currently does these things:
- Mounts one or more archives through `archivr-server.toml`.
- Lists archives.
- Lists root archive entries in a table.
- Supports simple client-side text filtering.
- Fetches entry detail for a selected row.
- Lists archive runs.
- Shows a minimal admin/mounted-archives view.
The browser UI does not yet do these things:
- Open stored artifacts.
- Serve archived files through stable URLs.
- Show rich entry details.
- Search on the server or through SQLite FTS.
- Manage hierarchical tags.
- Capture new material from the browser.
- Provide production auth/session behavior.
## Product Direction Already Decided
These preferences came from the design discussion:
- The first screen should be the archive table, not a stats dashboard.
- Capture should exist as a button/dialog, not as the main landing workflow.
- The UI should remain table-forward and practical.
- The table should use sans-serif typography.
- The right sidebar/detail rail idea is good.
- Search should become a major, powerful feature.
- Do not use "categories" as product language.
- Use "tags" or "hierarchical tags" for the tree-shaped organization model.
- Archives remain self-contained directories with their own `.archivr` database.
- The server can mount many archive directories, but it has its own registry config.
## Recommended Order
1. Make entry details and artifact access useful.
2. Define and implement search v1.
3. Rename/complete hierarchical tags.
4. Add browser capture.
5. Decide auth/session boundaries.
This order makes the UI increasingly useful without forcing organization work or auth decisions too early.
---
# Track 1: Entry Detail And Artifact Access Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:writing-plans` to expand this track before implementation. Steps should use checkbox syntax for tracking.
**Goal:** Let a user select an entry and inspect/open the files Archivr saved for it.
**Architecture:** Add stable artifact-serving routes to `archivr-server`, backed by archive metadata from `archivr-core`. Keep the static UI simple: selected row opens a right-side detail rail with metadata, original URL, structured root, and artifact links.
**Tech Stack:** Rust, Axum, `tower_http::services::ServeFile` or explicit file responses, static JavaScript.
## Files
| File | Responsibility |
|---|---|
| `crates/archivr-core/src/archive.rs` | Resolve an entry artifact to a safe on-disk path under the archive store |
| `crates/archivr-core/src/database.rs` | Add query helpers only if `archive.rs` cannot use existing schema cleanly |
| `crates/archivr-server/src/routes.rs` | Add artifact-serving API route |
| `crates/archivr-server/static/app.js` | Render selected-entry detail and artifact links |
| `crates/archivr-server/static/styles.css` | Make the detail rail readable and table selection obvious |
| `crates/archivr-server/src/routes.rs` tests | Cover missing archive, missing entry, missing artifact, and valid artifact response |
## Proposed API
```text
GET /api/archives/:archive_id/entries/:entry_uid/artifacts/:artifact_index
```
`artifact_index` is the zero-based index from `EntryDetail.artifacts`. This avoids exposing arbitrary relative paths in URLs for v1.
## Acceptance Criteria
- Selecting a row shows title, source kind, entity kind, original URL, visibility, archive timestamp, structured root, and artifacts.
- Artifact links open in a new tab.
- Invalid archive IDs return `404`.
- Invalid entry UIDs return `404`.
- Invalid artifact indexes return `404`.
- Resolved paths cannot escape the configured archive store directory.
- Existing `cargo test` passes.
- A browser smoke test can select a row and see at least one artifact link for an archive with artifacts.
## Key Risk
Path safety matters. Do not concatenate request path strings into filesystem paths. Resolve artifact paths from database rows, join them against the trusted store path, canonicalize where possible, and reject paths outside the store.
---
# Track 2: Search V1 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:brainstorming` first if changing query language behavior. Then use `superpowers:writing-plans` before implementation.
**Goal:** Replace simple client-side filtering with a real search API that can become Archivr's "OP search" foundation.
**Architecture:** Start with server-side structured filtering over existing SQLite columns. Keep query parsing small and explicit. Defer full SQLite FTS until the fields and syntax feel right.
**Tech Stack:** Rust, Axum query extractors, SQLite queries with bound parameters, static JavaScript.
## Files
| File | Responsibility |
|---|---|
| `crates/archivr-core/src/archive.rs` | Add `SearchEntriesQuery` and `search_entries` |
| `crates/archivr-server/src/routes.rs` | Add query parameters to entries endpoint or add `/search` endpoint |
| `crates/archivr-server/static/app.js` | Debounce search input and call server |
| `crates/archivr-server/static/index.html` | Keep one prominent search bar |
| `crates/archivr-server/static/styles.css` | Style search states without making the UI feel like a dashboard |
## Recommended API
```text
GET /api/archives/:archive_id/entries/search?q=...&source_kind=...&entity_kind=...&from=...&to=...
```
Keep `/entries` as the default unfiltered archive table.
## Query Language V1
Support plain text first:
```text
polymarket
```
Then add explicit prefixes:
```text
source:x
type:tweet
url:medium.com
title:"resume templates"
after:2026-01-01
before:2026-04-01
```
Do not implement fuzzy search, stemming, ranking, or boolean logic in v1.
## Acceptance Criteria
- Empty search returns the same rows as `/entries`.
- Plain text searches title, original URL, entry UID, source kind, entity kind, and visibility.
- Prefix filters are parsed deterministically.
- Unknown prefixes return `400` with a helpful message.
- Search uses SQL parameters, not string interpolation.
- The UI clearly distinguishes loading, no results, and error states.
- Existing `cargo test` passes.
## Key Risk
Search can sprawl. Keep v1 boring and correct, then iterate toward FTS/ranking after the query language feels right.
---
# Track 3: Hierarchical Tags Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:writing-plans` before implementation.
**Goal:** Make the existing tree organization model use product language: tags and hierarchical tags, not taxonomy/categories.
**Architecture:** Rename remaining domain types and APIs from taxonomy language to tag language where doing so does not break migrations unnecessarily. Keep the database table rename decision explicit: either preserve existing table names internally for compatibility, or migrate to `tags` and `entry_tag_assignments`.
**Tech Stack:** Rust, SQLite migrations/schema initialization, static JavaScript.
## Files
| File | Responsibility |
|---|---|
| `crates/archivr-core/src/database.rs` | Rename schema helpers/types/tests from taxonomy to tags |
| `crates/archivr-core/src/archive.rs` | Expose tag-tree and entry-tag APIs |
| `crates/archivr-server/src/routes.rs` | Add tag tree and entry assignment endpoints |
| `crates/archivr-server/static/app.js` | Render tag filters in the sidebar/detail rail |
| `crates/archivr-server/static/styles.css` | Style tag tree compactly |
| `PLAN.md` | Update old design language so future agents do not reintroduce "taxonomy" product language |
## Naming Decision
Preferred product names:
- `Tag`
- `TagNode` if a code type needs to emphasize tree structure
- `EntryTagAssignment`
- `full_path`, such as `/sciences/computer-science/compilers`
Avoid product-visible names:
- category
- taxonomy
- collection
## Acceptance Criteria
- Public/user-facing docs say "tags" or "hierarchical tags".
- No UI text says "taxonomy" or "categories".
- A tag can have a parent.
- An entry can be assigned to the most specific tag.
- Filtering by an ancestor tag can include descendant tags.
- Existing database tests continue to pass.
- New tests cover parent, child, and ancestor query behavior.
## Key Risk
Do not turn tags into first-screen triage work. The archive table remains the first screen; tags support filtering and detail context.
---
# Track 4: Browser Capture Button Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:writing-plans` before implementation.
**Goal:** Add a small browser capture workflow that starts archive runs from the web UI without making capture the landing page.
**Architecture:** Add a POST endpoint in `archivr-server` that delegates to `archivr-core` capture/archive logic. The UI gets a compact Capture button and dialog. Runs appear in the Runs tab.
**Tech Stack:** Rust, Axum POST routes, Serde JSON, existing downloader code, static JavaScript.
## Files
| File | Responsibility |
|---|---|
| `crates/archivr-core/src/archive.rs` | Factor CLI archive operation into reusable core function if needed |
| `crates/archivr-cli/src/main.rs` | Keep CLI as a thin adapter over core capture logic |
| `crates/archivr-server/src/routes.rs` | Add `POST /api/archives/:archive_id/captures` |
| `crates/archivr-server/static/index.html` | Add Capture button and dialog markup |
| `crates/archivr-server/static/app.js` | Submit capture request and refresh entries/runs |
| `crates/archivr-server/static/styles.css` | Style dialog and disabled/loading states |
## Proposed API
```http
POST /api/archives/:archive_id/captures
Content-Type: application/json
{
"locator": "tweet:1234567890"
}
```
Successful v1 response:
```json
{
"run_uid": "run_...",
"status": "completed"
}
```
## Acceptance Criteria
- Capture is a button, not the primary page.
- Empty locator returns `400`.
- Unsupported locator returns a useful error, not a panic.
- Successful capture creates a run and at least one entry.
- After success, the UI refreshes entries and runs.
- The server route reuses core archive logic rather than duplicating CLI behavior.
- Existing `cargo test` passes.
## Key Risk
Long-running captures will eventually need async job tracking. For v1, it is acceptable for the request to run synchronously if the UI clearly shows loading and errors.
---
# Track 5: Local Auth And Session Boundary Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:brainstorming` before implementation because this affects product/security boundaries.
**Goal:** Decide and document what "local development auth" means before adding public/admin behavior.
**Architecture:** Prefer a local-first model until remote/public hosting is real. Avoid half-building production auth. Document which endpoints are trusted-local and which are public-safe.
**Tech Stack:** Rust, Axum middleware later if needed, browser local UI.
## Files
| File | Responsibility |
|---|---|
| `ARCHIVR-MENTAL-MODEL.md` | Document local-only assumption or chosen auth boundary |
| `docs/README.md` | Tell users whether to expose `archivr-server` to a network |
| `crates/archivr-server/src/main.rs` | Bind address configuration if needed |
| `crates/archivr-server/src/routes.rs` | Add middleware only after the model is chosen |
## Recommended Decision For Now
Keep `archivr-server` local-only:
```text
127.0.0.1:8080
```
Do not advertise it as safe to expose publicly.
## Acceptance Criteria
- Docs clearly say whether the server is local-only.
- The default bind address remains loopback.
- If bind address becomes configurable, docs explain the risk of using `0.0.0.0`.
- No public-sharing feature is implemented before auth/session requirements are chosen.
## Key Risk
Public archive visibility exists in the database model, but that is not the same thing as a secure public web server.
---
# Track 6: Plan Cleanup Implementation Plan
> **For agentic workers:** This track can be done without product brainstorming. Use `superpowers:executing-plans` if implementing.
**Goal:** Remove stale planning ambiguity so future threads do not confuse old database plans with current roadmap.
**Architecture:** Keep root docs small and explicit. `PLAN.md` is currently an older database design plan; either rename it or add a status note at the top.
**Tech Stack:** Markdown only.
## Files
| File | Responsibility |
|---|---|
| `PLAN.md` | Mark as historical database design plan, or rename to `DATABASE-DESIGN-PLAN.md` |
| `ARCHIVR-MENTAL-MODEL.md` | Link to this handoff and any retained historical plan |
| `docs/README.md` | Link to current docs only if useful for users |
## Acceptance Criteria
- A future agent can tell which document is current architecture, which is historical design, and which is next-work planning.
- No recreated `docs/superpowers/` directory.
- No `.gitignore` exception is added for planning docs unless the user explicitly asks.
## Key Risk
Do not delete useful database context. The old plan contains schema rationale that may still be valuable.
---
## Suggested First New Thread Prompt
Use this in the next thread if the goal is to continue product development:
```text
Read ARCHIVR-MENTAL-MODEL.md and NEXT.md. I want to implement Track 1: Entry Detail And Artifact Access. Create a task-level implementation plan first, then wait for approval.
```
If the goal is search:
```text
Read ARCHIVR-MENTAL-MODEL.md and NEXT.md. I want to design Search V1. Start with brainstorming the query language and API boundary, then write an implementation plan.
```
If the goal is tags:
```text
Read ARCHIVR-MENTAL-MODEL.md and NEXT.md. I want to implement hierarchical tags and remove taxonomy/category language. Write the implementation plan first.
```
## Self-Review
- Spec coverage: This handoff covers the next UI usefulness work, search, hierarchical tags, web capture, auth boundary, and plan cleanup.
- Placeholder scan: No unresolved implementation slots are used as required work.
- Type consistency: The document uses current crate names and current API concepts from `ARCHIVR-MENTAL-MODEL.md`.

View file

@ -1,575 +0,0 @@
# Track 1: Entry Detail And Artifact Access — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let a user select an archive entry and inspect/open the files Archivr saved for it, served through a safe stable URL.
**Architecture:** Add `resolve_artifact_path` to `archivr-core` for safe path resolution, add a `serve_artifact` handler in `archivr-server` that streams the resolved file via `tower_http::services::ServeFile` (handles HTTP Range requests, ETags, and streaming — no full-file buffering), and expand the static UI's context rail to render all entry metadata and clickable artifact links.
**Tech Stack:** Rust, Axum 0.7, `tower_http::services::ServeFile`, static HTML/CSS/JavaScript. No new crate dependencies — `tower` moves from dev-dep to dep within the workspace.
---
## Files
| File | Change |
|---|---|
| `crates/archivr-server/Cargo.toml` | Move `tower` from `[dev-dependencies]` to `[dependencies]` |
| `crates/archivr-core/src/archive.rs` | Add `resolve_artifact_path(store_path, artifact)` returning a safe `PathBuf` |
| `crates/archivr-server/src/routes.rs` | Add `serve_artifact` handler, register route, add 3 tests |
| `crates/archivr-server/static/app.js` | Expand `renderContextDetail` to show all fields + artifact links |
| `crates/archivr-server/static/styles.css` | Add `.artifact-list`, `.artifact-link`, `.rail-section` styles |
---
### Task 1: `resolve_artifact_path` in archivr-core
**Files:**
- Modify: `crates/archivr-core/src/archive.rs`
- [ ] **Step 1: Write the failing tests in the existing `mod tests` block**
Add to the `mod tests` block at the bottom of `crates/archivr-core/src/archive.rs`:
```rust
#[test]
fn resolve_artifact_path_returns_absolute_path_within_store() {
let dir = tempfile::tempdir().unwrap();
let store_path = dir.path();
std::fs::create_dir_all(store_path.join("raw/a/b")).unwrap();
let artifact_file = store_path.join("raw/a/b/abc.pdf");
std::fs::write(&artifact_file, b"data").unwrap();
let artifact = EntryArtifactSummary {
artifact_role: "primary".to_string(),
storage_area: "raw".to_string(),
relpath: "raw/a/b/abc.pdf".to_string(),
byte_size: Some(4),
};
let resolved = resolve_artifact_path(store_path, &artifact).unwrap();
assert_eq!(resolved, artifact_file.canonicalize().unwrap());
}
#[test]
fn resolve_artifact_path_rejects_traversal() {
let dir = tempfile::tempdir().unwrap();
let store_path = dir.path();
let artifact = EntryArtifactSummary {
artifact_role: "primary".to_string(),
storage_area: "raw".to_string(),
relpath: "../escaped.txt".to_string(),
byte_size: None,
};
assert!(resolve_artifact_path(store_path, &artifact).is_err());
}
```
Note: `tempfile` is already a dev-dependency in `archivr-core`. Check that `use super::*;` is already at the top of the `mod tests` block (it is).
- [ ] **Step 2: Run the tests to see them fail**
```bash
cargo test -p archivr-core resolve_artifact 2>&1
```
Expected: compile error — `resolve_artifact_path` not defined yet.
- [ ] **Step 3: Implement `resolve_artifact_path`**
Add this function to `crates/archivr-core/src/archive.rs`, after `list_runs` and before `#[cfg(test)]`:
```rust
/// Resolves an artifact to its absolute on-disk path under `store_path`.
///
/// `artifact.relpath` is a store-relative path (e.g. `raw/a/b/abc.pdf`).
/// The returned path is canonicalized. Returns an error if the resolved path
/// escapes `store_path` (path traversal protection) or if the file does not exist.
pub fn resolve_artifact_path(
store_path: &Path,
artifact: &EntryArtifactSummary,
) -> Result<PathBuf> {
let joined = store_path.join(&artifact.relpath);
let canonical_store = store_path
.canonicalize()
.with_context(|| format!("failed to canonicalize store path: {}", store_path.display()))?;
let canonical_artifact = joined
.canonicalize()
.with_context(|| format!("artifact path does not exist: {}", joined.display()))?;
if !canonical_artifact.starts_with(&canonical_store) {
bail!(
"artifact path escapes store: {}",
canonical_artifact.display()
);
}
Ok(canonical_artifact)
}
```
- [ ] **Step 4: Run the tests to see them pass**
```bash
cargo test -p archivr-core resolve_artifact 2>&1
```
Expected: both tests PASS.
- [ ] **Step 5: Run full core test suite to confirm no regressions**
```bash
cargo test -p archivr-core 2>&1
```
Expected: all tests PASS.
- [ ] **Step 6: Commit**
```bash
git add crates/archivr-core/src/archive.rs
git commit -m "feat(core): add resolve_artifact_path with path traversal protection"
```
---
### Task 2: `serve_artifact` route in archivr-server
**Files:**
- Modify: `crates/archivr-server/Cargo.toml`
- Modify: `crates/archivr-server/src/routes.rs`
Route: `GET /api/archives/:archive_id/entries/:entry_uid/artifacts/:artifact_index`
`artifact_index` is the zero-based index into `EntryDetail.artifacts`. This avoids any raw filesystem path in the URL. `ServeFile` streams the file and handles HTTP Range requests — browsers can seek in `<video>` elements and large files never buffer in memory.
- [ ] **Step 1: Move `tower` to production dependencies**
In `crates/archivr-server/Cargo.toml`, move `tower` from `[dev-dependencies]` to `[dependencies]`:
```toml
[dependencies]
anyhow.workspace = true
archivr-core = { path = "../archivr-core" }
axum.workspace = true
serde.workspace = true
tokio.workspace = true
toml.workspace = true
tower.workspace = true
tower-http.workspace = true
[dev-dependencies]
tempfile.workspace = true
tower.workspace = true
```
(`tower` stays in `[dev-dependencies]` too so `tower::ServiceExt` remains available in tests without a redundant import path.)
- [ ] **Step 2: Write the failing tests**
Add to the existing `mod tests` block in `crates/archivr-server/src/routes.rs`, after the `missing_archive_returns_404` test:
```rust
#[tokio::test]
async fn artifact_missing_archive_returns_404() {
let response = app(ServerRegistry::default())
.oneshot(
Request::builder()
.uri("/api/archives/nope/entries/entry_abc/artifacts/0")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn artifact_missing_entry_returns_404() {
let dir = tempfile::tempdir().unwrap();
archivr_core::archive::initialize_archive(
dir.path(),
&dir.path().join("store"),
"test",
false,
)
.unwrap();
let archive_path = dir.path().join(".archivr");
let registry = ServerRegistry {
archives: vec![MountedArchive {
id: "test".to_string(),
label: "Test".to_string(),
archive_path,
}],
};
let response = app(registry)
.oneshot(
Request::builder()
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn artifact_out_of_range_index_returns_404() {
let dir = tempfile::tempdir().unwrap();
archivr_core::archive::initialize_archive(
dir.path(),
&dir.path().join("store"),
"test",
false,
)
.unwrap();
let archive_path = dir.path().join(".archivr");
let registry = ServerRegistry {
archives: vec![MountedArchive {
id: "test".to_string(),
label: "Test".to_string(),
archive_path,
}],
};
let response = app(registry)
.oneshot(
Request::builder()
.uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
```
- [ ] **Step 3: Run the failing tests**
```bash
cargo test -p archivr-server artifact 2>&1
```
Expected: compile error — `serve_artifact` not defined yet.
- [ ] **Step 4: Update imports in `routes.rs`**
Replace the existing `use axum` block with:
```rust
use axum::{
Json, Router,
body::Body,
extract::{Path, Request, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
};
```
Add after the `use tower_http` line:
```rust
use tower::ServiceExt;
use tower_http::services::{ServeDir, ServeFile};
```
(Replace the existing `use tower_http::services::{ServeDir, ServeFile};` line — just add `use tower::ServiceExt;` above or below it.)
- [ ] **Step 5: Implement `serve_artifact`**
Add this function to `routes.rs` after `list_runs` and before `mounted_archive`. There is no `content_type_for_path` helper — `ServeFile` infers content type from the file extension automatically:
```rust
async fn serve_artifact(
State(state): State<AppState>,
Path((archive_id, entry_uid, artifact_index)): Path<(String, String, usize)>,
req: Request,
) -> Result<Response, ApiError> {
let mounted = mounted_archive(&state, &archive_id)?;
let paths = archive::read_archive_paths(&mounted.archive_path)?;
let conn = database::open_or_initialize(&mounted.archive_path)?;
let detail = archive::get_entry_detail(&conn, &entry_uid)?
.ok_or(ApiError::not_found("entry not found"))?;
let artifact = detail
.artifacts
.get(artifact_index)
.ok_or(ApiError::not_found("artifact index out of range"))?;
let file_path = archive::resolve_artifact_path(&paths.store_path, artifact)?;
// ServeFile streams the file, handles Range requests (video seeking),
// sets Content-Type/ETag/Last-Modified, and returns 404 if missing.
// Its error type is Infallible — file-not-found becomes a 404 response.
Ok(ServeFile::new(&file_path)
.oneshot(req)
.await
.unwrap()
.into_response())
}
```
- [ ] **Step 6: Register the route in `app()`**
In the `app()` function, add after the `entry_detail` route:
```rust
.route(
"/api/archives/:archive_id/entries/:entry_uid/artifacts/:artifact_index",
get(serve_artifact),
)
```
The full router chain becomes:
```rust
Router::new()
.route("/health", get(|| async { "ok" }))
.route("/api/archives", get(list_archives))
.route("/api/archives/:archive_id/entries", get(list_entries))
.route(
"/api/archives/:archive_id/entries/:entry_uid",
get(entry_detail),
)
.route(
"/api/archives/:archive_id/entries/:entry_uid/artifacts/:artifact_index",
get(serve_artifact),
)
.route("/api/archives/:archive_id/runs", get(list_runs))
.nest_service("/assets", ServeDir::new(&static_dir))
.fallback_service(ServeFile::new(static_dir.join("index.html")))
.with_state(state)
```
- [ ] **Step 7: Run the new tests**
```bash
cargo test -p archivr-server artifact 2>&1
```
Expected: all three new tests PASS.
- [ ] **Step 8: Run full server test suite**
```bash
cargo test -p archivr-server 2>&1
```
Expected: all tests PASS.
- [ ] **Step 9: Commit**
```bash
git add crates/archivr-server/Cargo.toml crates/archivr-server/src/routes.rs
git commit -m "feat(server): add streaming serve_artifact route via ServeFile"
```
---
### Task 3: Expand the context rail UI
**Files:**
- Modify: `crates/archivr-server/static/app.js`
The current `renderContextDetail` (lines 133152) shows: title, type, visibility, artifact count (as number), structured root. Replace it with the full implementation showing all metadata fields and clickable artifact links.
- [ ] **Step 1: Replace `renderContextDetail` entirely**
Find the function at approximately line 133 and replace it with:
```js
function renderContextDetail(detail) {
contextBody.innerHTML = "";
// Title
const titleEl = document.createElement("strong");
titleEl.className = "rail-entry-title";
titleEl.textContent =
valueText(detail.summary.title) || valueText(detail.summary.entry_uid);
contextBody.append(titleEl);
// Metadata section
const metaSection = document.createElement("div");
metaSection.className = "rail-section";
if (detail.summary.original_url) {
const urlRow = document.createElement("div");
urlRow.className = "rail-item";
const urlLabel = document.createElement("span");
urlLabel.className = "rail-label";
urlLabel.textContent = "Original URL";
const urlLink = document.createElement("a");
urlLink.href = detail.summary.original_url;
urlLink.target = "_blank";
urlLink.rel = "noopener noreferrer";
urlLink.className = "rail-url-link";
urlLink.textContent = detail.summary.original_url;
urlRow.append(urlLabel, document.createTextNode(": "), urlLink);
metaSection.append(urlRow);
}
const metaFields = [
["Added", detail.summary.archived_at],
["Source", detail.summary.source_kind],
["Type", detail.summary.entity_kind],
["Visibility", detail.summary.visibility],
["Structured root", detail.structured_root_relpath],
];
for (const [label, value] of metaFields) {
const item = document.createElement("div");
item.className = "rail-item";
const labelEl = document.createElement("span");
labelEl.className = "rail-label";
labelEl.textContent = label;
item.append(labelEl, document.createTextNode(`: ${valueText(value)}`));
metaSection.append(item);
}
contextBody.append(metaSection);
// Artifacts section
if (detail.artifacts.length > 0) {
const artifactsSection = document.createElement("div");
artifactsSection.className = "rail-section";
const artifactsHeading = document.createElement("div");
artifactsHeading.className = "rail-section-heading";
artifactsHeading.textContent = `Artifacts (${detail.artifacts.length})`;
artifactsSection.append(artifactsHeading);
const list = document.createElement("ul");
list.className = "artifact-list";
detail.artifacts.forEach((artifact, index) => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = `/api/archives/${state.archiveId}/entries/${detail.summary.entry_uid}/artifacts/${index}`;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.className = "artifact-link";
const roleName = artifact.artifact_role.replace(/_/g, " ");
const size =
artifact.byte_size != null ? ` (${formatBytes(artifact.byte_size)})` : "";
a.textContent = `${roleName}${size}`;
li.append(a);
list.append(li);
});
artifactsSection.append(list);
contextBody.append(artifactsSection);
} else {
const noArtifacts = document.createElement("div");
noArtifacts.className = "rail-item muted";
noArtifacts.textContent = "No artifacts.";
contextBody.append(noArtifacts);
}
}
```
- [ ] **Step 2: Verify JS parses without errors**
```bash
node --input-type=module < crates/archivr-server/static/app.js 2>&1 | grep -v "ReferenceError\|Cannot find\|is not defined" || true
```
Expected: only DOM-related errors (which happen at runtime, not parse time); no `SyntaxError`.
- [ ] **Step 3: Commit**
```bash
git add crates/archivr-server/static/app.js
git commit -m "feat(ui): expand context rail with all metadata fields and artifact links"
```
---
### Task 4: Style the context rail additions
**Files:**
- Modify: `crates/archivr-server/static/styles.css`
- [ ] **Step 1: Append the new rules to `styles.css`**
Add before the closing `@media` block (or at the very end of the file):
```css
.rail-entry-title {
display: block;
font-size: 15px;
font-weight: 700;
color: var(--ink);
margin-bottom: 12px;
line-height: 1.4;
}
.rail-section {
margin-bottom: 18px;
}
.rail-section-heading {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--accent);
margin-bottom: 6px;
}
.rail-label {
font-weight: 600;
color: var(--ink);
}
.rail-url-link {
color: var(--accent);
word-break: break-all;
font-size: 13px;
}
.artifact-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.artifact-link {
display: block;
padding: 6px 8px;
background: var(--paper-3);
border: 1px solid var(--line);
color: var(--accent);
text-decoration: none;
font-size: 13px;
border-radius: 3px;
}
.artifact-link:hover {
background: var(--line);
text-decoration: underline;
}
```
- [ ] **Step 2: Run the full test suite one final time**
```bash
cargo test 2>&1
```
Expected: all tests in all three crates PASS.
- [ ] **Step 3: Commit**
```bash
git add crates/archivr-server/static/styles.css
git commit -m "feat(ui): style artifact list and context rail sections"
```
---
## Acceptance Criteria Checklist
- [ ] Selecting a row shows: title, source kind, entity kind, original URL (as link), visibility, archived_at, structured root, artifact list.
- [ ] Artifact links open in a new tab at `/api/archives/:id/entries/:uid/artifacts/:index`.
- [ ] Invalid archive ID returns `404`.
- [ ] Invalid entry UID returns `404`.
- [ ] Invalid artifact index (out of range) returns `404`.
- [ ] `relpath` containing `../` is rejected by `resolve_artifact_path`.
- [ ] `cargo test` passes across all three crates.
## Key Risk Note
`resolve_artifact_path` canonicalizes the joined path and verifies it starts with the canonicalized store root. This handles `..` traversal in `relpath`. The user only ever controls the numeric `artifact_index` in the URL — the actual `relpath` comes from the database row, so this is defense-in-depth, not the primary input guard.