diff --git a/.gitignore b/.gitignore index cc251a7..2240c85 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ !docs/ !docs/LICENSE !docs/README* + !crates !crates/** @@ -17,7 +18,6 @@ !Cargo.toml !Cargo.lock !ARCHIVR-MENTAL-MODEL.md -!NEXT.md !frontend/ !frontend/** diff --git a/Cargo.lock b/Cargo.lock index a0729aa..9ce1bd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,13 +125,7 @@ version = "0.1.0" dependencies = [ "anyhow", "archivr-core", - "argon2", "axum", - "axum-extra", - "base64", - "chrono", - "rand", - "rusqlite", "serde", "serde_json", "tempfile", @@ -141,18 +135,6 @@ dependencies = [ "tower-http", ] -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -231,57 +213,18 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-extra" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" -dependencies = [ - "axum", - "axum-core", - "bytes", - "cookie", - "fastrand", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "multer", - "pin-project-lite", - "serde", - "tower", - "tower-layer", - "tower-service", -] - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -378,17 +321,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -434,12 +366,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - [[package]] name = "digest" version = "0.10.7" @@ -448,7 +374,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1054,23 +979,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - [[package]] name = "native-tls" version = "0.2.18" @@ -1088,12 +996,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - [[package]] name = "num-traits" version = "0.2.19" @@ -1158,17 +1060,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1196,21 +1087,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro2" version = "1.0.101" @@ -1235,36 +1111,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - [[package]] name = "regex" version = "1.12.2" @@ -1567,12 +1413,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1656,36 +1496,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "time" -version = "0.3.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 64e0fa4..f88610d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,3 @@ tower-http = { version = "0.6.2", features = ["fs", "trace"] } uuid = { version = "1.18.1", features = ["v4"] } reqwest = { version = "0.12", features = ["blocking"] } base64 = "0.22" -argon2 = { version = "0.5", features = ["std"] } -rand = { version = "0.8", features = ["std"] } -axum-extra = { version = "0.9", features = ["cookie"] } diff --git a/NEXT.md b/NEXT.md deleted file mode 100644 index be3460e..0000000 --- a/NEXT.md +++ /dev/null @@ -1,225 +0,0 @@ -# Archivr Next Work Plan - -> **For future agentic workers:** Read `ARCHIVR-MENTAL-MODEL.md` first, then this file. -> All six tracks from the previous handoff are complete. This document is the current roadmap. - -## Current State - -All five product tracks from the prior session are done: - -| Feature | Status | -|---|---| -| Entry detail + artifact serving | Done | -| Server-side search with prefix filters | Done | -| Hierarchical tags (CRUD, tree, entry assignment, filter) | Done | -| Browser capture button and POST endpoint | Done | -| Local-only auth boundary, bind address config, docs | Done | - -The frontend is a React + Vite app (`frontend/`) that builds into `crates/archivr-server/static/`. - -The remaining gaps come from `docs/README.md` milestones that were never in the prior tracks. - ---- - -## Remaining Work (in order) - -### ~~1. Generic URL capture — plain file download~~ ✅ Done - -**Implemented:** `crates/archivr-core/src/downloader/http.rs` (`download` fn, extension helpers, -HTML rejection), `Source::Url` variant in `capture.rs`, `determine_source` routes all unmatched -`http://`/`https://` URLs to `Source::Url`, `perform_capture` arm calls the downloader and flows -through the existing temp → hash → raw store pipeline. `reqwest 0.12` (blocking) added to workspace. -`docs/README.md` "URLs" milestone checked off. 112 tests green. - ---- - -### ~~2. Web page archiving — single-file HTML snapshots~~ ✅ Done - -**Implemented:** `crates/archivr-core/src/downloader/singlefile.rs` (new module; shells out to -`single-file-cli` via `ARCHIVR_SINGLE_FILE` env var; launches headless Chromium via -`ARCHIVR_CHROME`; flags: `--browser-wait-until=networkidle2`, `--remove-unused-styles=false`, -`--block-scripts=false`, `--remove-alternative-medias=false` for full CSS/font fidelity). -`Source::WebPage` variant added to `capture.rs`; HEAD-first probe (`probe_url_kind` in `http.rs`) -routes `text/html` responses to `Source::WebPage` and everything else to `Source::Url`. -Font deduplication implemented as a follow-on: `font_extractor.rs` post-processes the saved HTML, -extracts embedded `@font-face` base64 blobs, stores each as a deduplicated artifact in the raw -store, and rewrites the HTML to reference them via `/api/archives/:id/blobs/:sha256`. -`hash_bytes` added to `hash.rs`; `get_blob_by_sha256` added to `database.rs`; -`GET /api/archives/:id/blobs/:sha256` route added to `routes.rs`. -`flake.nix` updated: `pkgs.single-file-cli` and (Linux-only) `pkgs.chromium` added to -`buildInputs`; `ARCHIVR_SINGLE_FILE` and `ARCHIVR_CHROME` env vars wired into both wrappers. -`docs/README.md` "Archive web pages" milestone checked off. 143 tests green. - ---- - -### 3. Async capture jobs - -**What:** Capture currently runs synchronously on the HTTP request thread. A YouTube video, -a large tweet thread, or a slow monolith page will stall the browser request. This track -adds a job queue so `POST /api/archives/:id/captures` returns immediately and the UI polls -for completion. - -**Where to edit:** - -| File | Change | -|---|---| -| `crates/archivr-core/src/database.rs` | Add `capture_jobs` table to the schema: `(id, job_uid, run_uid, archive_id, status TEXT CHECK(status IN ('pending','running','completed','failed')), error_text, created_at, updated_at)`. Add `create_capture_job`, `update_capture_job_status`, `get_capture_job` helpers. | -| `crates/archivr-core/src/archive.rs` | Add `CaptureJob` summary type and `get_capture_job` query. | -| `crates/archivr-server/src/routes.rs` | `POST /api/archives/:id/captures` → insert job row, spawn `tokio::task::spawn_blocking` for the capture call, return `{ job_uid, status: "pending" }` immediately. Add `GET /api/archives/:id/capture_jobs/:job_uid` route. | -| `frontend/src/api.js` | Add `pollCaptureJob(archiveId, jobUid)` | -| `frontend/src/components/CaptureDialog.jsx` | After submit: enter polling loop (500 ms interval), show "Running…" state, handle `completed` (close + refresh) and `failed` (show error). | - -**Acceptance criteria:** -- `POST /captures` returns within 50 ms regardless of capture duration. -- UI shows "Running…" and updates when the job finishes. -- Failed captures surface the error message in the dialog. -- A server restart while a job is `running` leaves it stalled as `running`; on next startup, - jobs stuck in `running` should be marked `failed` with a note ("interrupted by server restart"). -- Existing `cargo test` passes. - -**Key risk:** `spawn_blocking` runs on a Tokio thread pool thread. A very long capture -(15-minute YouTube video) will occupy one thread for the duration. For now that is acceptable. -A proper queue (channel + worker task) can replace it later without changing the DB schema or API. - ---- - -### ~~4. Auth foundation — session + role + setup~~ ✅ Done - -**Implemented:** `crates/archivr-server/src/auth.rs` (new; `AuthUser` extractor, Argon2id password -hashing, token generation, role bit constants). `crates/archivr-core/src/database.rs`: -`initialize_auth_schema` (roles, user_roles, sessions, api_tokens, instance_settings tables); -`open_auth_db` (separate server-level auth SQLite); `create_owner`, `compute_role_bits`, -`create_session`, `get_session`, `create_api_token`, `get_user_for_token` and related helpers. -`crates/archivr-server/src/routes.rs`: `AppState` gains `auth_db_path`; `setup_guard` middleware -returns 503 until owner created; `POST /api/auth/login`, `POST /api/auth/logout`, -`GET /api/auth/me`, `GET|POST /api/auth/setup`, `GET|POST|DELETE /api/auth/tokens` endpoints; -WRITE routes (captures, tags) guarded by `ROLE_USER`. `crates/archivr-server/src/main.rs`: -computes auth DB path from config directory; session cleanup background task. -Frontend: `LoginPage.jsx`, `SetupPage.jsx`, `AuthContext` in `App.jsx`, user menu in `Topbar.jsx`. -`docs/specs/2026-06-25-auth-foundation-design.md` has the full design. Roles use a bitmask: -guest=1, user=2, admin=4, owner=8. All tests green. - ---- - -### ~~5. User management~~ ✅ Done - -**Implemented:** `crates/archivr-core/src/database.rs`: `UserSummary` and `RoleRecord` structs; -10 new pub functions (`invalidate_user_sessions`, `get_user_id_by_uid`, `list_users`, -`get_user_by_uid`, `create_user`, `set_user_status`, `assign_role`, `remove_role`, `list_roles`, -`create_custom_role`). `crates/archivr-server/src/routes.rs`: 5 admin routes -(`GET|POST /api/admin/users`, `PATCH /api/admin/users/:uid/status`, -`POST|DELETE /api/admin/users/:uid/roles`, `GET|POST /api/admin/roles`); 7 handler functions; -request body structs. `frontend/src/api.js`: 7 admin helpers (`listAdminUsers`, -`createAdminUser`, `setUserStatus`, `assignRole`, `removeRole`, `listRoles`, `createRole`). -`frontend/src/components/AdminView.jsx`: two-tab admin panel (Users / Roles) with ban/unban, -create user form, create custom role form. Role bitmask: guest=1, user=2, admin=4, owner=8; -custom roles get bit_position≥4. Ban invalidates all sessions. Only-owner guard on role removal. -169 tests green. - ---- - -### ~~6. Permissions & visibility — collection model~~ ✅ Done - -**Implemented:** `database.rs`: `collections` + `collection_entries` tables; seed default collection ('All Entries'); -migration inserts existing entries into default collection with `visibility_bits` derived from legacy `visibility` string -(`'public'`→3, `'unlisted'`→2, `'private'`→0). 8 new pub functions (`ensure_default_collection`, `create_collection`, -`list_collections`, `get_collection_by_uid`, `add_entry_to_collection`, `update_collection_entry_visibility`, -`remove_entry_from_collection`, `get_entry_collection_memberships`). `create_archived_entry` auto-enrolls new entries -into the default collection. `archive.rs`: `list_root_entries` + `search_entries` accept `caller_bits: u32` and enforce -collection visibility (`admin/owner bypass`; others filtered by `visibility_bits & caller_bits`). `list_entries_for_collection`, -`get_entry_collections`, `EntryCollectionMembership` added. `routes.rs`: read routes extract auth and pass caller_bits; -5 collection-entry route groups (`GET|POST /api/archives/:id/collections`, `GET /api/archives/:id/collections/:uid`, -`POST|DELETE|PATCH /api/archives/:id/collections/:uid/entries(/:entry_uid)`, -`GET /api/archives/:id/entries/:uid/collections`). Frontend: `CollectionsView.jsx` (list + detail + create form); -`collections` nav item in Topbar; entry collection memberships + visibility shown in ContextRail. 169 tests green. -Depends on Track 5. - ---- - -### ~~7. Settings~~ ✅ Done - -**Implemented:** `database.rs`: `display_name TEXT` column migration (ALTER TABLE, idempotent); -`InstanceSettings` struct; 6 new pub fns (`get_instance_settings`, `update_instance_settings`, -`update_user_display_name`, `update_user_password`, `get_user_password_hash`, -`get_user_display_name`). `routes.rs`: `PATCH /api/auth/me` (display name update + current-password- -verified password change); `GET|PATCH /api/admin/instance-settings` (ROLE_ADMIN); -`auth_me` now returns `display_name`. `frontend/src/api.js`: 7 new helpers (`updateProfile`, -`changePassword`, `listTokens`, `createToken`, `deleteToken`, `getInstanceSettings`, -`updateInstanceSettings`). `frontend/src/components/SettingsView.jsx`: tabbed settings page -(Profile — display name + password change; API Tokens — create/list/revoke with one-time reveal; -Instance — public index, public content, open registration toggles + default visibility select). -`Topbar.jsx`: settings nav button; user-menu shows display_name ?? username. -`App.jsx`: renders SettingsView for `view === 'settings'`. 176 tests green. Depends on Track 5. - ---- - -### 8. Collections UI - -**What:** Full collection management — create, rename, delete, add/remove entries, -set per-entry visibility within a collection, make collections public. -Depends on Tracks 5–6. - ---- - -### 9. Cloud backup — S3-compatible - -**What:** A command or scheduled operation that syncs the archive store to an S3-compatible -bucket (AWS S3, Cloudflare R2, Backblaze B2). Incremental: only uploads blobs not already -present in the bucket. The DB is also backed up. - -**Design decisions to make before implementing:** -- CLI subcommand (`archivr backup`) vs. scheduled via the server vs. both. -- Per-archive config vs. a global backup profile. -- Whether to back up only the blob store or also the SQLite DB. - -**Recommended shape:** - -```toml -# In the archive's own config or in archivr-server.toml -[backup.s3] -bucket = "my-archivr-backup" -prefix = "personal/" -endpoint = "https://..." # optional, for R2/B2 -region = "us-east-1" -# credentials via AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars -``` - -**Where to edit (when ready):** - -| File | Change | -|---|---| -| `crates/archivr-core/src/backup.rs` | New module. Walk `store/raw/` tree, list objects in bucket, diff, upload new blobs. Also snapshot and upload `archivr.sqlite`. | -| `crates/archivr-cli/src/main.rs` | Add `archivr backup [--archive ]` subcommand | -| `Cargo.toml` | Add `aws-sdk-s3` or `object_store` crate dependency | -| `docs/README.md` | Document backup config and command | - -**Acceptance criteria:** -- `archivr backup` uploads all blobs not already in the bucket. -- A second run uploads nothing (idempotent). -- DB snapshot is included. -- Missing or invalid credentials return a clear error before any uploads begin. - ---- - -### 10. Cloud storage archiving (Google Drive, Dropbox, OneDrive) - -Deferred. Each requires per-service OAuth, API clients, and download logic. Implement after -Tracks 1–4 are stable. - -When approached, add a `Source::GoogleDrive`, `Source::Dropbox`, etc. variant in `capture.rs` -and a corresponding downloader module. Consider `rclone` as a shell-out strategy analogous to -`yt-dlp` and `monolith` — it handles auth and download for all three services. - ---- - -## What to Do First - -Tracks 1, 2, 3, 4, 5, and 6 are complete. Track 7 (settings) is the next priority. - -Open the next thread with: - -```text -Read ARCHIVR-MENTAL-MODEL.md and NEXT.md. I want to implement Track 7: settings — -account profile, password change, instance settings UI, API token management. -Create a task-level implementation plan first, then wait for approval. -``` diff --git a/crates/archivr-cli/src/main.rs b/crates/archivr-cli/src/main.rs index 22606be..4cf56a4 100644 --- a/crates/archivr-cli/src/main.rs +++ b/crates/archivr-cli/src/main.rs @@ -63,7 +63,7 @@ fn main() -> Result<()> { } }; let archive_paths = archive::read_archive_paths(&archive_path)?; - let result = archivr_core::capture::perform_capture(&archive_paths, path, None)?; + let result = archivr_core::capture::perform_capture(&archive_paths, path)?; println!("Archived: run {}", result.run_uid); Ok(()) } diff --git a/crates/archivr-core/src/archive.rs b/crates/archivr-core/src/archive.rs index 0961830..ae26171 100644 --- a/crates/archivr-core/src/archive.rs +++ b/crates/archivr-core/src/archive.rs @@ -60,17 +60,6 @@ pub struct RunSummary { pub error_summary: Option, } -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub struct CaptureJobSummary { - pub job_uid: String, - pub archive_id: String, - pub run_uid: Option, - pub status: String, - pub error_text: Option, - pub created_at: String, - pub updated_at: String, -} - #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] pub struct Tag { pub tag_uid: String, @@ -85,15 +74,6 @@ pub struct TagNode { pub children: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub struct CollectionSummary { - pub collection_uid: String, - pub name: String, - pub slug: String, - pub default_visibility_bits: u32, - pub created_at: String, -} - pub fn find_archive_path_from(start: &Path) -> Result> { let mut dir = start.to_path_buf(); loop { @@ -193,7 +173,7 @@ pub fn initialize_store_directories(store_path: &Path) -> Result<()> { Ok(()) } -pub fn list_root_entries(conn: &rusqlite::Connection, caller_bits: u32) -> Result> { +pub fn list_root_entries(conn: &rusqlite::Connection) -> Result> { let mut stmt = conn.prepare( "SELECT e.entry_uid, @@ -212,20 +192,12 @@ pub fn list_root_entries(conn: &rusqlite::Connection, caller_bits: u32) -> Resul LEFT JOIN entry_artifacts ea ON ea.entry_id = e.id LEFT JOIN blobs b ON b.id = ea.blob_id WHERE e.parent_entry_id IS NULL - AND ( - CAST(?1 AS INTEGER) & 12 != 0 - OR EXISTS ( - SELECT 1 FROM collection_entries ce - WHERE ce.entry_id = e.id - AND ce.visibility_bits & CAST(?1 AS INTEGER) != 0 - ) - ) GROUP BY e.id ORDER BY e.archived_at DESC, e.id DESC", )?; let entries = stmt - .query_map([caller_bits as i64], |row| { + .query_map([], |row| { Ok(EntrySummary { entry_uid: row.get(0)?, archived_at: row.get(1)?, @@ -269,7 +241,7 @@ pub fn get_entry_detail( return Ok(None); }; - let summary = list_root_entries(conn, u32::MAX)? + let summary = list_root_entries(conn)? .into_iter() .find(|entry| entry.entry_uid == entry_uid) .context("entry disappeared while loading detail")?; @@ -327,110 +299,6 @@ pub fn list_runs(conn: &rusqlite::Connection) -> Result> { Ok(runs) } -pub fn get_capture_job( - conn: &rusqlite::Connection, - job_uid: &str, -) -> Result> { - Ok(database::get_capture_job(conn, job_uid)?.map(|r| CaptureJobSummary { - job_uid: r.job_uid, - archive_id: r.archive_id, - run_uid: r.run_uid, - status: r.status, - error_text: r.error_text, - created_at: r.created_at, - updated_at: r.updated_at, - })) -} - -/// Lists all collections in the archive. -pub fn list_collections(conn: &rusqlite::Connection) -> Result> { - let records = database::list_collections(conn)?; - Ok(records - .into_iter() - .map(|r| CollectionSummary { - collection_uid: r.collection_uid, - name: r.name, - slug: r.slug, - default_visibility_bits: r.default_visibility_bits, - created_at: r.created_at, - }) - .collect()) -} - -/// Represents an entry's membership in a collection with its visibility bits. -#[derive(Debug, Clone, serde::Serialize)] -pub struct EntryCollectionMembership { - pub collection_uid: String, - pub visibility_bits: u32, -} - -/// Returns collection memberships for the given entry_uid. -/// Returns Ok(None) if the entry_uid does not exist. -pub fn get_entry_collections( - conn: &rusqlite::Connection, - entry_uid: &str, -) -> Result>> { - let Some(entry_id) = conn - .query_row( - "SELECT id FROM archived_entries WHERE entry_uid = ?1", - [entry_uid], - |row| row.get::<_, i64>(0), - ) - .optional()? - else { - return Ok(None); - }; - let memberships = database::get_entry_collection_memberships(conn, entry_id)?; - Ok(Some( - memberships - .into_iter() - .map(|(_, uid, bits)| EntryCollectionMembership { - collection_uid: uid, - visibility_bits: bits, - }) - .collect(), - )) -} - -/// Returns entries belonging to a collection, filtered by caller_bits visibility. -/// Caller with admin/owner bits (4|8) sees all entries regardless of visibility_bits. -pub fn list_entries_for_collection( - conn: &rusqlite::Connection, - collection_id: i64, - caller_bits: u32, -) -> Result> { - let sql = format!( - "{} {} \ - JOIN collection_entries ce ON ce.entry_id = e.id \ - WHERE ce.collection_id = ?1 \ - AND (CAST(?2 AS INTEGER) & 12 != 0 \ - OR (ce.visibility_bits & CAST(?2 AS INTEGER)) != 0) \ - GROUP BY e.id \ - ORDER BY e.archived_at DESC, e.id DESC", - ENTRY_SELECT_COLS, - ENTRY_FROM_JOINS, - ); - let mut stmt = conn.prepare(&sql)?; - let entries = stmt - .query_map([collection_id, caller_bits as i64], |row| { - Ok(EntrySummary { - entry_uid: row.get(0)?, - archived_at: row.get(1)?, - source_kind: row.get(2)?, - entity_kind: row.get(3)?, - title: row.get(4)?, - visibility: row.get(5)?, - original_url: row.get(6)?, - artifact_count: row.get(7)?, - total_artifact_bytes: row.get(8)?, - parent_entry_uid: row.get(9)?, - has_favicon: row.get::<_, i64>(10)? != 0, - }) - })? - .collect::>>()?; - Ok(entries) -} - /// 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`). @@ -456,7 +324,7 @@ pub fn resolve_artifact_path( Ok(canonical_artifact) } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct SearchEntriesQuery { /// Free-text term: LIKE-matched against title, canonical_url, entry_uid, source_kind, entity_kind, visibility pub q: Option, @@ -474,25 +342,6 @@ pub struct SearchEntriesQuery { pub before: Option, /// Tag full_path filter; includes all entries (root + child) matching the tag subtree pub tag: Option, - /// Role bits of the caller for visibility filtering. Admins (bits 4/8) bypass all filters. - /// Pass `u32::MAX` internally to bypass all visibility. Pass 0 for unauthenticated guests only. - pub caller_bits: u32, -} - -impl Default for SearchEntriesQuery { - fn default() -> Self { - Self { - q: None, - source_kind: None, - entity_kind: None, - url: None, - title: None, - after: None, - before: None, - tag: None, - caller_bits: u32::MAX, - } - } } /// Parses a raw search string into a [`SearchEntriesQuery`]. @@ -623,15 +472,6 @@ pub fn search_entries( params.push(b.clone()); } - // Visibility filter - let n = params.len() + 1; - sql.push_str(&format!( - " AND (CAST(?{n} AS INTEGER) & 12 != 0 \ - OR EXISTS (SELECT 1 FROM collection_entries ce \ - WHERE ce.entry_id = e.id AND ce.visibility_bits & CAST(?{n} AS INTEGER) != 0))" - )); - params.push(query.caller_bits.to_string()); - sql.push_str(" GROUP BY e.id ORDER BY e.archived_at DESC, e.id DESC"); let mut stmt = conn.prepare(&sql)?; @@ -983,7 +823,7 @@ mod tests { database::complete_archive_run_item(&conn, item.id, entry.id).unwrap(); database::finish_archive_run(&conn, run.id).unwrap(); - let entries = list_root_entries(&conn, u32::MAX).unwrap(); + let entries = list_root_entries(&conn).unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].title.as_deref(), Some("Saved Article")); assert_eq!(entries[0].artifact_count, 1); @@ -1154,7 +994,7 @@ mod tests { #[test] fn search_empty_query_returns_all_root_entries() { let conn = make_test_db_with_entries(); - let all = list_root_entries(&conn, u32::MAX).unwrap(); + let all = list_root_entries(&conn).unwrap(); let searched = search_entries(&conn, &SearchEntriesQuery::default()).unwrap(); assert_eq!(all.len(), searched.len()); } diff --git a/crates/archivr-core/src/capture.rs b/crates/archivr-core/src/capture.rs index c2345d7..d85ceaf 100644 --- a/crates/archivr-core/src/capture.rs +++ b/crates/archivr-core/src/capture.rs @@ -332,6 +332,9 @@ fn determine_source(path: &str) -> Source { fn hash_exists(hash: &str, file_extension: &str, store_path: &Path) -> Result { let path = store_path.join(raw_relative_path_from_hash(hash, file_extension)?); + + println!("Checking {}", path.display()); + Ok(path.exists()) } @@ -672,11 +675,7 @@ fn fail_run( anyhow::anyhow!("{}", message) } -pub fn perform_capture( - archive_paths: &ArchivePaths, - locator: &str, - archive_id: Option<&str>, -) -> Result { +pub fn perform_capture(archive_paths: &ArchivePaths, locator: &str) -> Result { let timestamp = Local::now().format("%Y-%m-%dT%H-%M-%S%.3f").to_string(); let store_path = &archive_paths.store_path; @@ -791,32 +790,13 @@ pub fn perform_capture( .join("temp") .join(×tamp) .join(format!("{timestamp}{file_extension}")); - - // Font extraction: rewrite the HTML in-place before hashing. - // Only runs when archive_id is known (server context). CLI passes - // None and keeps fonts embedded — no behaviour change for CLI. - let (html_hash, byte_size, extracted_fonts) = - if let Some(aid) = archive_id { - let content = fs::read_to_string(&temp_html) - .with_context(|| format!("failed to read {}", temp_html.display()))?; - let (rewritten, fonts) = - downloader::font_extractor::extract_and_rewrite(&content, store_path, aid) - .unwrap_or_else(|_| (content.clone(), vec![])); // non-fatal - fs::write(&temp_html, rewritten.as_bytes()) - .with_context(|| "failed to write rewritten HTML")?; - let size = rewritten.len() as i64; - let new_hash = crate::hash::hash_bytes(rewritten.as_bytes()); - (new_hash, size, fonts) - } else { - let size = fs::metadata(&temp_html) - .with_context(|| format!("failed to stat {}", temp_html.display()))? - .len() as i64; - (result.html_hash.clone(), size, vec![]) - }; + let byte_size = fs::metadata(&temp_html) + .with_context(|| format!("failed to stat staged file {}", temp_html.display()))? + .len() as i64; // 1. Move HTML to raw store (if this hash hasn't been seen before). - if !hash_exists(&html_hash, &file_extension, store_path)? { - move_temp_to_raw(&temp_html, &html_hash, store_path)?; + if !hash_exists(&result.html_hash, &file_extension, store_path)? { + move_temp_to_raw(&temp_html, &result.html_hash, store_path)?; } // 2. Process favicon while the temp dir still exists. @@ -857,7 +837,7 @@ pub fn perform_capture( locator, locator, source, - &html_hash, + &result.html_hash, &file_extension, byte_size, result.title, @@ -879,31 +859,6 @@ pub fn perform_capture( ); } - // 6. Register each extracted font as a deduplicated blob + artifact. - for font in extracted_fonts { - let font_blob = database::BlobRecord { - sha256: font.sha256.clone(), - byte_size: font.byte_size, - mime_type: mime_for_font_ext(&font.ext), - extension: font.ext.strip_prefix('.').map(|s| s.to_string()), - raw_relpath: font.raw_relpath.clone(), - }; - if let Ok(blob_id) = database::upsert_blob(&conn, &font_blob) { - let _ = database::add_entry_artifact( - &conn, - &database::NewArtifact { - entry_id: entry.id, - artifact_role: "font".to_string(), - storage_area: "raw".to_string(), - relpath: font.raw_relpath, - blob_id: Some(blob_id), - logical_path: None, - metadata_json: None, - }, - ); - } - } - database::finish_archive_run(&conn, run.id)?; return Ok(CaptureResult { run_uid: run.run_uid.clone(), @@ -1064,16 +1019,6 @@ pub fn perform_capture( }) } -fn mime_for_font_ext(ext: &str) -> Option { - match ext { - ".woff2" => Some("font/woff2".to_string()), - ".woff" => Some("font/woff".to_string()), - ".ttf" => Some("font/ttf".to_string()), - ".otf" => Some("font/otf".to_string()), - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/archivr-core/src/database.rs b/crates/archivr-core/src/database.rs index d690070..d207c35 100644 --- a/crates/archivr-core/src/database.rs +++ b/crates/archivr-core/src/database.rs @@ -73,81 +73,6 @@ pub struct TagRecord { pub full_path: String, } -#[derive(Debug, Clone)] -pub struct AuthUserRecord { - pub id: i64, - pub user_uid: String, - pub username: String, - pub password_hash: String, - pub status: String, -} - -#[derive(Debug, Clone)] -pub struct SessionRecord { - pub user_id: i64, - pub role_bits: u32, - pub last_seen_at: String, - pub session_uid: String, -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct ApiTokenRecord { - pub token_uid: String, - pub name: String, - pub created_at: String, - pub last_used_at: Option, -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct CaptureJobRecord { - pub job_uid: String, - pub archive_id: String, - pub run_uid: Option, - pub status: String, - pub error_text: Option, - pub created_at: String, - pub updated_at: String, -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct UserSummary { - pub user_uid: String, - pub username: String, - pub email: Option, - pub status: String, - pub created_at: String, - pub role_slugs: Vec, - pub role_bits: u32, -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct RoleRecord { - pub role_uid: String, - pub slug: String, - pub name: String, - pub level: i64, - pub bit_position: i64, - pub is_builtin: bool, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct InstanceSettings { - pub public_index_enabled: bool, - pub public_entry_content_enabled: bool, - pub open_registration_enabled: bool, // maps to public_archive_submission_enabled column - pub default_entry_visibility: u32, -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct CollectionRecord { - pub id: i64, - pub collection_uid: String, - pub name: String, - pub slug: String, - pub default_visibility_bits: u32, - pub created_at: String, -} - pub fn database_path(archive_path: &Path) -> PathBuf { archive_path.join(DATABASE_FILE_NAME) } @@ -290,17 +215,6 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> { PRIMARY KEY (entry_id, tag_id) ); - CREATE TABLE IF NOT EXISTS capture_jobs ( - id INTEGER PRIMARY KEY, - job_uid TEXT NOT NULL UNIQUE, - archive_id TEXT NOT NULL, - run_uid TEXT, - status TEXT NOT NULL CHECK (status IN ('pending','running','completed','failed')) DEFAULT 'pending', - error_text TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_capture_jobs_status ON capture_jobs(status); CREATE INDEX IF NOT EXISTS idx_archive_run_items_run_id ON archive_run_items(run_id); CREATE INDEX IF NOT EXISTS idx_archived_entries_source_identity_id ON archived_entries(source_identity_id); CREATE INDEX IF NOT EXISTS idx_archived_entries_created_by_user_id ON archived_entries(created_by_user_id); @@ -311,643 +225,11 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> { CREATE INDEX IF NOT EXISTS idx_entry_artifacts_blob_id ON entry_artifacts(blob_id); CREATE INDEX IF NOT EXISTS idx_tags_parent_tag_id ON tags(parent_tag_id); CREATE INDEX IF NOT EXISTS idx_entry_tag_assignments_tag_id ON entry_tag_assignments(tag_id); - - CREATE TABLE IF NOT EXISTS collections ( - id INTEGER PRIMARY KEY, - collection_uid TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - slug TEXT NOT NULL UNIQUE, - default_visibility_bits INTEGER NOT NULL DEFAULT 2, - created_at TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS collection_entries ( - collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE, - entry_id INTEGER NOT NULL REFERENCES archived_entries(id) ON DELETE CASCADE, - visibility_bits INTEGER NOT NULL DEFAULT 2, - added_at TEXT NOT NULL, - PRIMARY KEY (collection_id, entry_id) - ); - - CREATE INDEX IF NOT EXISTS idx_collection_entries_entry_id ON collection_entries(entry_id); - CREATE INDEX IF NOT EXISTS idx_collection_entries_collection_id ON collection_entries(collection_id); - - -- Seed default collection (idempotent) - INSERT OR IGNORE INTO collections (collection_uid, name, slug, default_visibility_bits, created_at) - VALUES ('coll_default', 'All Entries', '_default_', 2, datetime('now')); - - -- Migrate existing entries to default collection (idempotent) - INSERT OR IGNORE INTO collection_entries (collection_id, entry_id, visibility_bits, added_at) - SELECT - (SELECT id FROM collections WHERE slug = '_default_'), - ae.id, - CASE ae.visibility - WHEN 'public' THEN 3 - WHEN 'unlisted' THEN 2 - ELSE 0 - END, - ae.archived_at - FROM archived_entries ae; "#, )?; Ok(()) } -pub fn initialize_auth_schema(conn: &Connection) -> Result<()> { - conn.pragma_update(None, "journal_mode", "WAL")?; - conn.pragma_update(None, "foreign_keys", "ON")?; - conn.execute_batch( - r#" - CREATE TABLE IF NOT EXISTS roles ( - id INTEGER PRIMARY KEY, - role_uid TEXT NOT NULL UNIQUE, - slug TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - level INTEGER NOT NULL, - bit_position INTEGER NOT NULL UNIQUE, - is_builtin INTEGER NOT NULL DEFAULT 0 CHECK (is_builtin IN (0, 1)) - ); - - INSERT OR IGNORE INTO roles (role_uid, slug, name, level, bit_position, is_builtin) VALUES - ('role-guest', 'guest', 'Guest', 0, 0, 1), - ('role-user', 'user', 'User', 1, 1, 1), - ('role-admin', 'admin', 'Admin', 3, 2, 1), - ('role-owner', 'owner', 'Owner', 4, 3, 1); - - CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_id INTEGER NOT NULL REFERENCES roles(id), - assigned_at TEXT NOT NULL, - assigned_by_user_id INTEGER REFERENCES users(id), - PRIMARY KEY (user_id, role_id) - ); - - CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY, - session_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_bits INTEGER NOT NULL, - created_at TEXT NOT NULL, - last_seen_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - user_agent TEXT - ); - CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); - - CREATE TABLE IF NOT EXISTS api_tokens ( - id INTEGER PRIMARY KEY, - token_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - token_hash TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - created_at TEXT NOT NULL, - last_used_at TEXT, - expires_at TEXT - ); - CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id ON api_tokens(user_id); - - CREATE TABLE IF NOT EXISTS instance_settings ( - id INTEGER PRIMARY KEY CHECK (id = 1), - public_index_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_index_enabled IN (0, 1)), - public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)), - public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)), - default_entry_visibility INTEGER NOT NULL DEFAULT 2 - ); - - INSERT OR IGNORE INTO instance_settings - (id, public_index_enabled, public_entry_content_enabled, - public_archive_submission_enabled, default_entry_visibility) - VALUES (1, 0, 0, 0, 2); - - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - user_uid TEXT NOT NULL UNIQUE, - username TEXT NOT NULL UNIQUE, - email TEXT UNIQUE, - password_hash TEXT NOT NULL, - display_name TEXT, - status TEXT NOT NULL CHECK (status IN ('active', 'disabled')), - role TEXT NOT NULL CHECK (role IN ('admin', 'user')), - created_at TEXT NOT NULL, - last_login_at TEXT - ); - "#, - )?; - // Add display_name column to users if not present (idempotent migration) - let _ = conn.execute("ALTER TABLE users ADD COLUMN display_name TEXT", []); - - Ok(()) -} - -pub fn open_auth_db(auth_db_path: &Path) -> Result { - if let Some(parent) = auth_db_path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!("failed to create auth DB directory {}", parent.display()) - })?; - } - let conn = Connection::open(auth_db_path).with_context(|| { - format!("failed to open auth database at {}", auth_db_path.display()) - })?; - initialize_auth_schema(&conn)?; - Ok(conn) -} - -/// Returns true if an owner account exists. -pub fn ensure_owner_exists(conn: &Connection) -> Result { - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM user_roles ur - JOIN roles r ON r.id = ur.role_id - WHERE r.slug = 'owner'", - [], - |row| row.get(0), - )?; - Ok(count > 0) -} - -/// Creates a user and assigns all roles from `user` up to `owner` (cumulative). -/// `password_hash` must already be hashed by the caller. -pub fn create_owner(conn: &Connection, username: &str, password_hash: &str) -> Result { - let user_uid = public_id("usr"); - conn.execute( - "INSERT INTO users (user_uid, username, email, password_hash, status, role, created_at) - VALUES (?1, ?2, NULL, ?3, 'active', 'admin', ?4)", - params![user_uid, username, password_hash, now_timestamp()], - )?; - let user_id = conn.last_insert_rowid(); - for slug in &["user", "admin", "owner"] { - let role_id: i64 = conn.query_row( - "SELECT id FROM roles WHERE slug = ?1", - [slug], - |row| row.get(0), - )?; - conn.execute( - "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at) - VALUES (?1, ?2, ?3)", - params![user_id, role_id, now_timestamp()], - )?; - } - Ok(user_id) -} - -pub fn get_user_by_username(conn: &Connection, username: &str) -> Result> { - conn.query_row( - "SELECT id, user_uid, username, password_hash, status FROM users WHERE username = ?1", - [username], - |row| { - Ok(AuthUserRecord { - id: row.get(0)?, - user_uid: row.get(1)?, - username: row.get(2)?, - password_hash: row.get(3)?, - status: row.get(4)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -/// Computes role_bits = ROLE_GUEST (1) | OR(assigned role bit values). -pub fn compute_role_bits(conn: &Connection, user_id: i64) -> Result { - let mut stmt = conn.prepare( - "SELECT (1 << r.bit_position) FROM user_roles ur - JOIN roles r ON r.id = ur.role_id - WHERE ur.user_id = ?1", - )?; - let bits: u32 = stmt - .query_map([user_id], |row| row.get::<_, i64>(0))? - .try_fold(1u32, |acc, val| val.map(|v| acc | v as u32))?; - Ok(bits) -} - -/// Returns a new session_uid (UUID). -pub fn create_session( - conn: &Connection, - user_id: i64, - role_bits: u32, - user_agent: Option<&str>, -) -> Result { - let session_uid = public_id("sess"); - let now = now_timestamp(); - let expires_at = chrono::Utc::now() - .checked_add_signed(chrono::Duration::days(30)) - .unwrap() - .to_rfc3339(); - conn.execute( - "INSERT INTO sessions (session_uid, user_id, role_bits, created_at, last_seen_at, expires_at, user_agent) - VALUES (?1, ?2, ?3, ?4, ?4, ?5, ?6)", - params![session_uid, user_id, role_bits as i64, now, expires_at, user_agent], - )?; - Ok(session_uid) -} - -/// Returns session if it exists, the user is active, and it has not expired. -pub fn get_session(conn: &Connection, session_uid: &str) -> Result> { - let now = now_timestamp(); - conn.query_row( - "SELECT s.user_id, s.role_bits, s.last_seen_at, s.session_uid - FROM sessions s - JOIN users u ON u.id = s.user_id - WHERE s.session_uid = ?1 - AND u.status = 'active' - AND s.expires_at > ?2", - params![session_uid, now], - |row| { - Ok(SessionRecord { - user_id: row.get(0)?, - role_bits: row.get::<_, i64>(1)? as u32, - last_seen_at: row.get(2)?, - session_uid: row.get(3)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -pub fn delete_session(conn: &Connection, session_uid: &str) -> Result<()> { - conn.execute("DELETE FROM sessions WHERE session_uid = ?1", [session_uid])?; - Ok(()) -} - -/// Updates last_seen_at and extends expires_at by 30 days. -pub fn touch_session(conn: &Connection, session_uid: &str) -> Result<()> { - let now = now_timestamp(); - let new_expires = chrono::Utc::now() - .checked_add_signed(chrono::Duration::days(30)) - .unwrap() - .to_rfc3339(); - conn.execute( - "UPDATE sessions SET last_seen_at = ?1, expires_at = ?2 WHERE session_uid = ?3", - params![now, new_expires, session_uid], - )?; - Ok(()) -} - -pub fn delete_expired_sessions(conn: &Connection) -> Result { - let now = now_timestamp(); - let n = conn.execute("DELETE FROM sessions WHERE expires_at <= ?1", [now])?; - Ok(n) -} - -/// Creates an API token. `token_hash` is SHA3-256 hex of the raw token. -pub fn create_api_token( - conn: &Connection, - user_id: i64, - token_hash: &str, - name: &str, -) -> Result { - let token_uid = public_id("tok"); - conn.execute( - "INSERT INTO api_tokens (token_uid, user_id, token_hash, name, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![token_uid, user_id, token_hash, name, now_timestamp()], - )?; - Ok(token_uid) -} - -/// Returns the user_id for a given token hash, if the token is valid and user is active. -pub fn get_user_for_token(conn: &Connection, token_hash: &str) -> Result> { - let now = now_timestamp(); - conn.query_row( - "SELECT t.user_id FROM api_tokens t - JOIN users u ON u.id = t.user_id - WHERE t.token_hash = ?1 - AND u.status = 'active' - AND (t.expires_at IS NULL OR t.expires_at > ?2)", - params![token_hash, now], - |row| row.get(0), - ) - .optional() - .map_err(Into::into) -} - -pub fn touch_token(conn: &Connection, token_uid: &str) -> Result<()> { - conn.execute( - "UPDATE api_tokens SET last_used_at = ?1 WHERE token_uid = ?2", - params![now_timestamp(), token_uid], - )?; - Ok(()) -} - -/// Returns true if the token was found and deleted (user_id must match). -pub fn delete_api_token(conn: &Connection, token_uid: &str, user_id: i64) -> Result { - let n = conn.execute( - "DELETE FROM api_tokens WHERE token_uid = ?1 AND user_id = ?2", - params![token_uid, user_id], - )?; - Ok(n > 0) -} - -pub fn list_user_tokens(conn: &Connection, user_id: i64) -> Result> { - let mut stmt = conn.prepare( - "SELECT token_uid, name, created_at, last_used_at - FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC", - )?; - let records = stmt - .query_map([user_id], |row| { - Ok(ApiTokenRecord { - token_uid: row.get(0)?, - name: row.get(1)?, - created_at: row.get(2)?, - last_used_at: row.get(3)?, - }) - })? - .collect::, _>>()?; - Ok(records) -} - -pub fn get_instance_settings(conn: &Connection) -> Result { - conn.query_row( - "SELECT public_index_enabled, public_entry_content_enabled, - public_archive_submission_enabled, default_entry_visibility - FROM instance_settings WHERE id = 1", - [], - |row| { - Ok(InstanceSettings { - public_index_enabled: row.get::<_, i64>(0)? != 0, - public_entry_content_enabled: row.get::<_, i64>(1)? != 0, - open_registration_enabled: row.get::<_, i64>(2)? != 0, - default_entry_visibility: row.get::<_, i64>(3)? as u32, - }) - }, - ) - .map_err(Into::into) -} - -pub fn update_instance_settings(conn: &Connection, settings: &InstanceSettings) -> Result<()> { - conn.execute( - "UPDATE instance_settings - SET public_index_enabled = ?1, - public_entry_content_enabled = ?2, - public_archive_submission_enabled = ?3, - default_entry_visibility = ?4 - WHERE id = 1", - params![ - settings.public_index_enabled as i64, - settings.public_entry_content_enabled as i64, - settings.open_registration_enabled as i64, - settings.default_entry_visibility as i64, - ], - )?; - Ok(()) -} - -pub fn get_user_password_hash(conn: &Connection, user_id: i64) -> Result> { - conn.query_row( - "SELECT password_hash FROM users WHERE id = ?1", - [user_id], - |r| r.get(0), - ) - .optional() - .map_err(Into::into) -} - -pub fn update_user_password(conn: &Connection, user_id: i64, new_hash: &str) -> Result<()> { - conn.execute( - "UPDATE users SET password_hash = ?1 WHERE id = ?2", - params![new_hash, user_id], - )?; - Ok(()) -} - -pub fn update_user_display_name(conn: &Connection, user_id: i64, display_name: Option<&str>) -> Result<()> { - conn.execute( - "UPDATE users SET display_name = ?1 WHERE id = ?2", - params![display_name, user_id], - )?; - Ok(()) -} - -pub fn get_user_display_name(conn: &Connection, user_id: i64) -> Result> { - conn.query_row( - "SELECT display_name FROM users WHERE id = ?1", - [user_id], - |r| r.get(0), - ) - .optional() - .map_err(Into::into) -} - -/// Deletes all sessions for a user. Called on ban or role change. -pub fn invalidate_user_sessions(conn: &Connection, user_id: i64) -> Result { - let n = conn.execute("DELETE FROM sessions WHERE user_id = ?1", [user_id])?; - Ok(n) -} - -/// Returns the integer id for a user_uid, or None if not found. -pub fn get_user_id_by_uid(conn: &Connection, user_uid: &str) -> Result> { - conn.query_row("SELECT id FROM users WHERE user_uid = ?1", [user_uid], |r| r.get(0)) - .optional() - .map_err(Into::into) -} - -/// Lists all users with their assigned roles and computed role_bits. -pub fn list_users(conn: &Connection) -> Result> { - let mut stmt = conn.prepare( - "SELECT id, user_uid, username, email, status, created_at FROM users ORDER BY created_at ASC", - )?; - let rows: Vec<(i64, String, String, Option, String, String)> = stmt - .query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?, r.get(5)?)))? - .collect::>()?; - - rows.into_iter() - .map(|(id, user_uid, username, email, status, created_at)| { - let role_bits = compute_role_bits(conn, id)?; - let mut rs = conn.prepare( - "SELECT r.slug FROM user_roles ur JOIN roles r ON r.id = ur.role_id - WHERE ur.user_id = ?1 ORDER BY r.level, r.bit_position", - )?; - let role_slugs: Vec = - rs.query_map([id], |r| r.get(0))?.collect::>()?; - Ok(UserSummary { user_uid, username, email, status, created_at, role_slugs, role_bits }) - }) - .collect() -} - -/// Gets a single user by user_uid with roles and role_bits. -pub fn get_user_by_uid(conn: &Connection, user_uid: &str) -> Result> { - let row = conn - .query_row( - "SELECT id, user_uid, username, email, status, created_at FROM users WHERE user_uid = ?1", - [user_uid], - |r| Ok((r.get::<_, i64>(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?, r.get(5)?)), - ) - .optional()?; - match row { - None => Ok(None), - Some((id, user_uid, username, email, status, created_at)) => { - let role_bits = compute_role_bits(conn, id)?; - let mut rs = conn.prepare( - "SELECT r.slug FROM user_roles ur JOIN roles r ON r.id = ur.role_id - WHERE ur.user_id = ?1 ORDER BY r.level, r.bit_position", - )?; - let role_slugs: Vec = - rs.query_map([id], |r| r.get(0))?.collect::>()?; - Ok(Some(UserSummary { user_uid, username, email, status, created_at, role_slugs, role_bits })) - } - } -} - -/// Creates a new user (admin-created account) and assigns the 'user' role. -/// Returns the new user_uid. -pub fn create_user( - conn: &Connection, - username: &str, - email: Option<&str>, - password_hash: &str, - created_by_user_id: i64, -) -> Result { - let user_uid = public_id("usr"); - conn.execute( - "INSERT INTO users (user_uid, username, email, password_hash, status, role, created_at) - VALUES (?1, ?2, ?3, ?4, 'active', 'user', ?5)", - params![user_uid, username, email, password_hash, now_timestamp()], - )?; - let user_id = conn.last_insert_rowid(); - let role_id: i64 = conn.query_row( - "SELECT id FROM roles WHERE slug = 'user'", - [], - |r| r.get(0), - )?; - conn.execute( - "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at, assigned_by_user_id) - VALUES (?1, ?2, ?3, ?4)", - params![user_id, role_id, now_timestamp(), created_by_user_id], - )?; - Ok(user_uid) -} - -/// Sets a user's status ('active' | 'disabled'). Invalidates sessions when disabling. -/// Returns true if the user was found. -pub fn set_user_status(conn: &Connection, user_uid: &str, status: &str) -> Result { - if status == "disabled" { - let id: Option = conn - .query_row("SELECT id FROM users WHERE user_uid = ?1", [user_uid], |r| r.get(0)) - .optional()?; - if let Some(id) = id { - invalidate_user_sessions(conn, id)?; - } - } - let n = conn.execute( - "UPDATE users SET status = ?1 WHERE user_uid = ?2", - params![status, user_uid], - )?; - Ok(n > 0) -} - -/// Assigns a role to a user (cumulative: also ensures 'user' for any non-guest role, -/// and 'admin' for 'owner'). Invalidates the user's sessions so changes take effect on re-login. -pub fn assign_role( - conn: &Connection, - target_user_id: i64, - role_slug: &str, - assigned_by_user_id: i64, -) -> Result<()> { - let role_id: i64 = conn - .query_row("SELECT id FROM roles WHERE slug = ?1", [role_slug], |r| r.get(0)) - .map_err(|_| anyhow::anyhow!("role '{}' not found", role_slug))?; - conn.execute( - "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at, assigned_by_user_id) - VALUES (?1, ?2, ?3, ?4)", - params![target_user_id, role_id, now_timestamp(), assigned_by_user_id], - )?; - // Cumulative: ensure 'user' whenever any non-guest role is assigned - if role_slug != "user" && role_slug != "guest" { - let uid: i64 = conn.query_row("SELECT id FROM roles WHERE slug = 'user'", [], |r| r.get(0))?; - conn.execute( - "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at, assigned_by_user_id) - VALUES (?1, ?2, ?3, ?4)", - params![target_user_id, uid, now_timestamp(), assigned_by_user_id], - )?; - } - // Also ensure 'admin' when assigning 'owner' - if role_slug == "owner" { - let aid: i64 = conn.query_row("SELECT id FROM roles WHERE slug = 'admin'", [], |r| r.get(0))?; - conn.execute( - "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at, assigned_by_user_id) - VALUES (?1, ?2, ?3, ?4)", - params![target_user_id, aid, now_timestamp(), assigned_by_user_id], - )?; - } - invalidate_user_sessions(conn, target_user_id)?; - Ok(()) -} - -/// Removes a role from a user. Guards: can't remove the only owner's 'owner' role. -/// Invalidates the user's sessions. -pub fn remove_role(conn: &Connection, target_user_id: i64, role_slug: &str) -> Result<()> { - if role_slug == "owner" { - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE r.slug = 'owner'", - [], - |r| r.get(0), - )?; - if count <= 1 { - anyhow::bail!("cannot remove the last owner"); - } - } - let role_id: i64 = conn - .query_row("SELECT id FROM roles WHERE slug = ?1", [role_slug], |r| r.get(0)) - .map_err(|_| anyhow::anyhow!("role '{}' not found", role_slug))?; - conn.execute( - "DELETE FROM user_roles WHERE user_id = ?1 AND role_id = ?2", - params![target_user_id, role_id], - )?; - invalidate_user_sessions(conn, target_user_id)?; - Ok(()) -} - -/// Lists all roles ordered by level then bit_position. -pub fn list_roles(conn: &Connection) -> Result> { - let mut stmt = conn.prepare( - "SELECT role_uid, slug, name, level, bit_position, is_builtin FROM roles - ORDER BY level, bit_position", - )?; - stmt.query_map([], |r| { - Ok(RoleRecord { - role_uid: r.get(0)?, - slug: r.get(1)?, - name: r.get(2)?, - level: r.get(3)?, - bit_position: r.get(4)?, - is_builtin: r.get::<_, i64>(5)? != 0, - }) - })? - .collect::>() - .map_err(Into::into) -} - -/// Creates a new custom role (level=2, bit_position = max existing + 1, min 4). -/// Returns the created RoleRecord. -pub fn create_custom_role(conn: &Connection, slug: &str, name: &str) -> Result { - if slug.is_empty() || !slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { - anyhow::bail!("role slug must be non-empty and contain only ASCII letters, digits, or hyphens"); - } - let next_bit: i64 = conn.query_row( - "SELECT COALESCE(MAX(bit_position) + 1, 4) FROM roles WHERE bit_position >= 4", - [], - |r| r.get(0), - )?; - if next_bit >= 32 { - anyhow::bail!("maximum number of custom roles reached"); - } - let role_uid = public_id("role"); - conn.execute( - "INSERT INTO roles (role_uid, slug, name, level, bit_position, is_builtin) - VALUES (?1, ?2, ?3, 2, ?4, 0)", - params![role_uid, slug, name, next_bit], - )?; - Ok(RoleRecord { - role_uid, - slug: slug.to_string(), - name: name.to_string(), - level: 2, - bit_position: next_bit, - is_builtin: false, - }) -} - pub fn ensure_default_user(conn: &Connection) -> Result { if let Some(id) = conn .query_row( @@ -975,71 +257,6 @@ pub fn ensure_default_user(conn: &Connection) -> Result { Ok(conn.last_insert_rowid()) } -/// Creates a pending capture job. Returns the new `job_uid`. -pub fn create_capture_job(conn: &Connection, archive_id: &str) -> Result { - let job_uid = public_id("job"); - let now = now_timestamp(); - conn.execute( - "INSERT INTO capture_jobs (job_uid, archive_id, run_uid, status, error_text, created_at, updated_at) - VALUES (?1, ?2, NULL, 'pending', NULL, ?3, ?3)", - rusqlite::params![job_uid, archive_id, now], - )?; - Ok(job_uid) -} - -/// Updates the status (and optionally run_uid / error_text) of a capture job. -pub fn update_capture_job_status( - conn: &Connection, - job_uid: &str, - status: &str, - run_uid: Option<&str>, - error_text: Option<&str>, -) -> Result<()> { - let now = now_timestamp(); - conn.execute( - "UPDATE capture_jobs SET status = ?1, run_uid = COALESCE(?2, run_uid), - error_text = ?3, updated_at = ?4 WHERE job_uid = ?5", - rusqlite::params![status, run_uid, error_text, now, job_uid], - )?; - Ok(()) -} - -/// Returns a capture job by uid. -pub fn get_capture_job(conn: &Connection, job_uid: &str) -> Result> { - conn.query_row( - "SELECT job_uid, archive_id, run_uid, status, error_text, created_at, updated_at - FROM capture_jobs WHERE job_uid = ?1", - [job_uid], - |row| { - Ok(CaptureJobRecord { - job_uid: row.get(0)?, - archive_id: row.get(1)?, - run_uid: row.get(2)?, - status: row.get(3)?, - error_text: row.get(4)?, - created_at: row.get(5)?, - updated_at: row.get(6)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -/// Marks all 'running' capture jobs as 'failed' with a restart message. -/// Called at server startup to clean up jobs interrupted by a previous shutdown. -pub fn fail_stalled_capture_jobs(conn: &Connection) -> Result { - let now = now_timestamp(); - let n = conn.execute( - "UPDATE capture_jobs SET status = 'failed', - error_text = 'interrupted by server restart', - updated_at = ?1 - WHERE status = 'running'", - [now], - )?; - Ok(n) -} - pub fn create_archive_run( conn: &Connection, created_by_user_id: i64, @@ -1214,26 +431,6 @@ pub fn upsert_blob(conn: &Connection, blob: &BlobRecord) -> Result { Ok(id) } -/// Returns the `BlobRecord` for the given SHA-256 hex digest, or `None` if not found. -pub fn get_blob_by_sha256(conn: &Connection, sha256: &str) -> Result> { - conn.query_row( - "SELECT sha256, byte_size, mime_type, extension, raw_relpath - FROM blobs WHERE sha256 = ?1", - [sha256], - |row| { - Ok(BlobRecord { - sha256: row.get(0)?, - byte_size: row.get(1)?, - mime_type: row.get(2)?, - extension: row.get(3)?, - raw_relpath: row.get(4)?, - }) - }, - ) - .optional() - .map_err(anyhow::Error::from) -} - pub fn create_archived_entry(conn: &Connection, entry: &NewEntry) -> Result { validate_visibility(&entry.visibility)?; let entry_uid = public_id("entry"); @@ -1277,11 +474,6 @@ pub fn create_archived_entry(conn: &Connection, entry: &NewEntry) -> Result Result<()> { Ok(()) } -/// Maps legacy visibility strings to collection_entries.visibility_bits. -/// 'public'→3 (guest|user), 'unlisted'→2 (user only), 'private'→0 (nobody). -pub fn visibility_to_bits(visibility: &str) -> u32 { - match visibility { - "public" => 3, - "unlisted" => 2, - _ => 0, - } -} - -/// Returns the id of the '_default_' collection, creating it if absent. -pub fn ensure_default_collection(conn: &Connection) -> Result { - let now = now_timestamp(); - conn.execute( - "INSERT OR IGNORE INTO collections (collection_uid, name, slug, default_visibility_bits, created_at) \ - VALUES ('coll_default', 'All Entries', '_default_', 2, ?1)", - [&now], - )?; - let id: i64 = conn.query_row( - "SELECT id FROM collections WHERE slug = '_default_'", - [], - |row| row.get(0), - )?; - Ok(id) -} - -/// Creates a new collection. Returns the created record. -pub fn create_collection( - conn: &Connection, - name: &str, - slug: &str, - default_visibility_bits: u32, -) -> Result { - if slug.is_empty() || slug.starts_with('_') { - anyhow::bail!("collection slug must be non-empty and not start with underscore"); - } - let collection_uid = public_id("coll"); - let now = now_timestamp(); - conn.execute( - "INSERT INTO collections (collection_uid, name, slug, default_visibility_bits, created_at) \ - VALUES (?1, ?2, ?3, ?4, ?5)", - params![collection_uid, name, slug, default_visibility_bits as i64, now], - )?; - let id = conn.last_insert_rowid(); - Ok(CollectionRecord { - id, - collection_uid, - name: name.to_string(), - slug: slug.to_string(), - default_visibility_bits, - created_at: now, - }) -} - -/// Lists all collections ordered by creation date. -pub fn list_collections(conn: &Connection) -> Result> { - let mut stmt = conn.prepare( - "SELECT id, collection_uid, name, slug, default_visibility_bits, created_at \ - FROM collections ORDER BY created_at ASC", - )?; - stmt.query_map([], |row| { - Ok(CollectionRecord { - id: row.get(0)?, - collection_uid: row.get(1)?, - name: row.get(2)?, - slug: row.get(3)?, - default_visibility_bits: row.get::<_, i64>(4)? as u32, - created_at: row.get(5)?, - }) - })? - .collect::>() - .map_err(Into::into) -} - -/// Returns a collection by its uid, or None if not found. -pub fn get_collection_by_uid( - conn: &Connection, - uid: &str, -) -> Result> { - conn.query_row( - "SELECT id, collection_uid, name, slug, default_visibility_bits, created_at \ - FROM collections WHERE collection_uid = ?1", - [uid], - |row| { - Ok(CollectionRecord { - id: row.get(0)?, - collection_uid: row.get(1)?, - name: row.get(2)?, - slug: row.get(3)?, - default_visibility_bits: row.get::<_, i64>(4)? as u32, - created_at: row.get(5)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -/// Adds an entry to a collection with given visibility_bits. Idempotent (INSERT OR IGNORE). -pub fn add_entry_to_collection( - conn: &Connection, - collection_id: i64, - entry_id: i64, - visibility_bits: u32, -) -> Result<()> { - let now = now_timestamp(); - conn.execute( - "INSERT OR IGNORE INTO collection_entries (collection_id, entry_id, visibility_bits, added_at) \ - VALUES (?1, ?2, ?3, ?4)", - params![collection_id, entry_id, visibility_bits as i64, now], - )?; - Ok(()) -} - -/// Updates the visibility_bits of an entry in a collection. Returns true if updated. -pub fn update_collection_entry_visibility( - conn: &Connection, - collection_id: i64, - entry_id: i64, - visibility_bits: u32, -) -> Result { - let n = conn.execute( - "UPDATE collection_entries SET visibility_bits = ?1 \ - WHERE collection_id = ?2 AND entry_id = ?3", - params![visibility_bits as i64, collection_id, entry_id], - )?; - Ok(n > 0) -} - -/// Removes an entry from a collection. Returns true if removed. -pub fn remove_entry_from_collection( - conn: &Connection, - collection_id: i64, - entry_id: i64, -) -> Result { - let n = conn.execute( - "DELETE FROM collection_entries WHERE collection_id = ?1 AND entry_id = ?2", - params![collection_id, entry_id], - )?; - Ok(n > 0) -} - -/// Returns (collection_id, collection_uid, visibility_bits) for all collections containing an entry. -pub fn get_entry_collection_memberships( - conn: &Connection, - entry_id: i64, -) -> Result> { - let mut stmt = conn.prepare( - "SELECT ce.collection_id, c.collection_uid, ce.visibility_bits \ - FROM collection_entries ce \ - JOIN collections c ON c.id = ce.collection_id \ - WHERE ce.entry_id = ?1", - )?; - stmt.query_map([entry_id], |row| { - Ok((row.get(0)?, row.get(1)?, row.get::<_, i64>(2)? as u32)) - })? - .collect::>() - .map_err(Into::into) -} - -/// Renames a collection and/or updates its default_visibility_bits. -/// Returns true if updated, false if not found. -/// Refuses to rename the '_default_' collection. -pub fn update_collection( - conn: &Connection, - collection_uid: &str, - new_name: Option<&str>, - new_visibility_bits: Option, -) -> Result { - let coll = get_collection_by_uid(conn, collection_uid)?; - let Some(coll) = coll else { return Ok(false) }; - if coll.slug == "_default_" { - anyhow::bail!("cannot modify the default collection"); - } - let name = new_name.unwrap_or(&coll.name); - let vbits = new_visibility_bits.unwrap_or(coll.default_visibility_bits); - conn.execute( - "UPDATE collections SET name = ?1, default_visibility_bits = ?2 WHERE id = ?3", - params![name, vbits as i64, coll.id], - )?; - Ok(true) -} - -/// Deletes a collection and cascades to collection_entries. -/// Returns true if deleted, false if not found. -/// Refuses to delete the '_default_' collection. -pub fn delete_collection( - conn: &Connection, - collection_uid: &str, -) -> Result { - let coll = get_collection_by_uid(conn, collection_uid)?; - let Some(coll) = coll else { return Ok(false) }; - if coll.slug == "_default_" { - anyhow::bail!("cannot delete the default collection"); - } - conn.execute("DELETE FROM collections WHERE id = ?1", [coll.id])?; - Ok(true) -} - fn run_id_for_item(conn: &Connection, item_id: i64) -> Result { let run_id = conn.query_row( "SELECT run_id FROM archive_run_items WHERE id = ?1", @@ -2108,218 +1101,4 @@ mod tests { ); assert_eq!(entry_count_for_tag_path(&conn, "/sciences").unwrap(), 1); } - - #[test] - fn get_blob_by_sha256_round_trips() { - let conn = conn(); - let blob = BlobRecord { - sha256: "deadbeef01234567".repeat(4), // 64-char hex string - byte_size: 1234, - mime_type: Some("font/woff2".to_string()), - extension: Some("woff2".to_string()), - raw_relpath: "raw/d/e/deadbeef.woff2".to_string(), - }; - upsert_blob(&conn, &blob).unwrap(); - - let found = get_blob_by_sha256(&conn, &blob.sha256).unwrap(); - assert!(found.is_some(), "should find the blob we just upserted"); - let found = found.unwrap(); - assert_eq!(found.sha256, blob.sha256); - assert_eq!(found.byte_size, 1234); - assert_eq!(found.mime_type, Some("font/woff2".to_string())); - assert_eq!(found.raw_relpath, blob.raw_relpath); - } - - #[test] - fn get_blob_by_sha256_returns_none_for_unknown() { - let conn = conn(); - let result = get_blob_by_sha256(&conn, "0000000000000000000000000000000000000000000000000000000000000000").unwrap(); - assert!(result.is_none()); - } - - #[test] - fn auth_schema_seeds_builtin_roles() { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - let count: i64 = conn - .query_row("SELECT COUNT(*) FROM roles WHERE is_builtin = 1", [], |r| r.get(0)) - .unwrap(); - assert_eq!(count, 4); - let owner_bits: i64 = conn - .query_row("SELECT bit_position FROM roles WHERE slug = 'owner'", [], |r| r.get(0)) - .unwrap(); - assert_eq!(owner_bits, 3); - } - - #[test] - fn auth_schema_is_idempotent() { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - initialize_auth_schema(&conn).unwrap(); - } - - fn make_auth_conn() -> Connection { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - conn - } - - #[test] - fn ensure_owner_exists_returns_false_when_no_owner() { - let conn = make_auth_conn(); - assert!(!ensure_owner_exists(&conn).unwrap()); - } - - #[test] - fn create_owner_then_ensure_returns_true() { - let conn = make_auth_conn(); - create_owner(&conn, "alice", "hashed_pw").unwrap(); - assert!(ensure_owner_exists(&conn).unwrap()); - } - - #[test] - fn create_owner_assigns_cumulative_roles() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "hashed_pw").unwrap(); - let bits = compute_role_bits(&conn, user_id).unwrap(); - assert_eq!(bits, 15u32); - } - - #[test] - fn get_user_by_username_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_user_by_username(&conn, "nobody").unwrap().is_none()); - } - - #[test] - fn create_and_get_session() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - let uid = create_session(&conn, user_id, 15, None).unwrap(); - let sess = get_session(&conn, &uid).unwrap().unwrap(); - assert_eq!(sess.user_id, user_id); - assert_eq!(sess.role_bits, 15); - } - - #[test] - fn get_session_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_session(&conn, "nonexistent").unwrap().is_none()); - } - - #[test] - fn delete_session_removes_it() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - let uid = create_session(&conn, user_id, 15, None).unwrap(); - delete_session(&conn, &uid).unwrap(); - assert!(get_session(&conn, &uid).unwrap().is_none()); - } - - #[test] - fn token_hash_round_trips() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - create_api_token(&conn, user_id, "hash_abc", "My Token").unwrap(); - let found_id = get_user_for_token(&conn, "hash_abc").unwrap(); - assert_eq!(found_id, Some(user_id)); - } - - #[test] - fn get_user_for_token_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_user_for_token(&conn, "unknown").unwrap().is_none()); - } - - #[test] - fn capture_job_create_and_get() { - let conn = conn(); - let job_uid = create_capture_job(&conn, "personal").unwrap(); - let job = get_capture_job(&conn, &job_uid).unwrap().unwrap(); - assert_eq!(job.status, "pending"); - assert_eq!(job.archive_id, "personal"); - assert!(job.run_uid.is_none()); - } - - #[test] - fn capture_job_status_transitions() { - let conn = conn(); - let job_uid = create_capture_job(&conn, "test").unwrap(); - update_capture_job_status(&conn, &job_uid, "running", None, None).unwrap(); - update_capture_job_status(&conn, &job_uid, "completed", Some("run_abc"), None).unwrap(); - let job = get_capture_job(&conn, &job_uid).unwrap().unwrap(); - assert_eq!(job.status, "completed"); - assert_eq!(job.run_uid.as_deref(), Some("run_abc")); - } - - #[test] - fn fail_stalled_jobs_on_restart() { - let conn = conn(); - let uid = create_capture_job(&conn, "test").unwrap(); - update_capture_job_status(&conn, &uid, "running", None, None).unwrap(); - let n = fail_stalled_capture_jobs(&conn).unwrap(); - assert_eq!(n, 1); - let job = get_capture_job(&conn, &uid).unwrap().unwrap(); - assert_eq!(job.status, "failed"); - assert!(job.error_text.as_deref().unwrap().contains("interrupted")); - } - - fn make_auth_conn_for_mgmt() -> Connection { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - conn - } - - #[test] - fn user_create_and_list() { - let conn = make_auth_conn_for_mgmt(); - let owner_id = create_owner(&conn, "owner", "hash").unwrap(); - let uid = create_user(&conn, "alice", Some("alice@example.com"), "hash2", owner_id).unwrap(); - let users = list_users(&conn).unwrap(); - assert_eq!(users.len(), 2); - let alice = users.iter().find(|u| u.username == "alice").unwrap(); - assert_eq!(alice.user_uid, uid); - assert_eq!(alice.status, "active"); - assert!(alice.role_slugs.contains(&"user".to_string())); - } - - #[test] - fn set_status_disables_user_and_kills_sessions() { - let conn = make_auth_conn_for_mgmt(); - let owner_id = create_owner(&conn, "owner", "hash").unwrap(); - let uid = create_user(&conn, "bob", None, "hash", owner_id).unwrap(); - let bob_id: i64 = conn.query_row("SELECT id FROM users WHERE user_uid = ?1", [&uid], |r| r.get(0)).unwrap(); - create_session(&conn, bob_id, 3, None).unwrap(); - set_user_status(&conn, &uid, "disabled").unwrap(); - let sess_count: i64 = conn.query_row("SELECT COUNT(*) FROM sessions WHERE user_id = ?1", [bob_id], |r| r.get(0)).unwrap(); - assert_eq!(sess_count, 0, "sessions should be cleared on disable"); - let u = get_user_by_uid(&conn, &uid).unwrap().unwrap(); - assert_eq!(u.status, "disabled"); - } - - #[test] - fn assign_and_remove_role() { - let conn = make_auth_conn_for_mgmt(); - let owner_id = create_owner(&conn, "owner", "hash").unwrap(); - let uid = create_user(&conn, "carol", None, "hash", owner_id).unwrap(); - let carol_id = get_user_id_by_uid(&conn, &uid).unwrap().unwrap(); - let bits_before = compute_role_bits(&conn, carol_id).unwrap(); - assign_role(&conn, carol_id, "admin", owner_id).unwrap(); - let bits_after = compute_role_bits(&conn, carol_id).unwrap(); - assert!(bits_after & 4 != 0, "admin bit should be set"); - assert!(bits_after > bits_before); - remove_role(&conn, carol_id, "admin").unwrap(); - let bits_final = compute_role_bits(&conn, carol_id).unwrap(); - assert!(bits_final & 4 == 0, "admin bit should be cleared"); - } - - #[test] - fn custom_role_gets_next_bit_position() { - let conn = make_auth_conn_for_mgmt(); - let r1 = create_custom_role(&conn, "moderator", "Moderator").unwrap(); - assert_eq!(r1.bit_position, 4); - let r2 = create_custom_role(&conn, "helper", "Helper").unwrap(); - assert_eq!(r2.bit_position, 5); - assert_eq!(r2.level, 2); - } } diff --git a/crates/archivr-core/src/downloader/font_extractor.rs b/crates/archivr-core/src/downloader/font_extractor.rs deleted file mode 100644 index 3ae8ecc..0000000 --- a/crates/archivr-core/src/downloader/font_extractor.rs +++ /dev/null @@ -1,187 +0,0 @@ -use anyhow::Result; -use base64::engine::general_purpose::STANDARD as B64; -use base64::Engine as _; -use regex::Regex; -use std::{fs, path::Path}; - -use crate::hash::hash_bytes; - -#[derive(Debug, Clone)] -pub struct ExtractedFont { - /// SHA3-256 hex of the raw font bytes. - pub sha256: String, - /// File extension including leading dot, e.g. `".woff2"`. - pub ext: String, - /// Size in bytes of the decoded font. - pub byte_size: i64, - /// Store-relative path, e.g. `"raw/a/b/abc...woff2"`. - pub raw_relpath: String, -} - -/// Scans `html` for `@font-face` `src` attributes containing `data:font/...;base64,...` -/// URLs, decodes each font, stores it in `{store_path}/raw/`, and rewrites the src -/// to `/api/archives/{archive_id}/blobs/{sha256}`. -/// -/// Returns `(rewritten_html, Vec)`. Each occurrence is reported -/// individually; the caller deduplicates via `upsert_blob`. -pub fn extract_and_rewrite( - html: &str, - store_path: &Path, - archive_id: &str, -) -> Result<(String, Vec)> { - // Matches: url(data:font/MIME;base64,DATA) or url("data:font/MIME;base64,DATA") - // Also matches data:application/font-... for older MIME types. - // Group 2 captures the full MIME type (e.g. "font/woff2"), group 3 the base64 payload. - let re = Regex::new( - r#"url\("?(data:((?:font|application)/[^;]+);base64,([A-Za-z0-9+/=]+))"?\)"#, - )?; - - let mut fonts = Vec::new(); - let rewritten = re.replace_all(html, |caps: ®ex::Captures| { - let mime = caps[2].to_ascii_lowercase(); - let b64_data = &caps[3]; - - let ext = match mime_to_ext(&mime) { - Some(e) => e, - None => return caps[0].to_string(), // unknown MIME — leave as-is - }; - - let bytes = match B64.decode(b64_data) { - Ok(b) => b, - Err(_) => return caps[0].to_string(), // corrupt base64 — leave as-is - }; - - let sha256 = hash_bytes(&bytes); - let raw_relpath = font_raw_relpath(&sha256, ext); - let abs_path = store_path.join(&raw_relpath); - - if !abs_path.exists() { - if let Some(parent) = abs_path.parent() { - let _ = fs::create_dir_all(parent); - } - if fs::write(&abs_path, &bytes).is_err() { - return caps[0].to_string(); // write failed — leave as-is - } - } - - fonts.push(ExtractedFont { - sha256: sha256.clone(), - ext: ext.to_string(), - byte_size: bytes.len() as i64, - raw_relpath, - }); - - format!("url(/api/archives/{archive_id}/blobs/{sha256})") - }); - - Ok((rewritten.into_owned(), fonts)) -} - -fn font_raw_relpath(sha256: &str, ext: &str) -> String { - let mut chars = sha256.chars(); - let a = chars.next().unwrap_or('0'); - let b = chars.next().unwrap_or('0'); - format!("raw/{a}/{b}/{sha256}{ext}") -} - -fn mime_to_ext(mime: &str) -> Option<&'static str> { - match mime { - "font/woff2" | "application/font-woff2" => Some(".woff2"), - "font/woff" | "application/font-woff" => Some(".woff"), - "font/ttf" | "font/truetype" | "application/x-font-truetype" => Some(".ttf"), - "font/otf" | "font/opentype" | "application/x-font-opentype" => Some(".otf"), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn html_with_woff2_font() -> String { - let font_b64 = B64.encode(b"WOFF2FAKEDATA"); - format!( - "\ -

Hello

" - ) - } - - #[test] - fn replaces_data_url_with_api_url() { - let tmp = tempfile::tempdir().unwrap(); - std::fs::create_dir_all(tmp.path().join("raw")).unwrap(); - - let html = html_with_woff2_font(); - let (rewritten, fonts) = extract_and_rewrite(&html, tmp.path(), "myarchive").unwrap(); - - assert_eq!(fonts.len(), 1, "should extract one font"); - let font = &fonts[0]; - assert_eq!(font.ext, ".woff2"); - assert_eq!(font.byte_size, b"WOFF2FAKEDATA".len() as i64); - assert!(!font.sha256.is_empty()); - assert!(!rewritten.contains("data:font"), "data URL must be gone from HTML"); - assert!( - rewritten.contains(&format!("/api/archives/myarchive/blobs/{}", font.sha256)), - "rewritten HTML must contain the local API URL" - ); - } - - #[test] - fn font_file_is_written_to_raw_store() { - let tmp = tempfile::tempdir().unwrap(); - std::fs::create_dir_all(tmp.path().join("raw")).unwrap(); - - let html = html_with_woff2_font(); - let (_, fonts) = extract_and_rewrite(&html, tmp.path(), "myarchive").unwrap(); - - let font = &fonts[0]; - let raw_path = tmp.path().join(&font.raw_relpath); - assert!(raw_path.exists(), "font file must exist at raw_relpath"); - let written = std::fs::read(&raw_path).unwrap(); - assert_eq!(written, b"WOFF2FAKEDATA"); - } - - #[test] - fn deduplicates_identical_fonts() { - let tmp = tempfile::tempdir().unwrap(); - std::fs::create_dir_all(tmp.path().join("raw")).unwrap(); - - let font_b64 = B64.encode(b"SAMEDATA"); - let html = format!( - "" - ); - let (_, fonts) = extract_and_rewrite(&html, tmp.path(), "x").unwrap(); - assert_eq!(fonts.len(), 2); - assert_eq!(fonts[0].sha256, fonts[1].sha256); - let raw_path = tmp.path().join(&fonts[0].raw_relpath); - assert!(raw_path.exists()); - } - - #[test] - fn html_without_font_data_urls_is_unchanged() { - let tmp = tempfile::tempdir().unwrap(); - std::fs::create_dir_all(tmp.path().join("raw")).unwrap(); - - let html = "

no fonts

"; - let (rewritten, fonts) = extract_and_rewrite(html, tmp.path(), "x").unwrap(); - assert_eq!(fonts.len(), 0); - assert_eq!(rewritten, html); - } - - #[test] - fn ttf_font_gets_correct_extension() { - let tmp = tempfile::tempdir().unwrap(); - std::fs::create_dir_all(tmp.path().join("raw")).unwrap(); - - let font_b64 = B64.encode(b"TTFDATA"); - let html = format!( - "" - ); - let (_, fonts) = extract_and_rewrite(&html, tmp.path(), "x").unwrap(); - assert_eq!(fonts[0].ext, ".ttf"); - } -} diff --git a/crates/archivr-core/src/downloader/mod.rs b/crates/archivr-core/src/downloader/mod.rs index 6c8d62a..d8c3911 100644 --- a/crates/archivr-core/src/downloader/mod.rs +++ b/crates/archivr-core/src/downloader/mod.rs @@ -5,4 +5,3 @@ pub mod ytdlp; pub mod metadata; pub mod http; pub mod singlefile; -pub mod font_extractor; diff --git a/crates/archivr-core/src/downloader/singlefile.rs b/crates/archivr-core/src/downloader/singlefile.rs index d50d554..5d24712 100644 --- a/crates/archivr-core/src/downloader/singlefile.rs +++ b/crates/archivr-core/src/downloader/singlefile.rs @@ -63,16 +63,6 @@ fn save_with( ) .context("failed to write single-file user script")?; - // Chrome's user-data-dir for this capture. Required alongside - // --disable-web-security — newer Chromium silently ignores that flag - // without a writable user-data-dir. Using a subdirectory of temp_dir - // keeps it isolated and it gets cleaned up with the rest of the temp dir. - let chrome_data_dir = temp_dir.join("chrome-data"); - let browser_args = format!( - "[\"--disable-web-security\",\"--user-data-dir={}\"]", - chrome_data_dir.display() - ); - let out = Command::new(single_file) .arg(url) .arg(&out_file) @@ -82,11 +72,6 @@ fn save_with( // Extra delay after networkidle2: Cloudflare Fonts injects @font-face // CSS after HTML parse, so the font hook needs more time to see it. .arg("--browser-wait-delay=2000") - // Realistic UA: some origins block headless Chrome's default UA string. - .arg("--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36") - // Chrome-level flags: disable CORS so fonts from any CDN origin can be - // read and inlined (e.g. fonts.gstatic.com without ACAO:*). - .arg(format!("--browser-args={browser_args}")) // Preserve all CSS: single-file's defaults strip rules it considers // "unused" (breaks CSS nesting) and remove @media blocks that don't // match the capture viewport (breaks responsive layout). diff --git a/crates/archivr-core/src/hash.rs b/crates/archivr-core/src/hash.rs index 949a86b..cbf1194 100644 --- a/crates/archivr-core/src/hash.rs +++ b/crates/archivr-core/src/hash.rs @@ -1,56 +1,17 @@ use anyhow::Result; use sha3::{Digest, Sha3_256}; -use std::path::Path; +use std::{fs::File, io::Read, path::Path}; -/// Computes the SHA3-256 hex digest of `data` directly on raw bytes. -pub fn hash_bytes(data: &[u8]) -> String { - let mut hasher = Sha3_256::new(); - hasher.update(data); - hex::encode(hasher.finalize()) -} - -/// Reads `path` fully and returns its SHA3-256 hex digest. -/// Uses raw bytes — correct for both text and binary files. pub fn hash_file(path: &Path) -> Result { - let buf = std::fs::read(path)?; - Ok(hash_bytes(&buf)) + let mut file = File::open(path)?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf)?; + Ok(hash(String::from_utf8_lossy(&buf).to_string())) } -/// Hashes an arbitrary string (used for legacy callers; prefer `hash_bytes`). -pub fn hash(s: String) -> String { - hash_bytes(s.as_bytes()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hash_bytes_and_hash_file_agree_on_same_data() { - let data = b"hello archivr fonts"; - let tmp = tempfile::NamedTempFile::new().unwrap(); - std::fs::write(tmp.path(), data).unwrap(); - assert_eq!(hash_bytes(data), hash_file(tmp.path()).unwrap()); - } - - #[test] - fn hash_bytes_is_stable() { - let h = hash_bytes(b"archivr"); - assert_eq!(h.len(), 64); - assert!(h.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn hash_bytes_differs_from_hash_of_lossy_utf8_for_binary_data() { - let binary = b"\xff\xfe\xfd"; - let direct = hash_bytes(binary); - let lossy = String::from_utf8_lossy(binary).to_string(); - let via_old_path = { - use sha3::{Digest, Sha3_256}; - let mut h = Sha3_256::new(); - h.update(lossy.as_bytes()); - hex::encode(h.finalize()) - }; - assert_ne!(direct, via_old_path); - } +pub fn hash(path: String) -> String { + let mut hasher = Sha3_256::new(); + hasher.update(path.as_bytes()); + let result = hasher.finalize(); + hex::encode(result) } diff --git a/crates/archivr-server/Cargo.toml b/crates/archivr-server/Cargo.toml index b647f08..4c3548e 100644 --- a/crates/archivr-server/Cargo.toml +++ b/crates/archivr-server/Cargo.toml @@ -12,14 +12,8 @@ tokio.workspace = true toml.workspace = true tower.workspace = true tower-http.workspace = true -argon2.workspace = true -rand.workspace = true -axum-extra.workspace = true -chrono.workspace = true -base64.workspace = true -serde_json.workspace = true -rusqlite.workspace = true [dev-dependencies] tempfile.workspace = true tower.workspace = true +serde_json.workspace = true diff --git a/crates/archivr-server/src/auth.rs b/crates/archivr-server/src/auth.rs deleted file mode 100644 index 195e2b0..0000000 --- a/crates/archivr-server/src/auth.rs +++ /dev/null @@ -1,127 +0,0 @@ -use anyhow::Result; -use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; -use argon2::password_hash::{SaltString, rand_core::OsRng}; -use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; -use axum_extra::extract::CookieJar; -use rand::RngCore; - -use crate::routes::{ApiError, AppState}; -use archivr_core::database; - -// Role bit constants -pub const ROLE_GUEST: u32 = 1; // bit 0 -pub const ROLE_USER: u32 = 2; // bit 1 -pub const ROLE_ADMIN: u32 = 4; // bit 2 -pub const ROLE_OWNER: u32 = 8; // bit 3 - -#[derive(Clone, Debug)] -pub enum AuthUser { - Guest, - Authenticated { user_id: i64, role_bits: u32 }, -} - -impl AuthUser { - pub fn require_auth(&self) -> Result<(i64, u32), ApiError> { - match self { - AuthUser::Authenticated { user_id, role_bits } => Ok((*user_id, *role_bits)), - AuthUser::Guest => Err(ApiError::unauthorized("login required")), - } - } - - pub fn require_role(&self, bit: u32) -> Result<(), ApiError> { - match self { - AuthUser::Authenticated { role_bits, .. } if role_bits & bit != 0 => Ok(()), - AuthUser::Authenticated { .. } => Err(ApiError::forbidden("insufficient permissions")), - AuthUser::Guest => Err(ApiError::unauthorized("login required")), - } - } - - pub fn has_role(&self, bit: u32) -> bool { - matches!(self, AuthUser::Authenticated { role_bits, .. } if role_bits & bit != 0) - } -} - -#[async_trait] -impl FromRequestParts for AuthUser { - type Rejection = std::convert::Infallible; - - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - let auth_db_path = state.auth_db_path.as_ref(); - - // 1. Try session cookie - let jar = CookieJar::from_headers(&parts.headers); - if let Some(cookie) = jar.get("session") { - let session_uid = cookie.value().to_string(); - if let Ok(conn) = database::open_auth_db(auth_db_path) { - if let Ok(Some(session)) = database::get_session(&conn, &session_uid) { - // Conditional touch: only update if >60s since last_seen_at - let should_touch = chrono::DateTime::parse_from_rfc3339(&session.last_seen_at) - .map(|last| { - chrono::Utc::now() - last.with_timezone(&chrono::Utc) - > chrono::Duration::seconds(60) - }) - .unwrap_or(true); - if should_touch { - let _ = database::touch_session(&conn, &session_uid); - } - return Ok(AuthUser::Authenticated { - user_id: session.user_id, - role_bits: session.role_bits, - }); - } - } - } - - // 2. Try Bearer token - if let Some(auth_header) = parts.headers.get("Authorization") { - if let Ok(header_str) = auth_header.to_str() { - if let Some(raw_token) = header_str.strip_prefix("Bearer ") { - let token_hash = hash_token(raw_token); - if let Ok(conn) = database::open_auth_db(auth_db_path) { - if let Ok(Some(user_id)) = database::get_user_for_token(&conn, &token_hash) { - if let Ok(role_bits) = database::compute_role_bits(&conn, user_id) { - return Ok(AuthUser::Authenticated { user_id, role_bits }); - } - } - } - } - } - } - - Ok(AuthUser::Guest) - } -} - -// Password helpers - -pub fn hash_password(password: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); - let hash = Argon2::default() - .hash_password(password.as_bytes(), &salt) - .map_err(|e| anyhow::anyhow!("password hashing failed: {e}"))? - .to_string(); - Ok(hash) -} - -pub fn verify_password(password: &str, hash: &str) -> Result { - let parsed = PasswordHash::new(hash) - .map_err(|e| anyhow::anyhow!("invalid password hash: {e}"))?; - Ok(Argon2::default() - .verify_password(password.as_bytes(), &parsed) - .is_ok()) -} - -// Token helpers - -pub fn generate_token() -> String { - let mut bytes = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut bytes); - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bytes) -} - -pub fn hash_token(raw: &str) -> String { - archivr_core::hash::hash_bytes(raw.as_bytes()) -} diff --git a/crates/archivr-server/src/main.rs b/crates/archivr-server/src/main.rs index ab7366b..47f9532 100644 --- a/crates/archivr-server/src/main.rs +++ b/crates/archivr-server/src/main.rs @@ -1,4 +1,3 @@ -mod auth; mod registry; mod routes; @@ -13,45 +12,10 @@ async fn main() -> Result<()> { .nth(1) .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("archivr-server.toml")); - let registry = registry::load_registry(&config_path)?; + let app = routes::app(registry.clone()); - // Auth DB lives next to the config file unless overridden in the TOML. - let auth_db_path = registry.auth_db_path.clone().unwrap_or_else(|| { - config_path - .parent() - .unwrap_or_else(|| std::path::Path::new(".")) - .join("archivr-auth.sqlite") - }); - - let app = routes::app(registry.clone(), auth_db_path.clone()); - - // On startup, mark any jobs that were 'running' when the server last stopped as 'failed'. - for archive in ®istry.archives { - if let Ok(conn) = archivr_core::database::open_or_initialize(&archive.archive_path) { - match archivr_core::database::fail_stalled_capture_jobs(&conn) { - Ok(n) if n > 0 => eprintln!("info: marked {n} stalled capture job(s) as failed in '{}'", archive.id), - Err(e) => eprintln!("warn: stalled job cleanup failed for '{}': {e:#}", archive.id), - _ => {} - } - } - } - - // Spawn session cleanup: runs at startup and every 24h. - let cleanup_auth_path = auth_db_path.clone(); - tokio::spawn(async move { - loop { - if let Ok(conn) = archivr_core::database::open_auth_db(&cleanup_auth_path) { - match archivr_core::database::delete_expired_sessions(&conn) { - Ok(n) if n > 0 => eprintln!("info: cleaned up {n} expired sessions"), - Err(e) => eprintln!("warn: session cleanup failed: {e:#}"), - _ => {} - } - } - tokio::time::sleep(tokio::time::Duration::from_secs(24 * 60 * 60)).await; - } - }); - + // Bind address priority: ARCHIVR_BIND env var > TOML bind field > default loopback. let bind_str = std::env::var("ARCHIVR_BIND") .ok() .or_else(|| registry.bind.clone()) @@ -61,6 +25,15 @@ async fn main() -> Result<()> { .parse() .with_context(|| format!("invalid bind address: {bind_str}"))?; + // Warn when the server is reachable beyond localhost — it has no authentication. + if !addr.ip().is_loopback() { + eprintln!( + "warn: archivr-server is bound to {addr} — \ + this server has no authentication. \ + Only expose it on a trusted network." + ); + } + let listener = tokio::net::TcpListener::bind(addr).await?; println!("archivr-server listening on http://{addr}"); axum::serve(listener, app).await?; diff --git a/crates/archivr-server/src/registry.rs b/crates/archivr-server/src/registry.rs index f58fa04..a9fef6c 100644 --- a/crates/archivr-server/src/registry.rs +++ b/crates/archivr-server/src/registry.rs @@ -16,13 +16,10 @@ pub struct MountedArchive { pub struct ServerRegistry { #[serde(default)] pub archives: Vec, - /// Optional bind address. Defaults to `127.0.0.1:8080`. + /// Optional bind address for the server. Defaults to `127.0.0.1:8080`. + /// Set this to `0.0.0.0:8080` only on trusted networks — the server has no authentication. #[serde(default)] pub bind: Option, - /// Path to the server-level auth database. - /// Defaults to `archivr-auth.sqlite` in the same directory as the config file. - #[serde(default)] - pub auth_db_path: Option, } pub fn load_registry(path: &Path) -> Result { @@ -46,16 +43,19 @@ pub fn save_registry(path: &Path, registry: &ServerRegistry) -> Result<()> { } pub fn validate_registry(registry: &ServerRegistry) -> Result<()> { - let mut seen_ids = std::collections::HashSet::new(); + let mut ids = std::collections::HashSet::new(); for archive in ®istry.archives { if archive.id.trim().is_empty() { bail!("archive id must not be empty"); } - if !seen_ids.insert(archive.id.clone()) { + if !ids.insert(archive.id.as_str()) { bail!("duplicate archive id: {}", archive.id); } - if archive.label.trim().is_empty() { - bail!("archive label must not be empty for id={}", archive.id); + if !archive.archive_path.ends_with(".archivr") { + bail!( + "mounted archive path must point at a .archivr directory: {}", + archive.archive_path.display() + ); } } Ok(()) @@ -84,7 +84,6 @@ mod tests { archive_path: archive_path.clone(), }], bind: None, - auth_db_path: None, }; let path = temp.path().join("server.toml"); save_registry(&path, ®istry).unwrap(); @@ -110,7 +109,6 @@ mod tests { }, ], bind: None, - auth_db_path: None, }; let err = validate_registry(®istry).unwrap_err().to_string(); @@ -138,7 +136,6 @@ mod tests { let registry = ServerRegistry { archives: vec![], bind: Some("0.0.0.0:8080".to_string()), - auth_db_path: None, }; // validate_registry does not reject non-loopback bind — that's main's concern. assert!(validate_registry(®istry).is_ok()); diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index 3ae5804..3441f5e 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -1,23 +1,26 @@ -// ── Security Boundary ────────────────────────────────────────────────────────────────── -// setup_guard middleware returns 503 for all non-auth routes until POST /api/auth/setup -// creates the owner account. +// ── Security Boundary ──────────────────────────────────────────────────────── +// All routes are currently trusted-local: no authentication or authorization +// middleware is applied. The server is designed to bind on 127.0.0.1 only. // -// Route protection tiers: -// STATIC — no auth: GET /, GET /assets/* -// PUBLIC_READ — no auth (visibility filtering deferred to Track 6): -// GET /api/archives, GET /api/archives/:id/entries, etc. -// AUTH — requires login (ROLE_USER bit): -// POST /api/archives/:id/captures -// POST /api/archives/:id/tags -// POST/DELETE /api/archives/:id/entries/:uid/tags -// ADMIN — requires ROLE_ADMIN: (future) -// OWNER — requires ROLE_OWNER: (future) -// AUTH_SELF — own resources, require_auth() only: -// GET/POST/DELETE /api/auth/tokens -// POST /api/auth/logout, GET/PATCH /api/auth/me -// SETTINGS — instance settings, require ROLE_ADMIN: -// GET/PATCH /api/admin/instance-settings -// ──────────────────────────────────────────────────────────────────────────── +// Route classification (for when middleware is added later): +// +// STATIC — safe to expose publicly: GET / and static /assets/* +// READ — safe to expose read-only: GET /health +// GET /api/archives +// GET /api/archives/:id/entries +// GET /api/archives/:id/entries/search +// GET /api/archives/:id/entries/:uid +// GET /api/archives/:id/entries/:uid/artifacts/:idx +// GET /api/archives/:id/runs +// GET /api/archives/:id/tags +// ADMIN — requires auth if ever public: GET /api/admin/archives +// WRITE — requires auth if ever public: POST /api/archives/:id/captures +// POST /api/archives/:id/tags +// PUT /api/archives/:id/tags/:tag_id +// DELETE /api/archives/:id/tags/:tag_id +// +// Do not add middleware here until the auth model is chosen. See docs/README.md. +// ───────────────────────────────────────────────────────────────────────────── use std::{path::PathBuf, sync::Arc}; @@ -26,23 +29,17 @@ use axum::{ Json, Router, extract::{Path, Query, Request, State}, http::StatusCode, - middleware::Next, response::{IntoResponse, Response}, - routing::{delete, get, patch, post}, + routing::{delete, get, post}, }; use tower_http::services::{ServeDir, ServeFile}; use tower::ServiceExt; use crate::registry::{MountedArchive, ServerRegistry}; -use crate::auth; -pub use crate::auth::{AuthUser, ROLE_ADMIN, ROLE_GUEST, ROLE_OWNER, ROLE_USER}; -use axum_extra::extract::CookieJar; -use rusqlite::OptionalExtension; #[derive(Clone)] pub struct AppState { registry: Arc, - pub auth_db_path: Arc, } #[derive(Debug, serde::Deserialize, Default)] @@ -51,35 +48,9 @@ pub struct EntrySearchParams { pub tag: Option, } -/// Tower middleware: returns 503 on all non-exempt routes if setup hasn't been completed. -async fn setup_guard( - State(state): State, - req: Request, - next: Next, -) -> Response { - let path = req.uri().path().to_owned(); - let exempt = path.starts_with("/api/auth/") - || path.starts_with("/assets") - || path == "/" - || path == "/health"; - if !exempt { - if let Ok(conn) = database::open_auth_db(&state.auth_db_path) { - if matches!(database::ensure_owner_exists(&conn), Ok(false)) { - return ( - StatusCode::SERVICE_UNAVAILABLE, - axum::Json(serde_json::json!({ "error": "setup_required" })), - ) - .into_response(); - } - } - } - next.run(req).await -} - -pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router { +pub fn app(registry: ServerRegistry) -> Router { let state = AppState { registry: Arc::new(registry), - auth_db_path: Arc::new(auth_db_path), }; let static_dir = static_dir(); @@ -100,16 +71,8 @@ pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router "/api/archives/:archive_id/entries/:entry_uid/favicon", get(serve_entry_favicon), ) - .route( - "/api/archives/:archive_id/blobs/:sha256", - get(serve_blob), - ) .route("/api/archives/:archive_id/runs", get(list_runs)) .route("/api/archives/:archive_id/captures", post(capture_handler)) - .route( - "/api/archives/:archive_id/capture_jobs/:job_uid", - get(get_capture_job_handler), - ) .route("/api/archives/:archive_id/tags", get(list_tags).post(create_tag_handler)) .route( "/api/archives/:archive_id/entries/:entry_uid/tags", @@ -119,35 +82,8 @@ pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router "/api/archives/:archive_id/entries/:entry_uid/tags/:tag_uid", delete(remove_entry_tag_handler), ) - .route("/api/auth/setup", axum::routing::get(auth_setup_status).post(auth_setup)) - .route("/api/auth/login", axum::routing::post(auth_login)) - .route("/api/auth/logout", axum::routing::post(auth_logout)) - .route("/api/auth/me", axum::routing::get(auth_me).patch(patch_me)) - .route("/api/auth/tokens", axum::routing::get(list_tokens).post(create_token)) - .route("/api/auth/tokens/:token_uid", axum::routing::delete(delete_token)) - .route("/api/admin/users", get(admin_list_users).post(admin_create_user)) - .route("/api/admin/users/:uid/status", axum::routing::patch(admin_set_user_status)) - .route("/api/admin/users/:uid/roles", axum::routing::post(admin_assign_role)) - .route("/api/admin/users/:uid/roles/:role_slug", axum::routing::delete(admin_remove_role)) - .route("/api/admin/roles", get(admin_list_roles).post(admin_create_role)) - .route("/api/admin/instance-settings", - get(get_instance_settings_handler).patch(update_instance_settings_handler)) - .route("/api/archives/:archive_id/collections", - get(list_collections_handler).post(create_collection_handler)) - .route("/api/archives/:archive_id/collections/:coll_uid", - get(get_collection_handler) - .patch(patch_collection_handler) - .delete(delete_collection_handler)) - .route("/api/archives/:archive_id/collections/:coll_uid/entries", - post(add_entry_to_collection_handler)) - .route("/api/archives/:archive_id/collections/:coll_uid/entries/:entry_uid", - delete(remove_entry_from_collection_handler) - .patch(update_entry_visibility_handler)) - .route("/api/archives/:archive_id/entries/:entry_uid/collections", - get(list_entry_collections_handler)) .nest_service("/assets", ServeDir::new(static_dir.join("assets"))) .fallback_service(ServeFile::new(static_dir.join("index.html"))) - .layer(axum::middleware::from_fn_with_state(state.clone(), setup_guard)) .with_state(state) } @@ -163,18 +99,15 @@ async fn list_archives(State(state): State) -> Json, - auth: AuthUser, Path(archive_id): Path, ) -> Result>, ApiError> { let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; - let caller_bits = auth_to_caller_bits(&auth); - Ok(Json(archive::list_root_entries(&conn, caller_bits)?)) + Ok(Json(archive::list_root_entries(&conn)?)) } async fn search_entries_handler( State(state): State, - auth: AuthUser, Path(archive_id): Path, Query(params): Query, ) -> Result>, ApiError> { @@ -186,7 +119,6 @@ async fn search_entries_handler( if let Some(tag) = params.tag { search_query.tag = Some(tag); } - search_query.caller_bits = auth_to_caller_bits(&auth); Ok(Json(archive::search_entries(&conn, &search_query)?)) } @@ -257,39 +189,6 @@ async fn serve_entry_favicon( .into_response()) } -async fn serve_blob( - State(state): State, - Path((archive_id, sha256)): Path<(String, String)>, - req: Request, -) -> Result { - 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 blob = database::get_blob_by_sha256(&conn, &sha256)? - .ok_or(ApiError::not_found("blob not found"))?; - - let file_path = paths.store_path.join(&blob.raw_relpath); - - // Path-traversal guard: resolved path must stay inside store_path. - let canonical_file = file_path - .canonicalize() - .map_err(|_| ApiError::not_found("blob file not found"))?; - let canonical_store = paths - .store_path - .canonicalize() - .map_err(|_| ApiError::internal("invalid store path"))?; - if !canonical_file.starts_with(&canonical_store) { - return Err(ApiError::not_found("blob not found")); - } - - Ok(ServeFile::new(&canonical_file) - .oneshot(req) - .await - .unwrap() - .into_response()) -} - #[derive(Debug, serde::Deserialize)] struct CreateTagBody { path: String, @@ -300,34 +199,6 @@ struct AssignTagBody { tag_path: String, } -#[derive(Debug, serde::Deserialize)] -struct CreateCollectionBody { - name: String, - slug: String, - #[serde(default = "default_user_visibility")] - default_visibility_bits: u32, -} - -fn default_user_visibility() -> u32 { 2 } - -#[derive(Debug, serde::Deserialize)] -struct AddEntryBody { - entry_uid: String, - #[serde(default = "default_user_visibility")] - visibility_bits: u32, -} - -#[derive(Debug, serde::Deserialize)] -struct UpdateVisibilityBody { - visibility_bits: u32, -} - -#[derive(Debug, serde::Deserialize)] -struct PatchCollectionBody { - name: Option, - default_visibility_bits: Option, -} - async fn list_tags( State(state): State, Path(archive_id): Path, @@ -339,11 +210,9 @@ async fn list_tags( async fn create_tag_handler( State(state): State, - auth_user: AuthUser, Path(archive_id): Path, Json(body): Json, ) -> Result<(StatusCode, Json), ApiError> { - auth_user.require_role(ROLE_USER)?; if body.path.trim().is_empty() { return Err(ApiError::bad_request("tag path must not be empty")); } @@ -367,11 +236,9 @@ async fn list_entry_tags( async fn assign_entry_tag_handler( State(state): State, - auth_user: AuthUser, Path((archive_id, entry_uid)): Path<(String, String)>, Json(body): Json, ) -> Result<(StatusCode, Json), ApiError> { - auth_user.require_role(ROLE_USER)?; if body.tag_path.trim().is_empty() { return Err(ApiError::bad_request("tag_path must not be empty")); } @@ -385,10 +252,8 @@ async fn assign_entry_tag_handler( async fn remove_entry_tag_handler( State(state): State, - auth_user: AuthUser, Path((archive_id, entry_uid, tag_uid)): Path<(String, String, String)>, ) -> Result { - auth_user.require_role(ROLE_USER)?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; if archive::remove_entry_tag(&conn, &entry_uid, &tag_uid)? { @@ -403,431 +268,20 @@ struct CaptureBody { locator: String, } -#[derive(Debug, serde::Deserialize)] -struct LoginBody { - username: String, - password: String, -} - -#[derive(Debug, serde::Deserialize)] -struct SetupBody { - username: String, - password: String, -} - -#[derive(Debug, serde::Deserialize)] -struct CreateTokenBody { - name: String, -} - async fn capture_handler( State(state): State, - auth_user: AuthUser, Path(archive_id): Path, Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - auth_user.require_role(ROLE_USER)?; +) -> Result, ApiError> { if body.locator.trim().is_empty() { return Err(ApiError::bad_request("locator must not be empty")); } let mounted = mounted_archive(&state, &archive_id)?; let archive_paths = archive::read_archive_paths(&mounted.archive_path) .map_err(ApiError::from)?; - - // Create job record in the archive DB. - let conn = database::open_or_initialize(&mounted.archive_path)?; - let job_uid = database::create_capture_job(&conn, &archive_id)?; - drop(conn); - - // Spawn background capture. - let locator = body.locator.trim().to_string(); - let archive_path = mounted.archive_path.clone(); - let job_uid_bg = job_uid.clone(); - let archive_id_bg = archive_id.clone(); - tokio::task::spawn_blocking(move || { - let conn = match database::open_or_initialize(&archive_path) { - Ok(c) => c, - Err(e) => { - eprintln!("warn: capture job {job_uid_bg}: db open failed: {e:#}"); - return; - } - }; - database::update_capture_job_status(&conn, &job_uid_bg, "running", None, None).ok(); - match capture::perform_capture(&archive_paths, &locator, Some(&archive_id_bg)) { - Ok(result) => { - database::update_capture_job_status( - &conn, - &job_uid_bg, - "completed", - Some(&result.run_uid), - None, - ) - .ok(); - } - Err(e) => { - database::update_capture_job_status( - &conn, - &job_uid_bg, - "failed", - None, - Some(&format!("{e:#}")), - ) - .ok(); - } - } - }); - - Ok(( - StatusCode::ACCEPTED, - Json(serde_json::json!({ "job_uid": job_uid, "status": "pending" })), - )) -} - -async fn get_capture_job_handler( - State(state): State, - auth_user: AuthUser, - Path((archive_id, job_uid)): Path<(String, String)>, -) -> Result, ApiError> { - auth_user.require_role(ROLE_USER)?; - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - archive::get_capture_job(&conn, &job_uid)? - .map(Json) - .ok_or_else(|| ApiError::not_found("capture job not found")) -} - -async fn auth_setup_status( - State(state): State, -) -> Result, ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - let required = !database::ensure_owner_exists(&conn)?; - Ok(Json(serde_json::json!({ "setup_required": required }))) -} - -async fn auth_setup( - State(state): State, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - if database::ensure_owner_exists(&conn)? { - return Err(ApiError { - status: StatusCode::CONFLICT, - message: "already_configured".to_string(), - }); - } - if body.username.trim().is_empty() || body.password.len() < 8 { - return Err(ApiError::bad_request("username required and password must be at least 8 characters")); - } - let hash = auth::hash_password(&body.password).map_err(ApiError::from)?; - database::create_owner(&conn, &body.username, &hash)?; - let user = database::get_user_by_username(&conn, &body.username)? - .ok_or_else(|| ApiError::internal("user not found after creation"))?; - Ok((StatusCode::CREATED, Json(serde_json::json!({ - "user_uid": user.user_uid, - "username": user.username, - })))) -} - -async fn auth_login( - State(state): State, - headers: axum::http::HeaderMap, - Json(body): Json, -) -> Result<(StatusCode, axum::http::HeaderMap, Json), ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - let user = database::get_user_by_username(&conn, &body.username)? - .filter(|u| u.status == "active") - .ok_or_else(|| ApiError::unauthorized("invalid_credentials"))?; - if !auth::verify_password(&body.password, &user.password_hash) - .map_err(ApiError::from)? - { - return Err(ApiError::unauthorized("invalid_credentials")); - } - let role_bits = database::compute_role_bits(&conn, user.id)?; - let user_agent = headers.get("user-agent").and_then(|v| v.to_str().ok()); - let session_uid = database::create_session(&conn, user.id, role_bits, user_agent)?; - - let secure = headers - .get("x-forwarded-proto") - .and_then(|v| v.to_str().ok()) - .map(|v| v == "https") - .unwrap_or(false); - let cookie_value = format!( - "session={}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000{}", - session_uid, - if secure { "; Secure" } else { "" } - ); - let mut resp_headers = axum::http::HeaderMap::new(); - resp_headers.insert( - axum::http::header::SET_COOKIE, - cookie_value.parse().map_err(|_| ApiError::internal("cookie error"))?, - ); - - Ok((StatusCode::OK, resp_headers, Json(serde_json::json!({ - "user_uid": user.user_uid, - "username": user.username, - "role_bits": role_bits, - })))) -} - -async fn auth_logout( - State(state): State, - jar: CookieJar, -) -> Result<(StatusCode, axum::http::HeaderMap), ApiError> { - if let Some(cookie) = jar.get("session") { - let conn = database::open_auth_db(&state.auth_db_path)?; - database::delete_session(&conn, cookie.value())?; - } - let mut resp_headers = axum::http::HeaderMap::new(); - resp_headers.insert( - axum::http::header::SET_COOKIE, - "session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0" - .parse() - .unwrap(), - ); - Ok((StatusCode::NO_CONTENT, resp_headers)) -} - -async fn auth_me( - State(state): State, - auth_user: AuthUser, -) -> Result, ApiError> { - let (user_id, role_bits) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - let (username, display_name): (String, Option) = conn - .query_row( - "SELECT username, display_name FROM users WHERE id = ?1", - [user_id], - |r| Ok((r.get(0)?, r.get(1)?)) - ) - .map_err(|e| ApiError::from(anyhow::anyhow!("db error: {e}")))?; - Ok(Json(serde_json::json!({ - "role_bits": role_bits, - "username": username, - "display_name": display_name, - }))) -} - -async fn patch_me( - State(state): State, - auth_user: AuthUser, - Json(body): Json, -) -> Result { - let (user_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - - if let Some(ref new_pw) = body.new_password { - if new_pw.trim().is_empty() { - return Err(ApiError::bad_request("new_password must not be blank")); - } - let current_pw = body.current_password.as_deref().unwrap_or(""); - let hash = database::get_user_password_hash(&conn, user_id)? - .ok_or_else(|| ApiError::not_found("user not found"))?; - if !auth::verify_password(current_pw, &hash).map_err(ApiError::from)? { - return Err(ApiError::unauthorized("current password is incorrect")); - } - let new_hash = auth::hash_password(new_pw).map_err(ApiError::from)?; - database::update_user_password(&conn, user_id, &new_hash)?; - } - - if let Some(ref dn) = body.display_name { - let v: Option<&str> = if dn.trim().is_empty() { None } else { Some(dn.as_str()) }; - database::update_user_display_name(&conn, user_id, v)?; - } - - Ok(StatusCode::NO_CONTENT) -} - -async fn get_instance_settings_handler( - State(state): State, - auth_user: AuthUser, -) -> Result, ApiError> { - auth_user.require_role(ROLE_ADMIN)?; - let conn = database::open_auth_db(&state.auth_db_path)?; - Ok(Json(database::get_instance_settings(&conn)?)) -} - -async fn update_instance_settings_handler( - State(state): State, - auth_user: AuthUser, - Json(body): Json, -) -> Result { - auth_user.require_role(ROLE_ADMIN)?; - let conn = database::open_auth_db(&state.auth_db_path)?; - let mut settings = database::get_instance_settings(&conn)?; - if let Some(v) = body.public_index_enabled { settings.public_index_enabled = v; } - if let Some(v) = body.public_entry_content_enabled { settings.public_entry_content_enabled = v; } - if let Some(v) = body.open_registration_enabled { settings.open_registration_enabled = v; } - if let Some(v) = body.default_entry_visibility { settings.default_entry_visibility = v; } - database::update_instance_settings(&conn, &settings)?; - Ok(StatusCode::NO_CONTENT) -} - -async fn create_token( - State(state): State, - auth_user: AuthUser, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - let (user_id, _) = auth_user.require_auth()?; - if body.name.trim().is_empty() { - return Err(ApiError::bad_request("token name is required")); - } - let raw_token = auth::generate_token(); - let token_hash = auth::hash_token(&raw_token); - let conn = database::open_auth_db(&state.auth_db_path)?; - let token_uid = database::create_api_token(&conn, user_id, &token_hash, &body.name)?; - Ok((StatusCode::CREATED, Json(serde_json::json!({ - "token_uid": token_uid, - "raw_token": raw_token, - "name": body.name, - })))) -} - -async fn list_tokens( - State(state): State, - auth_user: AuthUser, -) -> Result>, ApiError> { - let (user_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - Ok(Json(database::list_user_tokens(&conn, user_id)?)) -} - -async fn delete_token( - State(state): State, - auth_user: AuthUser, - Path(token_uid): Path, -) -> Result { - let (user_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - if database::delete_api_token(&conn, &token_uid, user_id)? { - Ok(StatusCode::NO_CONTENT) - } else { - Err(ApiError::not_found("token not found")) - } -} - -#[derive(Debug, serde::Deserialize)] -struct AdminCreateUserBody { username: String, password: String, email: Option } - -#[derive(Debug, serde::Deserialize)] -struct AdminSetStatusBody { status: String } - -#[derive(Debug, serde::Deserialize)] -struct AdminAssignRoleBody { role_slug: String } - -#[derive(Debug, serde::Deserialize)] -struct AdminCreateRoleBody { slug: String, name: String } - -#[derive(Debug, serde::Deserialize)] -struct UpdateProfileBody { - display_name: Option, - current_password: Option, - new_password: Option, -} - -#[derive(Debug, serde::Deserialize)] -struct UpdateInstanceSettingsBody { - public_index_enabled: Option, - public_entry_content_enabled: Option, - open_registration_enabled: Option, - default_entry_visibility: Option, -} - -async fn admin_list_users( - State(state): State, - auth_user: AuthUser, -) -> Result>, ApiError> { - auth_user.require_role(ROLE_ADMIN)?; - let conn = database::open_auth_db(&state.auth_db_path)?; - Ok(Json(database::list_users(&conn)?)) -} - -async fn admin_create_user( - State(state): State, - auth_user: AuthUser, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - auth_user.require_role(ROLE_ADMIN)?; - let (caller_id, _) = auth_user.require_auth()?; - if body.username.trim().is_empty() || body.password.len() < 8 { - return Err(ApiError::bad_request("username required, password >= 8 chars")); - } - let conn = database::open_auth_db(&state.auth_db_path)?; - let hash = auth::hash_password(&body.password).map_err(ApiError::from)?; - let uid = database::create_user(&conn, &body.username, body.email.as_deref(), &hash, caller_id)?; - Ok((StatusCode::CREATED, Json(serde_json::json!({ "user_uid": uid, "username": body.username })))) -} - -async fn admin_set_user_status( - State(state): State, - auth_user: AuthUser, - Path(uid): Path, - Json(body): Json, -) -> Result, ApiError> { - auth_user.require_role(ROLE_ADMIN)?; - if body.status != "active" && body.status != "disabled" { - return Err(ApiError::bad_request("status must be 'active' or 'disabled'")); - } - let conn = database::open_auth_db(&state.auth_db_path)?; - if !database::set_user_status(&conn, &uid, &body.status)? { - return Err(ApiError::not_found("user not found")); - } - Ok(Json(serde_json::json!({ "user_uid": uid, "status": body.status }))) -} - -async fn admin_assign_role( - State(state): State, - auth_user: AuthUser, - Path(uid): Path, - Json(body): Json, -) -> Result { - auth_user.require_role(ROLE_ADMIN)?; - let (caller_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - let target_id = database::get_user_id_by_uid(&conn, &uid)? - .ok_or_else(|| ApiError::not_found("user not found"))?; - database::assign_role(&conn, target_id, &body.role_slug, caller_id)?; - Ok(StatusCode::OK) -} - -async fn admin_remove_role( - State(state): State, - auth_user: AuthUser, - Path((uid, role_slug)): Path<(String, String)>, -) -> Result { - auth_user.require_role(ROLE_ADMIN)?; - let conn = database::open_auth_db(&state.auth_db_path)?; - let target_id = database::get_user_id_by_uid(&conn, &uid)? - .ok_or_else(|| ApiError::not_found("user not found"))?; - database::remove_role(&conn, target_id, &role_slug)?; - Ok(StatusCode::NO_CONTENT) -} - -async fn admin_list_roles( - State(state): State, - auth_user: AuthUser, -) -> Result>, ApiError> { - auth_user.require_role(ROLE_ADMIN)?; - let conn = database::open_auth_db(&state.auth_db_path)?; - Ok(Json(database::list_roles(&conn)?)) -} - -async fn admin_create_role( - State(state): State, - auth_user: AuthUser, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - auth_user.require_role(ROLE_ADMIN)?; - let conn = database::open_auth_db(&state.auth_db_path)?; - let role = database::create_custom_role(&conn, &body.slug, &body.name) + let result = capture::perform_capture(&archive_paths, &body.locator) .map_err(ApiError::from)?; - Ok((StatusCode::CREATED, Json(role))) -} - -fn auth_to_caller_bits(auth: &AuthUser) -> u32 { - match auth { - AuthUser::Authenticated { role_bits, .. } => *role_bits, - AuthUser::Guest => ROLE_GUEST, - } + Ok(Json(result)) } fn mounted_archive<'a>( @@ -862,21 +316,6 @@ impl ApiError { message: message.to_string(), } } - - fn internal(message: &str) -> Self { - Self { - status: StatusCode::INTERNAL_SERVER_ERROR, - message: message.to_string(), - } - } - - pub fn unauthorized(message: &str) -> Self { - Self { status: StatusCode::UNAUTHORIZED, message: message.to_string() } - } - - pub fn forbidden(message: &str) -> Self { - Self { status: StatusCode::FORBIDDEN, message: message.to_string() } - } } impl From for ApiError @@ -894,220 +333,7 @@ where impl IntoResponse for ApiError { fn into_response(self) -> Response { - let body = serde_json::json!({ "error": self.message }); - (self.status, axum::Json(body)).into_response() - } -} - -// ── Collection handlers ──────────────────────────────────────────────────────── - -async fn list_collections_handler( - State(state): State, - Path(archive_id): Path, -) -> Result>, ApiError> { - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - Ok(Json(archive::list_collections(&conn)?)) -} - -async fn create_collection_handler( - State(state): State, - auth: AuthUser, - Path(archive_id): Path, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - auth.require_role(ROLE_USER)?; - if body.name.trim().is_empty() { - return Err(ApiError::bad_request("collection name must not be empty")); - } - if body.slug.trim().is_empty() || body.slug.starts_with('_') { - return Err(ApiError::bad_request("collection slug must not be empty or start with underscore")); - } - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - let record = database::create_collection(&conn, &body.name, &body.slug, body.default_visibility_bits) - .map_err(|e| ApiError::bad_request(&format!("{e:#}")))?; - Ok((StatusCode::CREATED, Json(archive::CollectionSummary { - collection_uid: record.collection_uid, - name: record.name, - slug: record.slug, - default_visibility_bits: record.default_visibility_bits, - created_at: record.created_at, - }))) -} - -async fn get_collection_handler( - State(state): State, - auth: AuthUser, - Path((archive_id, coll_uid)): Path<(String, String)>, -) -> Result, ApiError> { - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - let record = database::get_collection_by_uid(&conn, &coll_uid)? - .ok_or(ApiError::not_found("collection not found"))?; - let caller_bits = auth_to_caller_bits(&auth); - let entries = archive::list_entries_for_collection(&conn, record.id, caller_bits)?; - // Collect per-entry visibility bits from collection_entries - let mut vis_map: std::collections::HashMap = std::collections::HashMap::new(); - { - let mut stmt = conn.prepare( - "SELECT ae.entry_uid, ce.visibility_bits \ - FROM collection_entries ce \ - JOIN archived_entries ae ON ae.id = ce.entry_id \ - WHERE ce.collection_id = ?1", - )?; - let rows = stmt.query_map([record.id], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as u32)) - })?; - for r in rows { - if let Ok((uid, bits)) = r { - vis_map.insert(uid, bits); - } - } - } - let entries_json: Vec = entries.iter().map(|e| { - let vis = vis_map.get(&e.entry_uid).copied().unwrap_or(record.default_visibility_bits); - serde_json::json!({ - "entry_uid": e.entry_uid, - "title": e.title, - "source_kind": e.source_kind, - "archived_at": e.archived_at, - "collection_visibility_bits": vis, - }) - }).collect(); - Ok(Json(serde_json::json!({ - "collection_uid": record.collection_uid, - "name": record.name, - "slug": record.slug, - "default_visibility_bits": record.default_visibility_bits, - "created_at": record.created_at, - "entries": entries_json, - }))) -} - -async fn add_entry_to_collection_handler( - State(state): State, - auth: AuthUser, - Path((archive_id, coll_uid)): Path<(String, String)>, - Json(body): Json, -) -> Result { - auth.require_role(ROLE_USER)?; - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - let coll = database::get_collection_by_uid(&conn, &coll_uid)? - .ok_or(ApiError::not_found("collection not found"))?; - if coll.slug == "_default_" { - return Err(ApiError::bad_request("cannot manually add entries to the default collection")); - } - let entry_id: i64 = conn - .query_row( - "SELECT id FROM archived_entries WHERE entry_uid = ?1", - [body.entry_uid.as_str()], - |row| row.get(0), - ) - .optional()? - .ok_or(ApiError::not_found("entry not found"))?; - database::add_entry_to_collection(&conn, coll.id, entry_id, body.visibility_bits)?; - Ok(StatusCode::NO_CONTENT) -} - -async fn remove_entry_from_collection_handler( - State(state): State, - auth: AuthUser, - Path((archive_id, coll_uid, entry_uid)): Path<(String, String, String)>, -) -> Result { - auth.require_role(ROLE_USER)?; - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - let coll = database::get_collection_by_uid(&conn, &coll_uid)? - .ok_or(ApiError::not_found("collection not found"))?; - if coll.slug == "_default_" { - return Err(ApiError::bad_request("cannot manually remove entries from the default collection")); - } - let entry_id: i64 = conn - .query_row( - "SELECT id FROM archived_entries WHERE entry_uid = ?1", - [entry_uid.as_str()], - |row| row.get(0), - ) - .optional()? - .ok_or(ApiError::not_found("entry not found"))?; - if database::remove_entry_from_collection(&conn, coll.id, entry_id)? { - Ok(StatusCode::NO_CONTENT) - } else { - Err(ApiError::not_found("entry not in collection")) - } -} - -async fn update_entry_visibility_handler( - State(state): State, - auth: AuthUser, - Path((archive_id, coll_uid, entry_uid)): Path<(String, String, String)>, - Json(body): Json, -) -> Result { - auth.require_role(ROLE_USER)?; - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - let coll = database::get_collection_by_uid(&conn, &coll_uid)? - .ok_or(ApiError::not_found("collection not found"))?; - let entry_id: i64 = conn - .query_row( - "SELECT id FROM archived_entries WHERE entry_uid = ?1", - [entry_uid.as_str()], - |row| row.get(0), - ) - .optional()? - .ok_or(ApiError::not_found("entry not found"))?; - if database::update_collection_entry_visibility(&conn, coll.id, entry_id, body.visibility_bits)? { - Ok(StatusCode::NO_CONTENT) - } else { - Err(ApiError::not_found("entry not in collection")) - } -} - -async fn list_entry_collections_handler( - State(state): State, - Path((archive_id, entry_uid)): Path<(String, String)>, -) -> Result>, ApiError> { - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - match archive::get_entry_collections(&conn, &entry_uid)? { - Some(memberships) => Ok(Json(memberships)), - None => Err(ApiError::not_found("entry not found")), - } -} - -async fn patch_collection_handler( - State(state): State, - auth: AuthUser, - Path((archive_id, coll_uid)): Path<(String, String)>, - Json(body): Json, -) -> Result { - auth.require_role(ROLE_USER)?; - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - let name_ref: Option<&str> = body.name.as_deref(); - let updated = database::update_collection(&conn, &coll_uid, name_ref, body.default_visibility_bits)?; - if updated { - Ok(StatusCode::NO_CONTENT) - } else { - Err(ApiError::not_found("collection not found")) - } -} - -async fn delete_collection_handler( - State(state): State, - auth: AuthUser, - Path((archive_id, coll_uid)): Path<(String, String)>, -) -> Result { - auth.require_role(ROLE_USER)?; - let mounted = mounted_archive(&state, &archive_id)?; - let conn = database::open_or_initialize(&mounted.archive_path)?; - let deleted = database::delete_collection(&conn, &coll_uid)?; - if deleted { - Ok(StatusCode::NO_CONTENT) - } else { - Err(ApiError::not_found("collection not found")) + (self.status, self.message).into_response() } } @@ -1118,112 +344,8 @@ mod tests { use axum::http::{Request, StatusCode}; use tower::ServiceExt; - fn make_test_app() -> (Router, tempfile::TempDir) { - let dir = tempfile::tempdir().unwrap(); - let auth_path = dir.path().join("auth.sqlite"); - // Seed owner so setup_guard passes in normal tests - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "testowner", "test_hash_not_real").unwrap(); - } - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - (app(registry, auth_path), dir) - } - - fn make_setup_test_app() -> (Router, tempfile::TempDir) { - let dir = tempfile::tempdir().unwrap(); - let auth_path = dir.path().join("auth.sqlite"); - // NO owner seeded - for testing setup-required behavior - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - (app(registry, auth_path), dir) - } - - fn make_test_registry(dir: &tempfile::TempDir) -> (ServerRegistry, std::path::PathBuf, std::path::PathBuf) { - let paths = archivr_core::archive::initialize_archive( - dir.path(), - &dir.path().join("store"), - "test", - false, - ).unwrap(); - let auth_path = dir.path().join("auth.sqlite"); - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); - } - let registry = ServerRegistry { - archives: vec![MountedArchive { - id: "test".to_string(), - label: "Test".to_string(), - archive_path: paths.archive_path.clone(), - }], - bind: None, - auth_db_path: None, - }; - (registry, paths.archive_path, auth_path) - } - - /// Creates a session for the seeded 'testowner' and returns the cookie string. - fn make_test_session(auth_path: &std::path::Path) -> String { - let conn = archivr_core::database::open_auth_db(auth_path).unwrap(); - let user_id: i64 = conn - .query_row("SELECT id FROM users WHERE username = 'testowner'", [], |r| r.get(0)) - .unwrap(); - let role_bits = archivr_core::database::compute_role_bits(&conn, user_id).unwrap(); - let sess_uid = - archivr_core::database::create_session(&conn, user_id, role_bits, None).unwrap(); - format!("session={}", sess_uid) - } - - fn make_test_entry(archive_path: &std::path::Path) -> archivr_core::database::ArchivedEntry { - let conn = database::open_or_initialize(archive_path).unwrap(); - let user_id = database::ensure_default_user(&conn).unwrap(); - let run = database::create_archive_run(&conn, user_id, 1).unwrap(); - let si = database::upsert_source_identity( - &conn, "web", "page", None, - Some("https://example.com/test"), - "https://example.com/test", - ) - .unwrap(); - database::create_archived_entry( - &conn, - &database::NewEntry { - source_identity_id: si, - archive_run_id: run.id, - parent_entry_id: None, - root_entry_id: None, - created_by_user_id: user_id, - owned_by_user_id: user_id, - source_kind: "web".to_string(), - entity_kind: "page".to_string(), - title: Some("Test Entry".to_string()), - visibility: "private".to_string(), - representation_kind: "html".to_string(), - source_metadata_json: "{}".to_string(), - display_metadata_json: None, - }, - ) - .unwrap() - } - - async fn body_json(response: axum::response::Response) -> serde_json::Value { - let bytes = axum::body::to_bytes(response.into_body(), usize::MAX) - .await - .unwrap(); - serde_json::from_slice(&bytes).unwrap() - } - - fn json_body(payload: &serde_json::Value) -> Body { - Body::from(serde_json::to_vec(payload).unwrap()) - } - #[tokio::test] async fn archives_endpoint_lists_mounted_archives() { - let dir = tempfile::tempdir().unwrap(); - let auth_path = dir.path().join("auth.sqlite"); - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); - } let registry = ServerRegistry { archives: vec![MountedArchive { id: "personal".to_string(), @@ -1231,9 +353,8 @@ mod tests { archive_path: std::path::PathBuf::from("/tmp/personal/.archivr"), }], bind: None, - auth_db_path: None, }; - let response = app(registry, auth_path) + let response = app(registry) .oneshot( Request::builder() .uri("/api/archives") @@ -1248,8 +369,7 @@ mod tests { #[tokio::test] async fn missing_archive_returns_404() { - let (test_app, _dir) = make_test_app(); - let response = test_app + let response = app(ServerRegistry::default()) .oneshot( Request::builder() .uri("/api/archives/missing/entries") @@ -1264,8 +384,7 @@ mod tests { #[tokio::test] async fn artifact_missing_archive_returns_404() { - let (test_app, _dir) = make_test_app(); - let response = test_app + let response = app(ServerRegistry::default()) .oneshot( Request::builder() .uri("/api/archives/nope/entries/entry_abc/artifacts/0") @@ -1288,11 +407,6 @@ mod tests { ) .unwrap(); let archive_path = dir.path().join(".archivr"); - let auth_path = dir.path().join("auth.sqlite"); - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); - } let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), @@ -1300,9 +414,8 @@ mod tests { archive_path, }], bind: None, - auth_db_path: None, }; - let response = app(registry, auth_path) + let response = app(registry) .oneshot( Request::builder() .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0") @@ -1325,11 +438,6 @@ mod tests { ) .unwrap(); let archive_path = dir.path().join(".archivr"); - let auth_path = dir.path().join("auth.sqlite"); - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); - } let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), @@ -1337,9 +445,8 @@ mod tests { archive_path, }], bind: None, - auth_db_path: None, }; - let response = app(registry, auth_path) + let response = app(registry) .oneshot( Request::builder() .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99") @@ -1424,11 +531,6 @@ mod tests { .unwrap(); drop(conn); // release before the HTTP handler opens the same db file - let auth_path = dir.path().join("auth.sqlite"); - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); - } let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), @@ -1436,13 +538,12 @@ mod tests { archive_path: paths.archive_path.clone(), }], bind: None, - auth_db_path: None, }; let uri = format!( "/api/archives/test/entries/{}/artifacts/0", entry.entry_uid ); - let response = app(registry, auth_path) + let response = app(registry) .oneshot(Request::builder().uri(&uri).body(Body::empty()).unwrap()) .await .unwrap(); @@ -1451,8 +552,7 @@ mod tests { #[tokio::test] async fn search_missing_archive_returns_404() { - let (test_app, _dir) = make_test_app(); - let response = test_app + let response = app(ServerRegistry::default()) .oneshot( Request::builder() .uri("/api/archives/nope/entries/search?q=anything") @@ -1467,8 +567,23 @@ mod tests { #[tokio::test] async fn search_empty_q_returns_ok() { let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let response = app(registry, auth_path) + 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, + }], + bind: None, + }; + let response = app(registry) .oneshot( Request::builder() .uri("/api/archives/test/entries/search") @@ -1483,8 +598,23 @@ mod tests { #[tokio::test] async fn search_unknown_prefix_returns_400() { let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let response = app(registry, auth_path) + 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, + }], + bind: None, + }; + let response = app(registry) .oneshot( Request::builder() .uri("/api/archives/test/entries/search?q=unknownprefix%3Aval") @@ -1496,12 +626,74 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } + // ---- helpers ---- + + fn make_test_registry(dir: &tempfile::TempDir) -> (ServerRegistry, std::path::PathBuf) { + let paths = archivr_core::archive::initialize_archive( + dir.path(), + &dir.path().join("store"), + "test", + false, + ) + .unwrap(); + let registry = ServerRegistry { + archives: vec![MountedArchive { + id: "test".to_string(), + label: "Test".to_string(), + archive_path: paths.archive_path.clone(), + }], + bind: None, + }; + (registry, paths.archive_path) + } + + fn make_test_entry(archive_path: &std::path::Path) -> archivr_core::database::ArchivedEntry { + let conn = database::open_or_initialize(archive_path).unwrap(); + let user_id = database::ensure_default_user(&conn).unwrap(); + let run = database::create_archive_run(&conn, user_id, 1).unwrap(); + let si = database::upsert_source_identity( + &conn, "web", "page", None, + Some("https://example.com/test"), + "https://example.com/test", + ) + .unwrap(); + database::create_archived_entry( + &conn, + &database::NewEntry { + source_identity_id: si, + archive_run_id: run.id, + parent_entry_id: None, + root_entry_id: None, + created_by_user_id: user_id, + owned_by_user_id: user_id, + source_kind: "web".to_string(), + entity_kind: "page".to_string(), + title: Some("Test Entry".to_string()), + visibility: "private".to_string(), + representation_kind: "html".to_string(), + source_metadata_json: "{}".to_string(), + display_metadata_json: None, + }, + ) + .unwrap() + } + + async fn body_json(response: axum::response::Response) -> serde_json::Value { + let bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + serde_json::from_slice(&bytes).unwrap() + } + + fn json_body(payload: &serde_json::Value) -> Body { + Body::from(serde_json::to_vec(payload).unwrap()) + } + // ---- tag route tests ---- #[tokio::test] async fn test_list_tags_unknown_archive() { - let (test_app, _dir) = make_test_app(); - let response = test_app + let response = app(ServerRegistry::default()) .oneshot( Request::builder() .uri("/api/archives/ghost/tags") @@ -1515,8 +707,7 @@ mod tests { #[tokio::test] async fn test_create_tag_unknown_archive() { - let (test_app, _dir) = make_test_app(); - let response = test_app + let response = app(ServerRegistry::default()) .oneshot( Request::builder() .method("POST") @@ -1527,14 +718,14 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before archive lookup + assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_create_tag_empty_path() { let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let response = app(registry, auth_path) + let (registry, _) = make_test_registry(&dir); + let response = app(registry) .oneshot( Request::builder() .method("POST") @@ -1545,29 +736,28 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before validation + assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_tag_round_trip() { let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let session_cookie = make_test_session(&auth_path); + let (registry, _) = make_test_registry(&dir); - let create_response = app(registry.clone(), auth_path.clone()) + let create_response = app(registry.clone()) .oneshot( Request::builder() .method("POST") .uri("/api/archives/test/tags") .header("content-type", "application/json") - .header("cookie", &session_cookie) .body(json_body(&serde_json::json!({"path": "/science"}))) .unwrap(), ) .await .unwrap(); assert_eq!(create_response.status(), StatusCode::CREATED); - let list_response = app(registry.clone(), auth_path.clone()) + + let list_response = app(registry.clone()) .oneshot( Request::builder() .uri("/api/archives/test/tags") @@ -1590,20 +780,18 @@ mod tests { #[tokio::test] async fn test_entry_tag_assign_and_remove() { let dir = tempfile::tempdir().unwrap(); - let (registry, archive_path, auth_path) = make_test_registry(&dir); + let (registry, archive_path) = make_test_registry(&dir); let entry = make_test_entry(&archive_path); let entry_uid = entry.entry_uid.clone(); let entry_tags_uri = format!("/api/archives/test/entries/{entry_uid}/tags"); - let session_cookie = make_test_session(&auth_path); // Assign tag - let assign_response = app(registry.clone(), auth_path.clone()) + let assign_response = app(registry.clone()) .oneshot( Request::builder() .method("POST") .uri(&entry_tags_uri) .header("content-type", "application/json") - .header("cookie", &session_cookie) .body(json_body(&serde_json::json!({"tag_path": "/science"}))) .unwrap(), ) @@ -1614,7 +802,7 @@ mod tests { let tag_uid = assigned_tag["tag_uid"].as_str().unwrap().to_string(); // List entry tags — should contain the assigned tag - let list_response = app(registry.clone(), auth_path.clone()) + let list_response = app(registry.clone()) .oneshot( Request::builder() .uri(&entry_tags_uri) @@ -1629,12 +817,11 @@ mod tests { // Remove tag let delete_uri = format!("{entry_tags_uri}/{tag_uid}"); - let delete_response = app(registry.clone(), auth_path.clone()) + let delete_response = app(registry.clone()) .oneshot( Request::builder() .method("DELETE") .uri(&delete_uri) - .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1643,7 +830,7 @@ mod tests { assert_eq!(delete_response.status(), StatusCode::NO_CONTENT); // List entry tags again — should be empty - let list2_response = app(registry.clone(), auth_path.clone()) + let list2_response = app(registry.clone()) .oneshot( Request::builder() .uri(&entry_tags_uri) @@ -1660,19 +847,17 @@ mod tests { #[tokio::test] async fn test_search_with_tag_param() { let dir = tempfile::tempdir().unwrap(); - let (registry, archive_path, auth_path) = make_test_registry(&dir); + let (registry, archive_path) = make_test_registry(&dir); let entry = make_test_entry(&archive_path); let entry_uid = entry.entry_uid.clone(); - let session_cookie = make_test_session(&auth_path); // Assign /science tag to entry - let assign_resp = app(registry.clone(), auth_path.clone()) + let assign_resp = app(registry.clone()) .oneshot( Request::builder() .method("POST") .uri(format!("/api/archives/test/entries/{entry_uid}/tags")) .header("content-type", "application/json") - .header("cookie", &session_cookie) .body(json_body(&serde_json::json!({"tag_path": "/science"}))) .unwrap(), ) @@ -1680,12 +865,11 @@ mod tests { .unwrap(); assert_eq!(assign_resp.status(), StatusCode::CREATED, "assign tag should return 201"); - // Search with ?tag=/science — entry should appear (requires auth since entry is private) - let response = app(registry.clone(), auth_path.clone()) + // Search with ?tag=/science — entry should appear + let response = app(registry.clone()) .oneshot( Request::builder() .uri("/api/archives/test/entries/search?tag=%2Fscience") - .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1701,11 +885,10 @@ mod tests { ); // Search with ?tag=/art — should return empty - let response2 = app(registry.clone(), auth_path.clone()) + let response2 = app(registry.clone()) .oneshot( Request::builder() .uri("/api/archives/test/entries/search?tag=%2Fart") - .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1722,8 +905,8 @@ mod tests { #[tokio::test] async fn test_list_entry_tags_unknown_entry() { let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let response = app(registry, auth_path) + let (registry, _) = make_test_registry(&dir); + let response = app(registry) .oneshot( Request::builder() .uri("/api/archives/test/entries/ghost_uid/tags") @@ -1738,8 +921,8 @@ mod tests { #[tokio::test] async fn test_assign_entry_tag_unknown_entry() { let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let response = app(registry, auth_path) + let (registry, _) = make_test_registry(&dir); + let response = app(registry) .oneshot( Request::builder() .method("POST") @@ -1750,16 +933,16 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before entry lookup + assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_assign_entry_tag_empty_tag_path() { let dir = tempfile::tempdir().unwrap(); - let (registry, archive_path, auth_path) = make_test_registry(&dir); + let (registry, archive_path) = make_test_registry(&dir); let entry = make_test_entry(&archive_path); let entry_uid = entry.entry_uid.clone(); - let response = app(registry, auth_path) + let response = app(registry) .oneshot( Request::builder() .method("POST") @@ -1770,14 +953,14 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before validation + assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_remove_entry_tag_unknown_entry() { let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let response = app(registry, auth_path) + let (registry, _) = make_test_registry(&dir); + let response = app(registry) .oneshot( Request::builder() .method("DELETE") @@ -1787,13 +970,12 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before entry lookup + assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn capture_rejects_empty_locator() { - let (test_app, _dir) = make_test_app(); - let response = test_app + let response = app(ServerRegistry::default()) .oneshot( Request::builder() .method("POST") @@ -1804,13 +986,12 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before validation + assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn capture_rejects_unknown_archive() { - let (test_app, _dir) = make_test_app(); - let response = test_app + let response = app(ServerRegistry::default()) .oneshot( Request::builder() .method("POST") @@ -1821,276 +1002,6 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before archive lookup - } - - #[tokio::test] - async fn get_blob_returns_404_for_unknown_sha256() { - 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 auth_path = dir.path().join("auth.sqlite"); - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); - } - let registry = ServerRegistry { - archives: vec![MountedArchive { - id: "test".to_string(), - label: "Test".to_string(), - archive_path, - }], - bind: None, - auth_db_path: None, - }; - let response = app(registry, auth_path) - .oneshot( - Request::builder() - .uri("/api/archives/test/blobs/0000000000000000000000000000000000000000000000000000000000000000") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } - - #[tokio::test] - async fn setup_required_before_owner_created() { - let (test_app, _dir) = make_setup_test_app(); - let response = test_app - .oneshot(Request::builder().uri("/api/auth/setup").body(Body::empty()).unwrap()) - .await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); - let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(json["setup_required"], true); - } - - #[tokio::test] - async fn setup_post_returns_409_on_repeat() { - let dir = tempfile::tempdir().unwrap(); - let auth_path = dir.path().join("auth.sqlite"); - // Seed an owner directly so the second POST hits CONFLICT - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - archivr_core::database::create_owner(&conn, "owner", "dummy").unwrap(); - } - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - let second_app = app(registry, auth_path); - let r2 = second_app - .oneshot(Request::builder().method("POST").uri("/api/auth/setup") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner2","password":"hunter2!"}"#)) - .unwrap()) - .await.unwrap(); - assert_eq!(r2.status(), StatusCode::CONFLICT); - } - - #[tokio::test] - async fn login_wrong_password_returns_401() { - let dir = tempfile::tempdir().unwrap(); - let auth_path = dir.path().join("auth.sqlite"); - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - let hash = crate::auth::hash_password("correct_password").unwrap(); - archivr_core::database::create_owner(&conn, "owner", &hash).unwrap(); - } - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - let response = app(registry, auth_path) - .oneshot(Request::builder().method("POST").uri("/api/auth/login") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner","password":"wrong"}"#)) - .unwrap()) - .await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn create_token_requires_auth() { - let (test_app, _dir) = make_test_app(); - let response = test_app - .oneshot(Request::builder().method("POST").uri("/api/auth/tokens") - .header("content-type", "application/json") - .body(Body::from(r#"{"name":"my token"}"#)) - .unwrap()) - .await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn capture_returns_401_for_unauthenticated() { - let (test_app, _dir) = make_test_app(); - let response = test_app - .oneshot(Request::builder().method("POST") - .uri("/api/archives/test/captures") - .header("content-type", "application/json") - .body(Body::from(r#"{"locator":"https://example.com"}"#)) - .unwrap()) - .await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - - #[tokio::test] - async fn capture_post_returns_accepted_with_job_uid() { - let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let session_cookie = make_test_session(&auth_path); - let response = app(registry, auth_path) - .oneshot( - Request::builder() - .method("POST") - .uri("/api/archives/test/captures") - .header("content-type", "application/json") - .header("cookie", &session_cookie) - .body(Body::from(r#"{"locator":"local:/nonexistent"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::ACCEPTED); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); - let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert!(json["job_uid"].as_str().is_some(), "response must have job_uid"); - assert_eq!(json["status"], "pending"); - } - - #[tokio::test] - async fn admin_users_requires_admin_role() { - let (test_app, _dir) = make_test_app(); - let response = test_app.oneshot( - Request::builder().uri("/api/admin/users").body(Body::empty()).unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn admin_list_users_returns_ok_for_admin() { - let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let session_cookie = make_test_session(&auth_path); - let response = app(registry, auth_path).oneshot( - Request::builder().uri("/api/admin/users") - .header("cookie", &session_cookie) - .body(Body::empty()).unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - #[tokio::test] - async fn auth_me_returns_display_name_field() { - let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let session_cookie = make_test_session(&auth_path); - let response = app(registry, auth_path).oneshot( - Request::builder().uri("/api/auth/me") - .header("cookie", &session_cookie) - .body(Body::empty()).unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); - let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert!(json.get("display_name").is_some(), "auth/me must include display_name field"); - assert!(json.get("username").is_some()); - } - - #[tokio::test] - async fn patch_me_updates_display_name() { - let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let session_cookie = make_test_session(&auth_path); - let response = app(registry, auth_path).oneshot( - Request::builder().method("PATCH").uri("/api/auth/me") - .header("content-type", "application/json") - .header("cookie", &session_cookie) - .body(Body::from(r#"{"display_name":"Test Owner"}"#)) - .unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::NO_CONTENT); - } - - #[tokio::test] - async fn patch_me_requires_auth() { - let (test_app, _dir) = make_test_app(); - let response = test_app.oneshot( - Request::builder().method("PATCH").uri("/api/auth/me") - .header("content-type", "application/json") - .body(Body::from(r#"{"display_name":"anon"}"#)) - .unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn patch_me_rejects_wrong_current_password() { - let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - // Set a real password hash on the owner - { - let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); - let hash = crate::auth::hash_password("real_password").unwrap(); - let user_id: i64 = conn.query_row( - "SELECT id FROM users WHERE username = 'testowner'", [], |r| r.get(0) - ).unwrap(); - archivr_core::database::update_user_password(&conn, user_id, &hash).unwrap(); - } - let session_cookie = make_test_session(&auth_path); - let response = app(registry, auth_path).oneshot( - Request::builder().method("PATCH").uri("/api/auth/me") - .header("content-type", "application/json") - .header("cookie", &session_cookie) - .body(Body::from(r#"{"current_password":"wrong","new_password":"newpass"}"#)) - .unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn instance_settings_requires_admin() { - let (test_app, _dir) = make_test_app(); - let response = test_app.oneshot( - Request::builder().uri("/api/admin/instance-settings") - .body(Body::empty()).unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn instance_settings_get_returns_defaults() { - let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let session_cookie = make_test_session(&auth_path); - let response = app(registry, auth_path).oneshot( - Request::builder().uri("/api/admin/instance-settings") - .header("cookie", &session_cookie) - .body(Body::empty()).unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); - let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(json["public_index_enabled"], false); - assert_eq!(json["open_registration_enabled"], false); - } - - #[tokio::test] - async fn instance_settings_patch_updates_fields() { - let dir = tempfile::tempdir().unwrap(); - let (registry, _, auth_path) = make_test_registry(&dir); - let session_cookie = make_test_session(&auth_path); - let response = app(registry, auth_path).oneshot( - Request::builder().method("PATCH").uri("/api/admin/instance-settings") - .header("content-type", "application/json") - .header("cookie", &session_cookie) - .body(Body::from(r#"{"open_registration_enabled":true}"#)) - .unwrap() - ).await.unwrap(); - assert_eq!(response.status(), StatusCode::NO_CONTENT); - } } diff --git a/crates/archivr-server/static/assets/index-BfwsUa2j.js b/crates/archivr-server/static/assets/index-BfwsUa2j.js deleted file mode 100644 index 6632729..0000000 --- a/crates/archivr-server/static/assets/index-BfwsUa2j.js +++ /dev/null @@ -1,40 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const u of i.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var Zo={exports:{}},ol={},qo={exports:{}},D={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var rr=Symbol.for("react.element"),pc=Symbol.for("react.portal"),hc=Symbol.for("react.fragment"),mc=Symbol.for("react.strict_mode"),vc=Symbol.for("react.profiler"),gc=Symbol.for("react.provider"),yc=Symbol.for("react.context"),wc=Symbol.for("react.forward_ref"),Sc=Symbol.for("react.suspense"),xc=Symbol.for("react.memo"),kc=Symbol.for("react.lazy"),Uu=Symbol.iterator;function Cc(e){return e===null||typeof e!="object"?null:(e=Uu&&e[Uu]||e["@@iterator"],typeof e=="function"?e:null)}var bo={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},es=Object.assign,ts={};function vn(e,t,n){this.props=e,this.context=t,this.refs=ts,this.updater=n||bo}vn.prototype.isReactComponent={};vn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};vn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ns(){}ns.prototype=vn.prototype;function Hi(e,t,n){this.props=e,this.context=t,this.refs=ts,this.updater=n||bo}var Wi=Hi.prototype=new ns;Wi.constructor=Hi;es(Wi,vn.prototype);Wi.isPureReactComponent=!0;var Iu=Array.isArray,rs=Object.prototype.hasOwnProperty,Qi={current:null},ls={key:!0,ref:!0,__self:!0,__source:!0};function is(e,t,n){var r,l={},i=null,u=null;if(t!=null)for(r in t.ref!==void 0&&(u=t.ref),t.key!==void 0&&(i=""+t.key),t)rs.call(t,r)&&!ls.hasOwnProperty(r)&&(l[r]=t[r]);var o=arguments.length-2;if(o===1)l.children=n;else if(1>>1,B=E[U];if(0>>1;Ul(Oe,R))Bel(De,Oe)?(E[U]=De,E[Be]=R,U=Be):(E[U]=Oe,E[M]=R,U=M);else if(Bel(De,R))E[U]=De,E[Be]=R,U=Be;else break e}}return k}function l(E,k){var R=E.sortIndex-k.sortIndex;return R!==0?R:E.id-k.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var u=Date,o=u.now();e.unstable_now=function(){return u.now()-o}}var s=[],f=[],v=1,m=null,h=3,y=!1,w=!1,C=!1,T=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(E){for(var k=n(f);k!==null;){if(k.callback===null)r(f);else if(k.startTime<=E)r(f),k.sortIndex=k.expirationTime,t(s,k);else break;k=n(f)}}function g(E){if(C=!1,p(E),!w)if(n(s)!==null)w=!0,it(N);else{var k=n(f);k!==null&&jt(g,k.startTime-E)}}function N(E,k){w=!1,C&&(C=!1,d(z),z=-1),y=!0;var R=h;try{for(p(k),m=n(s);m!==null&&(!(m.expirationTime>k)||E&&!X());){var U=m.callback;if(typeof U=="function"){m.callback=null,h=m.priorityLevel;var B=U(m.expirationTime<=k);k=e.unstable_now(),typeof B=="function"?m.callback=B:m===n(s)&&r(s),p(k)}else r(s);m=n(s)}if(m!==null)var Bt=!0;else{var M=n(f);M!==null&&jt(g,M.startTime-k),Bt=!1}return Bt}finally{m=null,h=R,y=!1}}var _=!1,j=null,z=-1,L=5,O=-1;function X(){return!(e.unstable_now()-OE||125U?(E.sortIndex=R,t(f,E),n(s)===null&&E===n(f)&&(C?(d(z),z=-1):C=!0,jt(g,R-U))):(E.sortIndex=B,t(s,E),w||y||(w=!0,it(N))),E},e.unstable_shouldYield=X,e.unstable_wrapCallback=function(E){var k=h;return function(){var R=h;h=k;try{return E.apply(this,arguments)}finally{h=R}}}})(cs);as.exports=cs;var Dc=as.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Mc=S,xe=Dc;function x(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Zl=Object.prototype.hasOwnProperty,Fc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Vu={},Bu={};function $c(e){return Zl.call(Bu,e)?!0:Zl.call(Vu,e)?!1:Fc.test(e)?Bu[e]=!0:(Vu[e]=!0,!1)}function Uc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Ic(e,t,n,r){if(t===null||typeof t>"u"||Uc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function fe(e,t,n,r,l,i,u){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=u}var re={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){re[e]=new fe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];re[t]=new fe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){re[e]=new fe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){re[e]=new fe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){re[e]=new fe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){re[e]=new fe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){re[e]=new fe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){re[e]=new fe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){re[e]=new fe(e,5,!1,e.toLowerCase(),null,!1,!1)});var Yi=/[\-:]([a-z])/g;function Xi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Yi,Xi);re[t]=new fe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Yi,Xi);re[t]=new fe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Yi,Xi);re[t]=new fe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){re[e]=new fe(e,1,!1,e.toLowerCase(),null,!1,!1)});re.xlinkHref=new fe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){re[e]=new fe(e,1,!1,e.toLowerCase(),null,!0,!0)});function Gi(e,t,n,r){var l=re.hasOwnProperty(t)?re[t]:null;(l!==null?l.type!==0:r||!(2o||l[u]!==i[o]){var s=` -`+l[u].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=u&&0<=o);break}}}finally{El=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?_n(e):""}function Ac(e){switch(e.tag){case 5:return _n(e.type);case 16:return _n("Lazy");case 13:return _n("Suspense");case 19:return _n("SuspenseList");case 0:case 2:case 15:return e=jl(e.type,!1),e;case 11:return e=jl(e.type.render,!1),e;case 1:return e=jl(e.type,!0),e;default:return""}}function ti(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Qt:return"Fragment";case Wt:return"Portal";case ql:return"Profiler";case Ji:return"StrictMode";case bl:return"Suspense";case ei:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ps:return(e.displayName||"Context")+".Consumer";case ds:return(e._context.displayName||"Context")+".Provider";case Zi:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case qi:return t=e.displayName||null,t!==null?t:ti(e.type)||"Memo";case ot:t=e._payload,e=e._init;try{return ti(e(t))}catch{}}return null}function Vc(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ti(t);case 8:return t===Ji?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function xt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ms(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Bc(e){var t=ms(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(u){r=""+u,i.call(this,u)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(u){r=""+u},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function cr(e){e._valueTracker||(e._valueTracker=Bc(e))}function vs(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ms(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $r(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ni(e,t){var n=t.checked;return K({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Wu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=xt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function gs(e,t){t=t.checked,t!=null&&Gi(e,"checked",t,!1)}function ri(e,t){gs(e,t);var n=xt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?li(e,t.type,n):t.hasOwnProperty("defaultValue")&&li(e,t.type,xt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Qu(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function li(e,t,n){(t!=="number"||$r(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Pn=Array.isArray;function nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=fr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Vn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ln={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Hc=["Webkit","ms","Moz","O"];Object.keys(Ln).forEach(function(e){Hc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ln[t]=Ln[e]})});function xs(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Ln.hasOwnProperty(e)&&Ln[e]?(""+t).trim():t+"px"}function ks(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=xs(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Wc=K({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function oi(e,t){if(t){if(Wc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(x(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(x(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(x(61))}if(t.style!=null&&typeof t.style!="object")throw Error(x(62))}}function si(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ai=null;function bi(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ci=null,rn=null,ln=null;function Xu(e){if(e=ur(e)){if(typeof ci!="function")throw Error(x(280));var t=e.stateNode;t&&(t=dl(t),ci(e.stateNode,e.type,t))}}function Cs(e){rn?ln?ln.push(e):ln=[e]:rn=e}function Ns(){if(rn){var e=rn,t=ln;if(ln=rn=null,Xu(e),t)for(e=0;e>>=0,e===0?32:31-(tf(e)/nf|0)|0}var dr=64,pr=4194304;function Tn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Vr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,u=n&268435455;if(u!==0){var o=u&~l;o!==0?r=Tn(o):(i&=u,i!==0&&(r=Tn(i)))}else u=n&~l,u!==0?r=Tn(u):i!==0&&(r=Tn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function lr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ie(t),e[t]=n}function of(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=On),ro=" ",lo=!1;function Ws(e,t){switch(e){case"keyup":return Mf.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Qs(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Kt=!1;function $f(e,t){switch(e){case"compositionend":return Qs(t);case"keypress":return t.which!==32?null:(lo=!0,ro);case"textInput":return e=t.data,e===ro&&lo?null:e;default:return null}}function Uf(e,t){if(Kt)return e==="compositionend"||!ou&&Ws(e,t)?(e=Bs(),Pr=lu=ft=null,Kt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=so(n)}}function Gs(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Gs(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Js(){for(var e=window,t=$r();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=$r(e.document)}return t}function su(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Yf(e){var t=Js(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Gs(n.ownerDocument.documentElement,n)){if(r!==null&&su(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=ao(n,i);var u=ao(n,r);l&&u&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==u.node||e.focusOffset!==u.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(u.node,u.offset)):(t.setEnd(u.node,u.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Yt=null,vi=null,Mn=null,gi=!1;function co(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;gi||Yt==null||Yt!==$r(r)||(r=Yt,"selectionStart"in r&&su(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Mn&&Yn(Mn,r)||(Mn=r,r=Wr(vi,"onSelect"),0Jt||(e.current=Ci[Jt],Ci[Jt]=null,Jt--)}function I(e,t){Jt++,Ci[Jt]=e.current,e.current=t}var kt={},oe=Nt(kt),he=Nt(!1),Mt=kt;function cn(e,t){var n=e.type.contextTypes;if(!n)return kt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Kr(){V(he),V(oe)}function yo(e,t,n){if(oe.current!==kt)throw Error(x(168));I(oe,t),I(he,n)}function ia(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(x(108,Vc(e)||"Unknown",l));return K({},n,r)}function Yr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||kt,Mt=oe.current,I(oe,e),I(he,he.current),!0}function wo(e,t,n){var r=e.stateNode;if(!r)throw Error(x(169));n?(e=ia(e,t,Mt),r.__reactInternalMemoizedMergedChildContext=e,V(he),V(oe),I(oe,e)):V(he),I(he,n)}var Ge=null,pl=!1,Al=!1;function ua(e){Ge===null?Ge=[e]:Ge.push(e)}function id(e){pl=!0,ua(e)}function Et(){if(!Al&&Ge!==null){Al=!0;var e=0,t=$;try{var n=Ge;for($=1;e>=u,l-=u,Je=1<<32-Ie(t)+l|n<z?(L=j,j=null):L=j.sibling;var O=h(d,j,p[z],g);if(O===null){j===null&&(j=L);break}e&&j&&O.alternate===null&&t(d,j),c=i(O,c,z),_===null?N=O:_.sibling=O,_=O,j=L}if(z===p.length)return n(d,j),H&&_t(d,z),N;if(j===null){for(;zz?(L=j,j=null):L=j.sibling;var X=h(d,j,O.value,g);if(X===null){j===null&&(j=L);break}e&&j&&X.alternate===null&&t(d,j),c=i(X,c,z),_===null?N=X:_.sibling=X,_=X,j=L}if(O.done)return n(d,j),H&&_t(d,z),N;if(j===null){for(;!O.done;z++,O=p.next())O=m(d,O.value,g),O!==null&&(c=i(O,c,z),_===null?N=O:_.sibling=O,_=O);return H&&_t(d,z),N}for(j=r(d,j);!O.done;z++,O=p.next())O=y(j,d,z,O.value,g),O!==null&&(e&&O.alternate!==null&&j.delete(O.key===null?z:O.key),c=i(O,c,z),_===null?N=O:_.sibling=O,_=O);return e&&j.forEach(function(ge){return t(d,ge)}),H&&_t(d,z),N}function T(d,c,p,g){if(typeof p=="object"&&p!==null&&p.type===Qt&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case ar:e:{for(var N=p.key,_=c;_!==null;){if(_.key===N){if(N=p.type,N===Qt){if(_.tag===7){n(d,_.sibling),c=l(_,p.props.children),c.return=d,d=c;break e}}else if(_.elementType===N||typeof N=="object"&&N!==null&&N.$$typeof===ot&&ko(N)===_.type){n(d,_.sibling),c=l(_,p.props),c.ref=Nn(d,_,p),c.return=d,d=c;break e}n(d,_);break}else t(d,_);_=_.sibling}p.type===Qt?(c=Dt(p.props.children,d.mode,g,p.key),c.return=d,d=c):(g=Fr(p.type,p.key,p.props,null,d.mode,g),g.ref=Nn(d,c,p),g.return=d,d=g)}return u(d);case Wt:e:{for(_=p.key;c!==null;){if(c.key===_)if(c.tag===4&&c.stateNode.containerInfo===p.containerInfo&&c.stateNode.implementation===p.implementation){n(d,c.sibling),c=l(c,p.children||[]),c.return=d,d=c;break e}else{n(d,c);break}else t(d,c);c=c.sibling}c=Xl(p,d.mode,g),c.return=d,d=c}return u(d);case ot:return _=p._init,T(d,c,_(p._payload),g)}if(Pn(p))return w(d,c,p,g);if(wn(p))return C(d,c,p,g);Sr(d,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,c!==null&&c.tag===6?(n(d,c.sibling),c=l(c,p),c.return=d,d=c):(n(d,c),c=Yl(p,d.mode,g),c.return=d,d=c),u(d)):n(d,c)}return T}var dn=ca(!0),fa=ca(!1),Jr=Nt(null),Zr=null,bt=null,du=null;function pu(){du=bt=Zr=null}function hu(e){var t=Jr.current;V(Jr),e._currentValue=t}function ji(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function on(e,t){Zr=e,du=bt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(pe=!0),e.firstContext=null)}function Te(e){var t=e._currentValue;if(du!==e)if(e={context:e,memoizedValue:t,next:null},bt===null){if(Zr===null)throw Error(x(308));bt=e,Zr.dependencies={lanes:0,firstContext:e}}else bt=bt.next=e;return t}var zt=null;function mu(e){zt===null?zt=[e]:zt.push(e)}function da(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,mu(t)):(n.next=l.next,l.next=n),t.interleaved=n,tt(e,r)}function tt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var st=!1;function vu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function pa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function qe(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function gt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,F&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,tt(e,n)}return l=r.interleaved,l===null?(t.next=t,mu(r)):(t.next=l.next,l.next=t),r.interleaved=t,tt(e,n)}function zr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,tu(e,n)}}function Co(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var u={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=u:i=i.next=u,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function qr(e,t,n,r){var l=e.updateQueue;st=!1;var i=l.firstBaseUpdate,u=l.lastBaseUpdate,o=l.shared.pending;if(o!==null){l.shared.pending=null;var s=o,f=s.next;s.next=null,u===null?i=f:u.next=f,u=s;var v=e.alternate;v!==null&&(v=v.updateQueue,o=v.lastBaseUpdate,o!==u&&(o===null?v.firstBaseUpdate=f:o.next=f,v.lastBaseUpdate=s))}if(i!==null){var m=l.baseState;u=0,v=f=s=null,o=i;do{var h=o.lane,y=o.eventTime;if((r&h)===h){v!==null&&(v=v.next={eventTime:y,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var w=e,C=o;switch(h=t,y=n,C.tag){case 1:if(w=C.payload,typeof w=="function"){m=w.call(y,m,h);break e}m=w;break e;case 3:w.flags=w.flags&-65537|128;case 0:if(w=C.payload,h=typeof w=="function"?w.call(y,m,h):w,h==null)break e;m=K({},m,h);break e;case 2:st=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[o]:h.push(o))}else y={eventTime:y,lane:h,tag:o.tag,payload:o.payload,callback:o.callback,next:null},v===null?(f=v=y,s=m):v=v.next=y,u|=h;if(o=o.next,o===null){if(o=l.shared.pending,o===null)break;h=o,o=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(v===null&&(s=m),l.baseState=s,l.firstBaseUpdate=f,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do u|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Ut|=u,e.lanes=u,e.memoizedState=m}}function No(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Bl.transition;Bl.transition={};try{e(!1),t()}finally{$=n,Bl.transition=r}}function za(){return ze().memoizedState}function ad(e,t,n){var r=wt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},La(e))Ra(t,n);else if(n=da(e,t,n,r),n!==null){var l=ae();Ae(n,e,r,l),Oa(n,t,r)}}function cd(e,t,n){var r=wt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(La(e))Ra(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var u=t.lastRenderedState,o=i(u,n);if(l.hasEagerState=!0,l.eagerState=o,Ve(o,u)){var s=t.interleaved;s===null?(l.next=l,mu(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=da(e,t,l,r),n!==null&&(l=ae(),Ae(n,e,r,l),Oa(n,t,r))}}function La(e){var t=e.alternate;return e===Q||t!==null&&t===Q}function Ra(e,t){Fn=el=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Oa(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,tu(e,n)}}var tl={readContext:Te,useCallback:le,useContext:le,useEffect:le,useImperativeHandle:le,useInsertionEffect:le,useLayoutEffect:le,useMemo:le,useReducer:le,useRef:le,useState:le,useDebugValue:le,useDeferredValue:le,useTransition:le,useMutableSource:le,useSyncExternalStore:le,useId:le,unstable_isNewReconciler:!1},fd={readContext:Te,useCallback:function(e,t){return We().memoizedState=[e,t===void 0?null:t],e},useContext:Te,useEffect:jo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Rr(4194308,4,Ea.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Rr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Rr(4,2,e,t)},useMemo:function(e,t){var n=We();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=We();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=ad.bind(null,Q,e),[r.memoizedState,e]},useRef:function(e){var t=We();return e={current:e},t.memoizedState=e},useState:Eo,useDebugValue:Nu,useDeferredValue:function(e){return We().memoizedState=e},useTransition:function(){var e=Eo(!1),t=e[0];return e=sd.bind(null,e[1]),We().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Q,l=We();if(H){if(n===void 0)throw Error(x(407));n=n()}else{if(n=t(),ee===null)throw Error(x(349));$t&30||ga(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,jo(wa.bind(null,r,i,e),[e]),r.flags|=2048,tr(9,ya.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=We(),t=ee.identifierPrefix;if(H){var n=Ze,r=Je;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=bn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=u.createElement(n,{is:r.is}):(e=u.createElement(n),n==="select"&&(u=e,r.multiple?u.multiple=!0:r.size&&(u.size=r.size))):e=u.createElementNS(e,n),e[Qe]=t,e[Jn]=r,Ha(e,t,!1,!1),t.stateNode=e;e:{switch(u=si(n,r),n){case"dialog":A("cancel",e),A("close",e),l=r;break;case"iframe":case"object":case"embed":A("load",e),l=r;break;case"video":case"audio":for(l=0;lmn&&(t.flags|=128,r=!0,En(i,!1),t.lanes=4194304)}else{if(!r)if(e=br(u),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),En(i,!0),i.tail===null&&i.tailMode==="hidden"&&!u.alternate&&!H)return ie(t),null}else 2*G()-i.renderingStartTime>mn&&n!==1073741824&&(t.flags|=128,r=!0,En(i,!1),t.lanes=4194304);i.isBackwards?(u.sibling=t.child,t.child=u):(n=i.last,n!==null?n.sibling=u:t.child=u,i.last=u)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=G(),t.sibling=null,n=W.current,I(W,r?n&1|2:n&1),t):(ie(t),null);case 22:case 23:return zu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ye&1073741824&&(ie(t),t.subtreeFlags&6&&(t.flags|=8192)):ie(t),null;case 24:return null;case 25:return null}throw Error(x(156,t.tag))}function wd(e,t){switch(cu(t),t.tag){case 1:return me(t.type)&&Kr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return pn(),V(he),V(oe),wu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return yu(t),null;case 13:if(V(W),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(x(340));fn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return V(W),null;case 4:return pn(),null;case 10:return hu(t.type._context),null;case 22:case 23:return zu(),null;case 24:return null;default:return null}}var kr=!1,ue=!1,Sd=typeof WeakSet=="function"?WeakSet:Set,P=null;function en(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Y(e,t,r)}else n.current=null}function Mi(e,t,n){try{n()}catch(r){Y(e,t,r)}}var $o=!1;function xd(e,t){if(yi=Br,e=Js(),su(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var u=0,o=-1,s=-1,f=0,v=0,m=e,h=null;t:for(;;){for(var y;m!==n||l!==0&&m.nodeType!==3||(o=u+l),m!==i||r!==0&&m.nodeType!==3||(s=u+r),m.nodeType===3&&(u+=m.nodeValue.length),(y=m.firstChild)!==null;)h=m,m=y;for(;;){if(m===e)break t;if(h===n&&++f===l&&(o=u),h===i&&++v===r&&(s=u),(y=m.nextSibling)!==null)break;m=h,h=m.parentNode}m=y}n=o===-1||s===-1?null:{start:o,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(wi={focusedElem:e,selectionRange:n},Br=!1,P=t;P!==null;)if(t=P,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,P=e;else for(;P!==null;){t=P;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var C=w.memoizedProps,T=w.memoizedState,d=t.stateNode,c=d.getSnapshotBeforeUpdate(t.elementType===t.type?C:Fe(t.type,C),T);d.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(x(163))}}catch(g){Y(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,P=e;break}P=t.return}return w=$o,$o=!1,w}function $n(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Mi(t,n,i)}l=l.next}while(l!==r)}}function vl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Fi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Ka(e){var t=e.alternate;t!==null&&(e.alternate=null,Ka(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Qe],delete t[Jn],delete t[ki],delete t[rd],delete t[ld])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Ya(e){return e.tag===5||e.tag===3||e.tag===4}function Uo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ya(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function $i(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Qr));else if(r!==4&&(e=e.child,e!==null))for($i(e,t,n),e=e.sibling;e!==null;)$i(e,t,n),e=e.sibling}function Ui(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Ui(e,t,n),e=e.sibling;e!==null;)Ui(e,t,n),e=e.sibling}var te=null,$e=!1;function ut(e,t,n){for(n=n.child;n!==null;)Xa(e,t,n),n=n.sibling}function Xa(e,t,n){if(Ke&&typeof Ke.onCommitFiberUnmount=="function")try{Ke.onCommitFiberUnmount(sl,n)}catch{}switch(n.tag){case 5:ue||en(n,t);case 6:var r=te,l=$e;te=null,ut(e,t,n),te=r,$e=l,te!==null&&($e?(e=te,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):te.removeChild(n.stateNode));break;case 18:te!==null&&($e?(e=te,n=n.stateNode,e.nodeType===8?Il(e.parentNode,n):e.nodeType===1&&Il(e,n),Qn(e)):Il(te,n.stateNode));break;case 4:r=te,l=$e,te=n.stateNode.containerInfo,$e=!0,ut(e,t,n),te=r,$e=l;break;case 0:case 11:case 14:case 15:if(!ue&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,u=i.destroy;i=i.tag,u!==void 0&&(i&2||i&4)&&Mi(n,t,u),l=l.next}while(l!==r)}ut(e,t,n);break;case 1:if(!ue&&(en(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){Y(n,t,o)}ut(e,t,n);break;case 21:ut(e,t,n);break;case 22:n.mode&1?(ue=(r=ue)||n.memoizedState!==null,ut(e,t,n),ue=r):ut(e,t,n);break;default:ut(e,t,n)}}function Io(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Sd),t.forEach(function(r){var l=zd.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Me(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=u),r&=~i}if(r=l,r=G()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Cd(r/1960))-r,10e?16:e,dt===null)var r=!1;else{if(e=dt,dt=null,ll=0,F&6)throw Error(x(331));var l=F;for(F|=4,P=e.current;P!==null;){var i=P,u=i.child;if(P.flags&16){var o=i.deletions;if(o!==null){for(var s=0;sG()-Pu?Ot(e,0):_u|=n),ve(e,t)}function nc(e,t){t===0&&(e.mode&1?(t=pr,pr<<=1,!(pr&130023424)&&(pr=4194304)):t=1);var n=ae();e=tt(e,t),e!==null&&(lr(e,t,n),ve(e,n))}function Td(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),nc(e,n)}function zd(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(x(314))}r!==null&&r.delete(t),nc(e,n)}var rc;rc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||he.current)pe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return pe=!1,gd(e,t,n);pe=!!(e.flags&131072)}else pe=!1,H&&t.flags&1048576&&oa(t,Gr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Or(e,t),e=t.pendingProps;var l=cn(t,oe.current);on(t,n),l=xu(null,t,r,e,l,n);var i=ku();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,me(r)?(i=!0,Yr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,vu(t),l.updater=ml,t.stateNode=l,l._reactInternals=t,Pi(t,r,e,n),t=Li(null,t,r,!0,i,n)):(t.tag=0,H&&i&&au(t),se(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Or(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Rd(r),e=Fe(r,e),l){case 0:t=zi(null,t,r,e,n);break e;case 1:t=Do(null,t,r,e,n);break e;case 11:t=Ro(null,t,r,e,n);break e;case 14:t=Oo(null,t,r,Fe(r.type,e),n);break e}throw Error(x(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),zi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Do(e,t,r,l,n);case 3:e:{if(Aa(t),e===null)throw Error(x(387));r=t.pendingProps,i=t.memoizedState,l=i.element,pa(e,t),qr(t,r,null,n);var u=t.memoizedState;if(r=u.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:u.cache,pendingSuspenseBoundaries:u.pendingSuspenseBoundaries,transitions:u.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=hn(Error(x(423)),t),t=Mo(e,t,r,n,l);break e}else if(r!==l){l=hn(Error(x(424)),t),t=Mo(e,t,r,n,l);break e}else for(we=vt(t.stateNode.containerInfo.firstChild),Se=t,H=!0,Ue=null,n=fa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(fn(),r===l){t=nt(e,t,n);break e}se(e,t,r,n)}t=t.child}return t;case 5:return ha(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,u=l.children,Si(r,l)?u=null:i!==null&&Si(r,i)&&(t.flags|=32),Ia(e,t),se(e,t,u,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return Va(e,t,n);case 4:return gu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=dn(t,null,r,n):se(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Ro(e,t,r,l,n);case 7:return se(e,t,t.pendingProps,n),t.child;case 8:return se(e,t,t.pendingProps.children,n),t.child;case 12:return se(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,u=l.value,I(Jr,r._currentValue),r._currentValue=u,i!==null)if(Ve(i.value,u)){if(i.children===l.children&&!he.current){t=nt(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var o=i.dependencies;if(o!==null){u=i.child;for(var s=o.firstContext;s!==null;){if(s.context===r){if(i.tag===1){s=qe(-1,n&-n),s.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var v=f.pending;v===null?s.next=s:(s.next=v.next,v.next=s),f.pending=s}}i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),ji(i.return,n,t),o.lanes|=n;break}s=s.next}}else if(i.tag===10)u=i.type===t.type?null:i.child;else if(i.tag===18){if(u=i.return,u===null)throw Error(x(341));u.lanes|=n,o=u.alternate,o!==null&&(o.lanes|=n),ji(u,n,t),u=i.sibling}else u=i.child;if(u!==null)u.return=i;else for(u=i;u!==null;){if(u===t){u=null;break}if(i=u.sibling,i!==null){i.return=u.return,u=i;break}u=u.return}i=u}se(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,on(t,n),l=Te(l),r=r(l),t.flags|=1,se(e,t,r,n),t.child;case 14:return r=t.type,l=Fe(r,t.pendingProps),l=Fe(r.type,l),Oo(e,t,r,l,n);case 15:return $a(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Or(e,t),t.tag=1,me(r)?(e=!0,Yr(t)):e=!1,on(t,n),Da(t,r,l),Pi(t,r,l,n),Li(null,t,r,!0,e,n);case 19:return Ba(e,t,n);case 22:return Ua(e,t,n)}throw Error(x(156,t.tag))};function lc(e,t){return Ls(e,t)}function Ld(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function _e(e,t,n,r){return new Ld(e,t,n,r)}function Ru(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Rd(e){if(typeof e=="function")return Ru(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Zi)return 11;if(e===qi)return 14}return 2}function St(e,t){var n=e.alternate;return n===null?(n=_e(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Fr(e,t,n,r,l,i){var u=2;if(r=e,typeof e=="function")Ru(e)&&(u=1);else if(typeof e=="string")u=5;else e:switch(e){case Qt:return Dt(n.children,l,i,t);case Ji:u=8,l|=8;break;case ql:return e=_e(12,n,t,l|2),e.elementType=ql,e.lanes=i,e;case bl:return e=_e(13,n,t,l),e.elementType=bl,e.lanes=i,e;case ei:return e=_e(19,n,t,l),e.elementType=ei,e.lanes=i,e;case hs:return yl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ds:u=10;break e;case ps:u=9;break e;case Zi:u=11;break e;case qi:u=14;break e;case ot:u=16,r=null;break e}throw Error(x(130,e==null?e:typeof e,""))}return t=_e(u,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Dt(e,t,n,r){return e=_e(7,e,r,t),e.lanes=n,e}function yl(e,t,n,r){return e=_e(22,e,r,t),e.elementType=hs,e.lanes=n,e.stateNode={isHidden:!1},e}function Yl(e,t,n){return e=_e(6,e,null,t),e.lanes=n,e}function Xl(e,t,n){return t=_e(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Od(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Pl(0),this.expirationTimes=Pl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Pl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ou(e,t,n,r,l,i,u,o,s){return e=new Od(e,t,n,o,s),t===1?(t=1,i===!0&&(t|=8)):t=0,i=_e(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},vu(i),e}function Dd(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(sc)}catch(e){console.error(e)}}sc(),ss.exports=ke;var Id=ss.exports,ac,Yo=Id;ac=Yo.createRoot,Yo.hydrateRoot;async function Ne(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function Ad(){return Ne("/api/archives")}async function Vd(e){return Ne(`/api/archives/${e}/entries`)}async function Bd(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),Ne(`/api/archives/${e}/entries/search?${r}`)}async function Hd(e,t){return Ne(`/api/archives/${e}/entries/${t}`)}async function Gl(e,t){return Ne(`/api/archives/${e}/entries/${t}/tags`)}async function Wd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tag_path:n})});if(!r.ok)throw new Error(`Failed to add tag (${r.status})`)}async function Qd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags/${n}`,{method:"DELETE"});if(!r.ok)throw new Error(`Remove failed (${r.status})`)}async function Xo(e){return Ne(`/api/archives/${e}/runs`)}async function Jl(e){return Ne(`/api/archives/${e}/tags`)}async function Kd(e,t){const n=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locator:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function Yd(e,t){return Ne(`/api/archives/${e}/capture_jobs/${t}`)}async function Xd(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function Gd(e,t){const n=await fetch("/api/auth/setup",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Setup failed");return n.json()}async function Jd(e,t){const n=await fetch("/api/auth/login",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Login failed");return n.json()}async function Zd(){await fetch("/api/auth/logout",{method:"POST"})}async function qd(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function bd(){return Ne("/api/admin/users")}async function ep(e,t,n){const r=await fetch("/api/admin/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:t,email:n||void 0})});if(!r.ok){const l=await r.json().catch(()=>({}));throw new Error(l.error||`HTTP ${r.status}`)}return r.json()}async function tp(e,t){const n=await fetch(`/api/admin/users/${e}/status`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function np(){return Ne("/api/admin/roles")}async function rp(e,t){const n=await fetch("/api/admin/roles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:e,name:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function lp(e){return Ne(`/api/archives/${e}/collections`)}async function ip(e,t,n,r=2){const l=await fetch(`/api/archives/${e}/collections`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:t,slug:n,default_visibility_bits:r})});if(!l.ok){const i=await l.json().catch(()=>({error:l.statusText}));throw new Error(i.error||l.statusText)}return l.json()}async function up(e,t){return Ne(`/api/archives/${e}/collections/${t}`)}async function op(e,t){return Ne(`/api/archives/${e}/entries/${t}/collections`)}const sp=window.fetch;window.fetch=async(...e)=>{var n;const t=await sp(...e);return t.status===401&&((typeof e[0]=="string"?e[0]:((n=e[0])==null?void 0:n.url)??"").includes("/api/auth/")||window.dispatchEvent(new CustomEvent("auth:expired"))),t};function ap({onLogin:e}){const[t,n]=S.useState(""),[r,l]=S.useState(""),[i,u]=S.useState(null),[o,s]=S.useState(!1);async function f(v){v.preventDefault(),u(null),s(!0);try{const m=await Jd(t,r);e(m)}catch(m){u(m.message)}finally{s(!1)}}return a.jsxs("div",{className:"login-page",children:[a.jsx("h1",{children:"Archivr"}),a.jsxs("form",{onSubmit:f,children:[a.jsxs("label",{children:["Username",a.jsx("input",{type:"text",value:t,onChange:v=>n(v.target.value),autoFocus:!0,required:!0})]}),a.jsxs("label",{children:["Password",a.jsx("input",{type:"password",value:r,onChange:v=>l(v.target.value),required:!0})]}),i&&a.jsx("p",{className:"error",children:i}),a.jsx("button",{type:"submit",disabled:o,children:o?"Logging in…":"Log in"})]})]})}function cp({onComplete:e}){const[t,n]=S.useState(""),[r,l]=S.useState(""),[i,u]=S.useState(""),[o,s]=S.useState(null),[f,v]=S.useState(!1);async function m(h){if(h.preventDefault(),r!==i){s("Passwords do not match");return}if(r.length<8){s("Password must be at least 8 characters");return}s(null),v(!0);try{await Gd(t,r),e()}catch(y){s(y.message)}finally{v(!1)}}return a.jsxs("div",{className:"setup-page",children:[a.jsx("h1",{children:"Welcome to Archivr"}),a.jsx("p",{children:"Create your owner account to get started."}),a.jsxs("form",{onSubmit:m,children:[a.jsxs("label",{children:["Username",a.jsx("input",{type:"text",value:t,onChange:h=>n(h.target.value),autoFocus:!0,required:!0})]}),a.jsxs("label",{children:["Password",a.jsx("input",{type:"password",value:r,onChange:h=>l(h.target.value),required:!0})]}),a.jsxs("label",{children:["Confirm password",a.jsx("input",{type:"password",value:i,onChange:h=>u(h.target.value),required:!0})]}),o&&a.jsx("p",{className:"error",children:o}),a.jsx("button",{type:"submit",disabled:f,children:f?"Creating account…":"Create account"})]})]})}function fp({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:u,setCurrentUser:o}=S.useContext($u)??{},[s,f]=S.useState(!1);async function v(){f(!0),await Zd(),o(null),window.location.reload()}return a.jsxs("header",{className:"topbar",children:[a.jsx("div",{className:"brand",children:"Archivr"}),a.jsx("select",{className:"archive-switcher","aria-label":"Select archive",value:t??"",onChange:m=>n(m.target.value),children:e.map(m=>a.jsx("option",{value:m.id,children:m.label},m.id))}),a.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","runs","admin","tags","collections"].map(m=>a.jsx("button",{className:`nav-link${r===m?" is-active":""}`,onClick:()=>l(m),children:m.charAt(0).toUpperCase()+m.slice(1)},m))}),a.jsx("button",{className:"capture-button",onClick:i,children:"+ Capture"}),u&&a.jsxs("div",{className:"user-menu",children:[a.jsx("span",{className:"username",children:u.username}),a.jsx("button",{onClick:v,disabled:s,className:"logout-btn",children:s?"Logging out…":"Log out"})]})]})}function dp({open:e,archiveId:t,onClose:n,onCaptured:r}){const l=S.useRef(null),[i,u]=S.useState(""),[o,s]=S.useState(null),[f,v]=S.useState(!1),[m,h]=S.useState(null),y=S.useRef(null);S.useEffect(()=>{const T=l.current;if(!T)return;const d=()=>{clearInterval(y.current),n()};return T.addEventListener("close",d),()=>T.removeEventListener("close",d)},[n]),S.useEffect(()=>{const T=l.current;T&&(e?(u(""),s(null),h(null),v(!1),clearInterval(y.current),T.open||T.showModal()):T.open&&T.close())},[e]);async function w(){if(!i.trim()){s("Enter a locator.");return}v(!0),s(null),h(null);try{const T=await Kd(t,i.trim());h("running"),y.current=setInterval(async()=>{var d;try{const c=await Yd(t,T.job_uid);c.status==="completed"?(clearInterval(y.current),y.current=null,v(!1),h("completed"),(d=l.current)==null||d.close(),r()):c.status==="failed"&&(clearInterval(y.current),y.current=null,v(!1),h("failed"),s(c.error_text||"Capture failed."))}catch(c){clearInterval(y.current),y.current=null,v(!1),s(c.message)}},500)}catch(T){s(T.message),v(!1)}}function C(){return f?m==="running"?"Running…":"Capturing…":"Capture"}return a.jsx("dialog",{ref:l,className:"capture-dialog",children:a.jsxs("div",{className:"capture-dialog-inner",children:[a.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),a.jsx("label",{htmlFor:"capture-locator",className:"capture-label",children:"Locator"}),a.jsx("input",{id:"capture-locator",className:"capture-input",type:"text",placeholder:"tweet:1234567890 or https://...",value:i,onChange:T=>u(T.target.value),onKeyDown:T=>{T.key==="Enter"&&w()},autoComplete:"off"}),o&&a.jsx("div",{className:"capture-error",children:o}),a.jsxs("div",{className:"capture-actions",children:[a.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var T;return(T=l.current)==null?void 0:T.close()},children:"Cancel"}),a.jsx("button",{type:"button",className:"capture-submit",onClick:w,disabled:f,children:C()})]})]})})}function cc(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&rString(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const Go={youtube:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function pp(e){return Go[e]??Go.other}function hp({entry:e,archiveId:t,isSelected:n,onSelect:r}){const[l,i]=S.useState(!1),o=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!l?a.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>i(!0),style:{objectFit:"contain"}}):a.jsx("span",{dangerouslySetInnerHTML:{__html:pp(e.source_kind)}});return a.jsxs("div",{className:n?"is-selected":void 0,tabIndex:0,"data-entry-uid":e.entry_uid,onClick:r,onKeyDown:s=>{s.key==="Enter"&&r()},children:[a.jsx("div",{className:"col-added",children:fc(e.archived_at)}),a.jsxs("div",{className:"col-title",children:[a.jsx("span",{className:"source-icon",children:o}),a.jsx("span",{className:"entry-title",children:Rt(e.title)||Rt(e.entry_uid)})]}),a.jsx("div",{className:"col-type",children:a.jsx("span",{className:"type-pill",children:Rt(e.entity_kind)})}),a.jsx("div",{className:"col-size",children:cc(e.total_artifact_bytes)}),a.jsx("div",{className:"url-cell col-url",children:Rt(e.original_url)})]})}function mp({entries:e,selectedEntryUid:t,onSelectEntry:n,archiveId:r}){return a.jsx("section",{id:"archive-view",className:"view is-active",children:a.jsxs("div",{className:"entry-table",children:[a.jsxs("div",{className:"entry-header-row",children:[a.jsx("div",{className:"col-added",children:"Added"}),a.jsx("div",{className:"col-title",children:"Title"}),a.jsx("div",{className:"col-type",children:"Type"}),a.jsx("div",{className:"col-size",children:"Size"}),a.jsx("div",{className:"col-url",children:"Original URL"})]}),a.jsx("div",{id:"entries-body",children:e.map(l=>a.jsx(hp,{entry:l,archiveId:r,isSelected:l.entry_uid===t,onSelect:()=>n(l)},l.entry_uid))})]})})}function vp({runs:e}){return a.jsx("section",{id:"runs-view",className:"view is-active",children:a.jsxs("table",{className:"entry-table",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Started"}),a.jsx("th",{children:"Status"}),a.jsx("th",{children:"Requested"}),a.jsx("th",{children:"Completed"}),a.jsx("th",{children:"Failed"})]})}),a.jsx("tbody",{children:e.map((t,n)=>a.jsxs("tr",{children:[a.jsx("td",{children:t.started_at??""}),a.jsx("td",{children:t.status??""}),a.jsx("td",{children:t.requested_count??""}),a.jsx("td",{children:t.completed_count??""}),a.jsx("td",{children:t.failed_count??""})]},n))})]})})}const gp=4;function yp({archives:e}){const{currentUser:t}=S.useContext($u)??{},n=t&&(t.role_bits&gp)!==0,[r,l]=S.useState("users"),[i,u]=S.useState([]),[o,s]=S.useState([]),[f,v]=S.useState(!1),[m,h]=S.useState(null),[y,w]=S.useState(""),[C,T]=S.useState(""),[d,c]=S.useState(""),[p,g]=S.useState(null),[N,_]=S.useState(!1),[j,z]=S.useState(""),[L,O]=S.useState(""),[X,ge]=S.useState(null),[Le,Re]=S.useState(!1),lt=S.useCallback(async()=>{if(n){v(!0),h(null);try{const[k,R]=await Promise.all([bd(),np()]);u(k),s(R)}catch(k){h(k.message)}finally{v(!1)}}},[n]);S.useEffect(()=>{lt()},[lt]);async function it(k){const R=k.status==="active"?"disabled":"active";try{await tp(k.user_uid,R),u(U=>U.map(B=>B.user_uid===k.user_uid?{...B,status:R}:B))}catch(U){h(U.message)}}async function jt(k){if(k.preventDefault(),!y.trim()||!C){g("Username and password required");return}_(!0),g(null);try{await ep(y.trim(),C,d.trim()||void 0),w(""),T(""),c(""),await lt()}catch(R){g(R.message)}finally{_(!1)}}async function E(k){if(k.preventDefault(),!j.trim()||!L.trim()){ge("Slug and name required");return}Re(!0),ge(null);try{await rp(j.trim(),L.trim()),z(""),O(""),await lt()}catch(R){ge(R.message)}finally{Re(!1)}}return n?a.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[a.jsx("h1",{children:"Admin"}),a.jsxs("div",{className:"admin-tabs",children:[a.jsx("button",{className:`admin-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),a.jsx("button",{className:`admin-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),a.jsx("button",{className:`admin-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),m&&a.jsx("div",{className:"capture-error",children:m}),r==="users"&&a.jsxs("div",{className:"admin-section",children:[a.jsx("h2",{children:"Users"}),f?a.jsx("p",{className:"muted",children:"Loading…"}):a.jsxs("table",{className:"admin-table",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Username"}),a.jsx("th",{children:"Email"}),a.jsx("th",{children:"Roles"}),a.jsx("th",{children:"Status"}),a.jsx("th",{children:"Actions"})]})}),a.jsx("tbody",{children:i.map(k=>a.jsxs("tr",{className:k.status==="disabled"?"admin-row-disabled":"",children:[a.jsx("td",{children:k.username}),a.jsx("td",{className:"muted",children:k.email||"—"}),a.jsx("td",{children:k.role_slugs.join(", ")||"—"}),a.jsx("td",{children:a.jsx("span",{className:`status-badge status-${k.status}`,children:k.status})}),a.jsx("td",{children:a.jsx("button",{className:"admin-action-btn",onClick:()=>it(k),children:k.status==="active"?"Ban":"Unban"})})]},k.user_uid))})]}),a.jsx("h3",{children:"Create User"}),a.jsxs("form",{className:"admin-form",onSubmit:jt,children:[a.jsx("input",{className:"admin-input",placeholder:"Username",value:y,onChange:k=>w(k.target.value),required:!0}),a.jsx("input",{className:"admin-input",type:"password",placeholder:"Password (min 8 chars)",value:C,onChange:k=>T(k.target.value),required:!0}),a.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:d,onChange:k=>c(k.target.value)}),p&&a.jsx("div",{className:"capture-error",children:p}),a.jsx("button",{className:"capture-submit",type:"submit",disabled:N,children:N?"Creating…":"Create User"})]})]}),r==="roles"&&a.jsxs("div",{className:"admin-section",children:[a.jsx("h2",{children:"Roles"}),f?a.jsx("p",{className:"muted",children:"Loading…"}):a.jsxs("table",{className:"admin-table",children:[a.jsx("thead",{children:a.jsxs("tr",{children:[a.jsx("th",{children:"Slug"}),a.jsx("th",{children:"Name"}),a.jsx("th",{children:"Level"}),a.jsx("th",{children:"Bit"}),a.jsx("th",{children:"Built-in"})]})}),a.jsx("tbody",{children:o.map(k=>a.jsxs("tr",{children:[a.jsx("td",{children:a.jsx("code",{children:k.slug})}),a.jsx("td",{children:k.name}),a.jsx("td",{children:k.level}),a.jsx("td",{children:k.bit_position}),a.jsx("td",{children:k.is_builtin?"✓":""})]},k.role_uid))})]}),a.jsx("h3",{children:"Create Custom Role"}),a.jsxs("form",{className:"admin-form",onSubmit:E,children:[a.jsx("input",{className:"admin-input",placeholder:"Slug (e.g. moderator)",value:j,onChange:k=>z(k.target.value),required:!0}),a.jsx("input",{className:"admin-input",placeholder:"Display Name (e.g. Moderator)",value:L,onChange:k=>O(k.target.value),required:!0}),X&&a.jsx("div",{className:"capture-error",children:X}),a.jsx("button",{className:"capture-submit",type:"submit",disabled:Le,children:Le?"Creating…":"Create Role"})]})]}),r==="archives"&&a.jsxs("div",{className:"admin-section",children:[a.jsx("h2",{children:"Mounted Archives"}),a.jsx("div",{className:"admin-list",children:e.map(k=>a.jsxs("div",{className:"admin-archive",children:[a.jsx("strong",{children:k.label}),a.jsx("div",{className:"muted",children:k.archive_path})]},k.id))})]})]}):a.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[a.jsx("h1",{children:"Admin"}),a.jsx("p",{className:"muted",children:"You need admin privileges to access this panel."}),a.jsx("h2",{children:"Mounted Archives"}),a.jsx("div",{className:"admin-list",children:e.map(k=>a.jsxs("div",{className:"admin-archive",children:[a.jsx("strong",{children:k.label}),a.jsx("div",{className:"muted",children:k.archive_path})]},k.id))})]})}function dc({node:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){var u;const l=t===e.tag.full_path;function i(){const o=l?null:e.tag.full_path;n(o),r("archive")}return a.jsxs("li",{children:[a.jsx("button",{className:`tag-node-btn${l?" is-active":""}`,title:e.tag.full_path,onClick:i,children:e.tag.name}),((u=e.children)==null?void 0:u.length)>0&&a.jsx("div",{className:"tag-children",children:a.jsx("ul",{className:"tag-tree-list",children:e.children.map(o=>a.jsx(dc,{node:o,tagFilter:t,onTagFilterSet:n,onViewChange:r},o.tag.tag_uid))})})]})}function wp({tagNodes:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){return a.jsx("section",{id:"tags-view",className:"view is-active",children:a.jsx("div",{className:"tag-tree",children:e.length===0?a.jsx("div",{children:"No tags yet."}):a.jsx("ul",{className:"tag-tree-list",children:e.map(l=>a.jsx(dc,{node:l,tagFilter:t,onTagFilterSet:n,onViewChange:r},l.tag.tag_uid))})})})}const Jo={0:"Private",1:"Public",2:"Users only",3:"Public"};function Sp({archiveId:e}){const[t,n]=S.useState([]),[r,l]=S.useState(!1),[i,u]=S.useState(null),[o,s]=S.useState(null),[f,v]=S.useState(null),[m,h]=S.useState(!1),[y,w]=S.useState(""),[C,T]=S.useState(""),[d,c]=S.useState(2),[p,g]=S.useState(!1),[N,_]=S.useState(null),j=S.useCallback(async()=>{if(e){l(!0),u(null);try{const L=await lp(e);n(L)}catch(L){u(L.message)}finally{l(!1)}}},[e]);S.useEffect(()=>{j()},[j]),S.useEffect(()=>{if(!o){v(null);return}h(!0),up(e,o.collection_uid).then(L=>v(L)).catch(L=>u(L.message)).finally(()=>h(!1))},[o,e]);async function z(L){L.preventDefault();const O=y.trim(),X=C.trim();if(!(!O||!X)){g(!0),_(null);try{await ip(e,O,X,d),w(""),T(""),c(2),await j()}catch(ge){_(ge.message)}finally{g(!1)}}}return e?a.jsxs("div",{className:"collections-view",children:[a.jsx("h2",{className:"view-heading",children:"Collections"}),r&&a.jsx("div",{className:"muted",children:"Loading…"}),i&&a.jsx("div",{className:"error-text",children:i}),a.jsxs("div",{className:"collections-layout",children:[a.jsxs("div",{className:"collections-list",children:[t.map(L=>a.jsxs("div",{className:`collection-row${(o==null?void 0:o.collection_uid)===L.collection_uid?" is-selected":""}`,onClick:()=>s(L),children:[a.jsx("span",{className:"collection-name",children:L.name}),a.jsx("span",{className:"muted",style:{fontSize:"0.8em"},children:Jo[L.default_visibility_bits]??L.default_visibility_bits})]},L.collection_uid)),t.length===0&&!r&&a.jsx("div",{className:"muted",children:"No collections yet."})]}),o&&a.jsxs("div",{className:"collection-detail",children:[a.jsx("h3",{className:"collection-detail-name",children:o.name}),a.jsxs("div",{className:"muted",style:{marginBottom:"0.75rem"},children:["Default visibility: ",Jo[o.default_visibility_bits]??o.default_visibility_bits]}),m?a.jsx("div",{className:"muted",children:"Loading entries…"}):f?f.entries.length===0?a.jsx("div",{className:"muted",children:"No entries visible to you in this collection."}):a.jsx("ul",{className:"collection-entries-list",children:f.entries.map(L=>a.jsxs("li",{className:"collection-entry-item",children:[a.jsx("span",{className:"entry-title",children:L.title||L.entry_uid}),a.jsx("span",{className:"muted",style:{fontSize:"0.8em"},children:L.source_kind})]},L.entry_uid))}):null]})]}),a.jsxs("details",{className:"create-collection-form",style:{marginTop:"1.5rem"},children:[a.jsx("summary",{style:{cursor:"pointer",fontWeight:600},children:"Create collection"}),a.jsxs("form",{onSubmit:z,style:{marginTop:"0.75rem",display:"flex",flexDirection:"column",gap:"0.5rem",maxWidth:400},children:[a.jsxs("label",{children:["Name",a.jsx("input",{className:"form-input",type:"text",value:y,onChange:L=>{w(L.target.value),C||T(L.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),a.jsxs("label",{children:["Slug",a.jsx("input",{className:"form-input",type:"text",value:C,onChange:L=>T(L.target.value),placeholder:"my-collection",required:!0})]}),a.jsxs("label",{children:["Default visibility",a.jsxs("select",{className:"form-input",value:d,onChange:L=>c(Number(L.target.value)),children:[a.jsx("option",{value:0,children:"Private (admin/owner only)"}),a.jsx("option",{value:2,children:"Users only (logged in)"}),a.jsx("option",{value:3,children:"Public"})]})]}),N&&a.jsx("div",{className:"muted",style:{color:"var(--accent)"},children:N}),a.jsx("button",{className:"btn",type:"submit",disabled:p,children:p?"Creating…":"Create"})]})]})]}):a.jsx("div",{className:"view-placeholder",children:"Select an archive."})}function xp({archiveId:e,selectedEntry:t,onTagFilterSet:n,tagNodes:r,onTagsRefresh:l}){const[i,u]=S.useState(null),[o,s]=S.useState([]),[f,v]=S.useState(""),[m,h]=S.useState([]),[y,w]=S.useState(""),C=S.useRef(0);S.useEffect(()=>{if(!t||!e){u(null),s([]),h([]);return}const c=++C.current;u(null),s([]),Promise.all([Hd(e,t.entry_uid),Gl(e,t.entry_uid),op(e,t.entry_uid)]).then(([p,g,N])=>{c===C.current&&(u(p),s(g),h(N))}).catch(()=>{})},[t,e]);async function T(){const c=f.trim();if(!(!c||!t))try{await Wd(e,t.entry_uid,c),v(""),w("");const p=await Gl(e,t.entry_uid);s(p),l()}catch(p){w(p.message)}}async function d(c){try{await Qd(e,t.entry_uid,c);const p=await Gl(e,t.entry_uid);s(p),l()}catch{}}return a.jsxs("aside",{className:"context-rail",children:[a.jsx("div",{className:"rail-title",children:"Context"}),t?i?a.jsxs("div",{className:"rail-body",children:[a.jsx("strong",{className:"rail-entry-title",children:Rt(i.summary.title)||Rt(i.summary.entry_uid)}),a.jsxs("div",{className:"rail-section",children:[i.summary.original_url&&a.jsxs("div",{className:"rail-item",children:[a.jsx("span",{className:"rail-label",children:"Original URL"}),":"," ",a.jsx("a",{href:i.summary.original_url,target:"_blank",rel:"noopener noreferrer",className:"rail-url-link",children:i.summary.original_url})]}),[["Added",fc(i.summary.archived_at)],["Source",i.summary.source_kind],["Type",i.summary.entity_kind],["Visibility",i.summary.visibility],["Structured root",i.structured_root_relpath]].map(([c,p])=>a.jsxs("div",{className:"rail-item",children:[a.jsx("span",{className:"rail-label",children:c}),": ",Rt(p)]},c))]}),i.artifacts.length>0?a.jsxs("div",{className:"rail-section",children:[a.jsxs("div",{className:"rail-section-heading",children:["Artifacts (",i.artifacts.length,")"]}),a.jsx("ul",{className:"artifact-list",children:i.artifacts.map((c,p)=>a.jsx("li",{children:a.jsxs("a",{href:`/api/archives/${e}/entries/${i.summary.entry_uid}/artifacts/${p}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[c.artifact_role.replace(/_/g," "),c.byte_size!=null?` (${cc(c.byte_size)})`:""]})},p))})]}):a.jsx("div",{className:"rail-item muted",children:"No artifacts."})]}):a.jsx("div",{className:"rail-body",children:"Loading…"}):a.jsx("div",{className:"rail-body",children:"Select an entry."}),t&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"entry-tags",children:o.length===0?a.jsx("span",{className:"muted",children:"No tags."}):o.map(c=>a.jsxs("span",{className:"tag-pill",title:c.full_path,children:[c.name,a.jsx("button",{className:"remove-tag",title:`Remove tag ${c.full_path}`,onClick:()=>d(c.tag_uid),children:"×"})]},c.tag_uid))}),a.jsxs("div",{className:"assign-tag-form",children:[a.jsx("input",{className:"assign-tag-input",type:"text",placeholder:"/science/cs",autoComplete:"off",value:f,onChange:c=>v(c.target.value),onKeyDown:c=>{c.key==="Enter"&&T()}}),a.jsx("button",{className:"assign-tag-btn",onClick:T,children:"Add tag"}),y&&a.jsx("div",{className:"muted",style:{fontSize:"0.85em",color:"var(--accent)"},children:y})]}),t&&m.length>0&&a.jsxs("div",{className:"rail-section",children:[a.jsx("div",{className:"rail-section-heading",children:"Collections"}),m.map(c=>a.jsxs("div",{className:"rail-item",children:[a.jsx("span",{className:"rail-label",children:c.collection_uid}),":"," ",a.jsx("span",{className:"muted",children:{0:"Private",1:"Public",2:"Users only",3:"Public"}[c.visibility_bits]??`bits:${c.visibility_bits}`})]},c.collection_uid))]})]})]})}const $u=S.createContext(null);function kp(){const[e,t]=S.useState("loading"),[n,r]=S.useState(null);S.useEffect(()=>{(async()=>{if(await Xd()){t("setup");return}const Oe=await qd();if(!Oe){t("login");return}r(Oe),t("authenticated")})()},[]),S.useEffect(()=>{const M=()=>{r(null),t("login")};return window.addEventListener("auth:expired",M),()=>window.removeEventListener("auth:expired",M)},[]);const[l,i]=S.useState([]),[u,o]=S.useState(null),[s,f]=S.useState([]),[v,m]=S.useState(null),[h,y]=S.useState(null),[w,C]=S.useState(null),[T,d]=S.useState("archive"),[c,p]=S.useState(""),[g,N]=S.useState(""),[_,j]=S.useState(!1),[z,L]=S.useState([]),[O,X]=S.useState([]),[ge,Le]=S.useState(!1),Re=S.useCallback(async(M,Oe,Be)=>{if(M){j(!0);try{let De;Oe||Be?De=await Bd(M,Oe,Be):De=await Vd(M),f(De),N(De.length===0?"No results":`${De.length} result${De.length===1?"":"s"}`)}catch{f([]),N("Search failed. Try again.")}finally{j(!1)}}},[]);S.useEffect(()=>{Ad().then(M=>{if(i(M),M.length>0){const Oe=M[0].id;o(Oe)}})},[]),S.useEffect(()=>{u&&(C(null),y(null),m(null),Promise.all([Re(u,"",null),Xo(u).then(L),Jl(u).then(X)]))},[u]),S.useEffect(()=>{if(u===null)return;const M=setTimeout(()=>{Re(u,c,w)},300);return()=>clearTimeout(M)},[c,u]),S.useEffect(()=>{u!==null&&(d("archive"),Re(u,c,w))},[w,u]);const lt=S.useCallback(M=>{o(M)},[]),it=S.useCallback(M=>{d(M),M==="tags"&&u&&Jl(u).then(X)},[u]),jt=S.useCallback(M=>{m(M?M.entry_uid:null),y(M)},[]),E=S.useCallback(M=>{C(M)},[]),k=S.useCallback(()=>{C(null)},[]),R=S.useCallback(()=>{u&&Jl(u).then(X)},[u]),U=S.useCallback(()=>{Le(!0)},[]),B=S.useCallback(()=>{Le(!1)},[]),Bt=S.useCallback(()=>{u&&Promise.all([Re(u,c,w),Xo(u).then(L)])},[u,c,w,Re]);return e==="loading"?a.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?a.jsx(cp,{onComplete:()=>t("login")}):e==="login"?a.jsx(ap,{onLogin:M=>{r(M),t("authenticated")}}):a.jsx($u.Provider,{value:{currentUser:n,setCurrentUser:r},children:a.jsxs(a.Fragment,{children:[a.jsx(fp,{archives:l,archiveId:u,onArchiveChange:lt,view:T,onViewChange:it,onCaptureClick:U}),a.jsxs("main",{className:"app-shell",children:[a.jsxs("div",{className:"workspace",children:[T==="archive"&&a.jsxs("div",{className:"search-row",children:[a.jsx("input",{className:"search-input",type:"search","aria-label":"Search archive","aria-busy":_,value:c,onChange:M=>p(M.target.value)}),a.jsxs("div",{className:"result-count",children:[g,w&&a.jsxs("button",{className:"tag-filter-badge",onClick:k,children:["× ",w]})]})]}),T==="archive"&&a.jsx(mp,{entries:s,selectedEntryUid:v,onSelectEntry:jt,archiveId:u,tagFilter:w,onClearTagFilter:k,searchQuery:c,onSearchChange:p,resultCount:g,searchBusy:_}),T==="runs"&&a.jsx(vp,{runs:z}),T==="admin"&&a.jsx(yp,{archives:l}),T==="tags"&&a.jsx(wp,{tagNodes:O,tagFilter:w,onTagFilterSet:E,onViewChange:it}),T==="collections"&&a.jsx(Sp,{archiveId:u})]}),a.jsx(xp,{archiveId:u,selectedEntry:h,onTagFilterSet:E,tagNodes:O,onTagsRefresh:R})]}),a.jsx(dp,{open:ge,archiveId:u,onClose:B,onCaptured:Bt})]})})}ac(document.getElementById("root")).render(a.jsx(S.StrictMode,{children:a.jsx(kp,{})})); diff --git a/crates/archivr-server/static/assets/index-C6h0jcH3.js b/crates/archivr-server/static/assets/index-C6h0jcH3.js deleted file mode 100644 index 5000950..0000000 --- a/crates/archivr-server/static/assets/index-C6h0jcH3.js +++ /dev/null @@ -1,40 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var nu={exports:{}},fl={},ru={exports:{}},$={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var ur=Symbol.for("react.element"),Cc=Symbol.for("react.portal"),Nc=Symbol.for("react.fragment"),jc=Symbol.for("react.strict_mode"),Ec=Symbol.for("react.profiler"),_c=Symbol.for("react.provider"),Pc=Symbol.for("react.context"),Tc=Symbol.for("react.forward_ref"),zc=Symbol.for("react.suspense"),Lc=Symbol.for("react.memo"),Rc=Symbol.for("react.lazy"),Hs=Symbol.iterator;function Oc(e){return e===null||typeof e!="object"?null:(e=Hs&&e[Hs]||e["@@iterator"],typeof e=="function"?e:null)}var lu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},iu=Object.assign,su={};function xn(e,t,n){this.props=e,this.context=t,this.refs=su,this.updater=n||lu}xn.prototype.isReactComponent={};xn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};xn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ou(){}ou.prototype=xn.prototype;function Gi(e,t,n){this.props=e,this.context=t,this.refs=su,this.updater=n||lu}var Ji=Gi.prototype=new ou;Ji.constructor=Gi;iu(Ji,xn.prototype);Ji.isPureReactComponent=!0;var Ws=Array.isArray,uu=Object.prototype.hasOwnProperty,Zi={current:null},au={key:!0,ref:!0,__self:!0,__source:!0};function cu(e,t,n){var r,l={},i=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(i=""+t.key),t)uu.call(t,r)&&!au.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,O=E[A];if(0>>1;Al(se,R))Rel(we,se)?(E[A]=we,E[Re]=R,A=Re):(E[A]=se,E[M]=R,A=M);else if(Rel(we,R))E[A]=we,E[Re]=R,A=Re;else break e}}return k}function l(E,k){var R=E.sortIndex-k.sortIndex;return R!==0?R:E.id-k.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var s=Date,u=s.now();e.unstable_now=function(){return s.now()-u}}var a=[],d=[],v=1,m=null,h=3,w=!1,x=!1,S=!1,j=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(E){for(var k=n(d);k!==null;){if(k.callback===null)r(d);else if(k.startTime<=E)r(d),k.sortIndex=k.expirationTime,t(a,k);else break;k=n(d)}}function g(E){if(S=!1,p(E),!x)if(n(a)!==null)x=!0,Ae(N);else{var k=n(d);k!==null&&Be(g,k.startTime-E)}}function N(E,k){x=!1,S&&(S=!1,f(L),L=-1),w=!0;var R=h;try{for(p(k),m=n(a);m!==null&&(!(m.expirationTime>k)||E&&!J());){var A=m.callback;if(typeof A=="function"){m.callback=null,h=m.priorityLevel;var O=A(m.expirationTime<=k);k=e.unstable_now(),typeof O=="function"?m.callback=O:m===n(a)&&r(a),p(k)}else r(a);m=n(a)}if(m!==null)var de=!0;else{var M=n(d);M!==null&&Be(g,M.startTime-k),de=!1}return de}finally{m=null,h=R,w=!1}}var T=!1,P=null,L=-1,U=5,D=-1;function J(){return!(e.unstable_now()-DE||125A?(E.sortIndex=R,t(d,E),n(a)===null&&E===n(d)&&(S?(f(L),L=-1):S=!0,Be(g,R-A))):(E.sortIndex=O,t(a,E),x||w||(x=!0,Ae(N))),E},e.unstable_shouldYield=J,e.unstable_wrapCallback=function(E){var k=h;return function(){var R=h;h=k;try{return E.apply(this,arguments)}finally{h=R}}}})(mu);hu.exports=mu;var Wc=hu.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Qc=y,Te=Wc;function C(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ri=Object.prototype.hasOwnProperty,Kc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Ks={},Ys={};function Yc(e){return ri.call(Ys,e)?!0:ri.call(Ks,e)?!1:Kc.test(e)?Ys[e]=!0:(Ks[e]=!0,!1)}function Xc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Gc(e,t,n,r){if(t===null||typeof t>"u"||Xc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function me(e,t,n,r,l,i,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=s}var ie={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ie[e]=new me(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ie[t]=new me(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ie[e]=new me(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ie[e]=new me(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ie[e]=new me(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ie[e]=new me(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ie[e]=new me(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ie[e]=new me(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ie[e]=new me(e,5,!1,e.toLowerCase(),null,!1,!1)});var bi=/[\-:]([a-z])/g;function es(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(bi,es);ie[t]=new me(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(bi,es);ie[t]=new me(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(bi,es);ie[t]=new me(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ie[e]=new me(e,1,!1,e.toLowerCase(),null,!1,!1)});ie.xlinkHref=new me("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ie[e]=new me(e,1,!1,e.toLowerCase(),null,!0,!0)});function ts(e,t,n,r){var l=ie.hasOwnProperty(t)?ie[t]:null;(l!==null?l.type!==0:r||!(2u||l[s]!==i[u]){var a=` -`+l[s].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=s&&0<=u);break}}}finally{Ll=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Ln(e):""}function Jc(e){switch(e.tag){case 5:return Ln(e.type);case 16:return Ln("Lazy");case 13:return Ln("Suspense");case 19:return Ln("SuspenseList");case 0:case 2:case 15:return e=Rl(e.type,!1),e;case 11:return e=Rl(e.type.render,!1),e;case 1:return e=Rl(e.type,!0),e;default:return""}}function oi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Gt:return"Fragment";case Xt:return"Portal";case li:return"Profiler";case ns:return"StrictMode";case ii:return"Suspense";case si:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case gu:return(e.displayName||"Context")+".Consumer";case yu:return(e._context.displayName||"Context")+".Provider";case rs:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ls:return t=e.displayName||null,t!==null?t:oi(e.type)||"Memo";case ft:t=e._payload,e=e._init;try{return oi(e(t))}catch{}}return null}function Zc(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return oi(t);case 8:return t===ns?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Et(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function xu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function qc(e){var t=xu(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(s){r=""+s,i.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function mr(e){e._valueTracker||(e._valueTracker=qc(e))}function Su(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=xu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Vr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ui(e,t){var n=t.checked;return X({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Gs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Et(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ku(e,t){t=t.checked,t!=null&&ts(e,"checked",t,!1)}function ai(e,t){ku(e,t);var n=Et(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ci(e,t.type,n):t.hasOwnProperty("defaultValue")&&ci(e,t.type,Et(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Js(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ci(e,t,n){(t!=="number"||Vr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Rn=Array.isArray;function on(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=vr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Kn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var $n={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},bc=["Webkit","ms","Moz","O"];Object.keys($n).forEach(function(e){bc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),$n[t]=$n[e]})});function Eu(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||$n.hasOwnProperty(e)&&$n[e]?(""+t).trim():t+"px"}function _u(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Eu(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var ed=X({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function pi(e,t){if(t){if(ed[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(C(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(C(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(C(61))}if(t.style!=null&&typeof t.style!="object")throw Error(C(62))}}function hi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var mi=null;function is(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var vi=null,un=null,an=null;function bs(e){if(e=dr(e)){if(typeof vi!="function")throw Error(C(280));var t=e.stateNode;t&&(t=yl(t),vi(e.stateNode,e.type,t))}}function Pu(e){un?an?an.push(e):an=[e]:un=e}function Tu(){if(un){var e=un,t=an;if(an=un=null,bs(e),t)for(e=0;e>>=0,e===0?32:31-(dd(e)/fd|0)|0}var yr=64,gr=4194304;function On(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Kr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,s=n&268435455;if(s!==0){var u=s&~l;u!==0?r=On(u):(i&=s,i!==0&&(r=On(i)))}else s=n&~l,s!==0?r=On(s):i!==0&&(r=On(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function ar(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ye(t),e[t]=n}function vd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=In),uo=" ",ao=!1;function Gu(e,t){switch(e){case"keyup":return Wd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ju(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Jt=!1;function Kd(e,t){switch(e){case"compositionend":return Ju(t);case"keypress":return t.which!==32?null:(ao=!0,uo);case"textInput":return e=t.data,e===uo&&ao?null:e;default:return null}}function Yd(e,t){if(Jt)return e==="compositionend"||!ps&&Gu(e,t)?(e=Yu(),Or=cs=vt=null,Jt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ho(n)}}function ea(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ea(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ta(){for(var e=window,t=Vr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Vr(e.document)}return t}function hs(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function nf(e){var t=ta(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ea(n.ownerDocument.documentElement,n)){if(r!==null&&hs(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=mo(n,i);var s=mo(n,r);l&&s&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Zt=null,ki=null,An=null,Ci=!1;function vo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ci||Zt==null||Zt!==Vr(r)||(r=Zt,"selectionStart"in r&&hs(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),An&&qn(An,r)||(An=r,r=Gr(ki,"onSelect"),0en||(e.current=Ti[en],Ti[en]=null,en--)}function V(e,t){en++,Ti[en]=e.current,e.current=t}var _t={},ce=Tt(_t),ke=Tt(!1),At=_t;function hn(e,t){var n=e.type.contextTypes;if(!n)return _t;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function Ce(e){return e=e.childContextTypes,e!=null}function Zr(){W(ke),W(ce)}function Co(e,t,n){if(ce.current!==_t)throw Error(C(168));V(ce,t),V(ke,n)}function ca(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(C(108,Zc(e)||"Unknown",l));return X({},n,r)}function qr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||_t,At=ce.current,V(ce,e),V(ke,ke.current),!0}function No(e,t,n){var r=e.stateNode;if(!r)throw Error(C(169));n?(e=ca(e,t,At),r.__reactInternalMemoizedMergedChildContext=e,W(ke),W(ce),V(ce,e)):W(ke),V(ke,n)}var nt=null,gl=!1,Kl=!1;function da(e){nt===null?nt=[e]:nt.push(e)}function mf(e){gl=!0,da(e)}function zt(){if(!Kl&&nt!==null){Kl=!0;var e=0,t=B;try{var n=nt;for(B=1;e>=s,l-=s,rt=1<<32-Ye(t)+l|n<L?(U=P,P=null):U=P.sibling;var D=h(f,P,p[L],g);if(D===null){P===null&&(P=U);break}e&&P&&D.alternate===null&&t(f,P),c=i(D,c,L),T===null?N=D:T.sibling=D,T=D,P=U}if(L===p.length)return n(f,P),Q&&Rt(f,L),N;if(P===null){for(;LL?(U=P,P=null):U=P.sibling;var J=h(f,P,D.value,g);if(J===null){P===null&&(P=U);break}e&&P&&J.alternate===null&&t(f,P),c=i(J,c,L),T===null?N=J:T.sibling=J,T=J,P=U}if(D.done)return n(f,P),Q&&Rt(f,L),N;if(P===null){for(;!D.done;L++,D=p.next())D=m(f,D.value,g),D!==null&&(c=i(D,c,L),T===null?N=D:T.sibling=D,T=D);return Q&&Rt(f,L),N}for(P=r(f,P);!D.done;L++,D=p.next())D=w(P,f,L,D.value,g),D!==null&&(e&&D.alternate!==null&&P.delete(D.key===null?L:D.key),c=i(D,c,L),T===null?N=D:T.sibling=D,T=D);return e&&P.forEach(function(je){return t(f,je)}),Q&&Rt(f,L),N}function j(f,c,p,g){if(typeof p=="object"&&p!==null&&p.type===Gt&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case hr:e:{for(var N=p.key,T=c;T!==null;){if(T.key===N){if(N=p.type,N===Gt){if(T.tag===7){n(f,T.sibling),c=l(T,p.props.children),c.return=f,f=c;break e}}else if(T.elementType===N||typeof N=="object"&&N!==null&&N.$$typeof===ft&&_o(N)===T.type){n(f,T.sibling),c=l(T,p.props),c.ref=Pn(f,T,p),c.return=f,f=c;break e}n(f,T);break}else t(f,T);T=T.sibling}p.type===Gt?(c=Ut(p.props.children,f.mode,g,p.key),c.return=f,f=c):(g=Br(p.type,p.key,p.props,null,f.mode,g),g.ref=Pn(f,c,p),g.return=f,f=g)}return s(f);case Xt:e:{for(T=p.key;c!==null;){if(c.key===T)if(c.tag===4&&c.stateNode.containerInfo===p.containerInfo&&c.stateNode.implementation===p.implementation){n(f,c.sibling),c=l(c,p.children||[]),c.return=f,f=c;break e}else{n(f,c);break}else t(f,c);c=c.sibling}c=ei(p,f.mode,g),c.return=f,f=c}return s(f);case ft:return T=p._init,j(f,c,T(p._payload),g)}if(Rn(p))return x(f,c,p,g);if(Cn(p))return S(f,c,p,g);jr(f,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,c!==null&&c.tag===6?(n(f,c.sibling),c=l(c,p),c.return=f,f=c):(n(f,c),c=bl(p,f.mode,g),c.return=f,f=c),s(f)):n(f,c)}return j}var vn=ma(!0),va=ma(!1),tl=Tt(null),nl=null,rn=null,gs=null;function ws(){gs=rn=nl=null}function xs(e){var t=tl.current;W(tl),e._currentValue=t}function Ri(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function dn(e,t){nl=e,gs=rn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Se=!0),e.firstContext=null)}function Fe(e){var t=e._currentValue;if(gs!==e)if(e={context:e,memoizedValue:t,next:null},rn===null){if(nl===null)throw Error(C(308));rn=e,nl.dependencies={lanes:0,firstContext:e}}else rn=rn.next=e;return t}var Mt=null;function Ss(e){Mt===null?Mt=[e]:Mt.push(e)}function ya(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Ss(t)):(n.next=l.next,l.next=n),t.interleaved=n,ut(e,r)}function ut(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var pt=!1;function ks(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function ga(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function it(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function kt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,F&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,ut(e,n)}return l=r.interleaved,l===null?(t.next=t,Ss(r)):(t.next=l.next,l.next=t),r.interleaved=t,ut(e,n)}function Mr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,os(e,n)}}function Po(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=s:i=i.next=s,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function rl(e,t,n,r){var l=e.updateQueue;pt=!1;var i=l.firstBaseUpdate,s=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var a=u,d=a.next;a.next=null,s===null?i=d:s.next=d,s=a;var v=e.alternate;v!==null&&(v=v.updateQueue,u=v.lastBaseUpdate,u!==s&&(u===null?v.firstBaseUpdate=d:u.next=d,v.lastBaseUpdate=a))}if(i!==null){var m=l.baseState;s=0,v=d=a=null,u=i;do{var h=u.lane,w=u.eventTime;if((r&h)===h){v!==null&&(v=v.next={eventTime:w,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var x=e,S=u;switch(h=t,w=n,S.tag){case 1:if(x=S.payload,typeof x=="function"){m=x.call(w,m,h);break e}m=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=S.payload,h=typeof x=="function"?x.call(w,m,h):x,h==null)break e;m=X({},m,h);break e;case 2:pt=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[u]:h.push(u))}else w={eventTime:w,lane:h,tag:u.tag,payload:u.payload,callback:u.callback,next:null},v===null?(d=v=w,a=m):v=v.next=w,s|=h;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;h=u,u=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(v===null&&(a=m),l.baseState=a,l.firstBaseUpdate=d,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do s|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Ht|=s,e.lanes=s,e.memoizedState=m}}function To(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Xl.transition;Xl.transition={};try{e(!1),t()}finally{B=n,Xl.transition=r}}function Ma(){return Ie().memoizedState}function wf(e,t,n){var r=Nt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},$a(e))Fa(t,n);else if(n=ya(e,t,n,r),n!==null){var l=pe();Xe(n,e,r,l),Ia(n,t,r)}}function xf(e,t,n){var r=Nt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if($a(e))Fa(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,u=i(s,n);if(l.hasEagerState=!0,l.eagerState=u,Ge(u,s)){var a=t.interleaved;a===null?(l.next=l,Ss(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}finally{}n=ya(e,t,l,r),n!==null&&(l=pe(),Xe(n,e,r,l),Ia(n,t,r))}}function $a(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function Fa(e,t){Bn=il=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ia(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,os(e,n)}}var sl={readContext:Fe,useCallback:oe,useContext:oe,useEffect:oe,useImperativeHandle:oe,useInsertionEffect:oe,useLayoutEffect:oe,useMemo:oe,useReducer:oe,useRef:oe,useState:oe,useDebugValue:oe,useDeferredValue:oe,useTransition:oe,useMutableSource:oe,useSyncExternalStore:oe,useId:oe,unstable_isNewReconciler:!1},Sf={readContext:Fe,useCallback:function(e,t){return Ze().memoizedState=[e,t===void 0?null:t],e},useContext:Fe,useEffect:Lo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Fr(4194308,4,za.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Fr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Fr(4,2,e,t)},useMemo:function(e,t){var n=Ze();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ze();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=wf.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ze();return e={current:e},t.memoizedState=e},useState:zo,useDebugValue:zs,useDeferredValue:function(e){return Ze().memoizedState=e},useTransition:function(){var e=zo(!1),t=e[0];return e=gf.bind(null,e[1]),Ze().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Y,l=Ze();if(Q){if(n===void 0)throw Error(C(407));n=n()}else{if(n=t(),ne===null)throw Error(C(349));Vt&30||ka(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Lo(Na.bind(null,r,i,e),[e]),r.flags|=2048,sr(9,Ca.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ze(),t=ne.identifierPrefix;if(Q){var n=lt,r=rt;n=(r&~(1<<32-Ye(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=lr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[qe]=t,e[tr]=r,Xa(e,t,!1,!1),t.stateNode=e;e:{switch(s=hi(n,r),n){case"dialog":H("cancel",e),H("close",e),l=r;break;case"iframe":case"object":case"embed":H("load",e),l=r;break;case"video":case"audio":for(l=0;lwn&&(t.flags|=128,r=!0,Tn(i,!1),t.lanes=4194304)}else{if(!r)if(e=ll(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Tn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!s.alternate&&!Q)return ue(t),null}else 2*Z()-i.renderingStartTime>wn&&n!==1073741824&&(t.flags|=128,r=!0,Tn(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(n=i.last,n!==null?n.sibling=s:t.child=s,i.last=s)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=Z(),t.sibling=null,n=K.current,V(K,r?n&1|2:n&1),t):(ue(t),null);case 22:case 23:return $s(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ee&1073741824&&(ue(t),t.subtreeFlags&6&&(t.flags|=8192)):ue(t),null;case 24:return null;case 25:return null}throw Error(C(156,t.tag))}function Tf(e,t){switch(vs(t),t.tag){case 1:return Ce(t.type)&&Zr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return yn(),W(ke),W(ce),js(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ns(t),null;case 13:if(W(K),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(C(340));mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return W(K),null;case 4:return yn(),null;case 10:return xs(t.type._context),null;case 22:case 23:return $s(),null;case 24:return null;default:return null}}var _r=!1,ae=!1,zf=typeof WeakSet=="function"?WeakSet:Set,z=null;function ln(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){G(e,t,r)}else n.current=null}function Bi(e,t,n){try{n()}catch(r){G(e,t,r)}}var Vo=!1;function Lf(e,t){if(Ni=Yr,e=ta(),hs(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var s=0,u=-1,a=-1,d=0,v=0,m=e,h=null;t:for(;;){for(var w;m!==n||l!==0&&m.nodeType!==3||(u=s+l),m!==i||r!==0&&m.nodeType!==3||(a=s+r),m.nodeType===3&&(s+=m.nodeValue.length),(w=m.firstChild)!==null;)h=m,m=w;for(;;){if(m===e)break t;if(h===n&&++d===l&&(u=s),h===i&&++v===r&&(a=s),(w=m.nextSibling)!==null)break;m=h,h=m.parentNode}m=w}n=u===-1||a===-1?null:{start:u,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(ji={focusedElem:e,selectionRange:n},Yr=!1,z=t;z!==null;)if(t=z,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,z=e;else for(;z!==null;){t=z;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var S=x.memoizedProps,j=x.memoizedState,f=t.stateNode,c=f.getSnapshotBeforeUpdate(t.elementType===t.type?S:We(t.type,S),j);f.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(C(163))}}catch(g){G(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,z=e;break}z=t.return}return x=Vo,Vo=!1,x}function Vn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Bi(t,n,i)}l=l.next}while(l!==r)}}function Sl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Vi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Za(e){var t=e.alternate;t!==null&&(e.alternate=null,Za(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[qe],delete t[tr],delete t[Pi],delete t[pf],delete t[hf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function qa(e){return e.tag===5||e.tag===3||e.tag===4}function Ho(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||qa(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Hi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Jr));else if(r!==4&&(e=e.child,e!==null))for(Hi(e,t,n),e=e.sibling;e!==null;)Hi(e,t,n),e=e.sibling}function Wi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Wi(e,t,n),e=e.sibling;e!==null;)Wi(e,t,n),e=e.sibling}var re=null,Qe=!1;function dt(e,t,n){for(n=n.child;n!==null;)ba(e,t,n),n=n.sibling}function ba(e,t,n){if(be&&typeof be.onCommitFiberUnmount=="function")try{be.onCommitFiberUnmount(pl,n)}catch{}switch(n.tag){case 5:ae||ln(n,t);case 6:var r=re,l=Qe;re=null,dt(e,t,n),re=r,Qe=l,re!==null&&(Qe?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Qe?(e=re,n=n.stateNode,e.nodeType===8?Ql(e.parentNode,n):e.nodeType===1&&Ql(e,n),Jn(e)):Ql(re,n.stateNode));break;case 4:r=re,l=Qe,re=n.stateNode.containerInfo,Qe=!0,dt(e,t,n),re=r,Qe=l;break;case 0:case 11:case 14:case 15:if(!ae&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,s=i.destroy;i=i.tag,s!==void 0&&(i&2||i&4)&&Bi(n,t,s),l=l.next}while(l!==r)}dt(e,t,n);break;case 1:if(!ae&&(ln(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){G(n,t,u)}dt(e,t,n);break;case 21:dt(e,t,n);break;case 22:n.mode&1?(ae=(r=ae)||n.memoizedState!==null,dt(e,t,n),ae=r):dt(e,t,n);break;default:dt(e,t,n)}}function Wo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new zf),t.forEach(function(r){var l=Af.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function He(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=s),r&=~i}if(r=l,r=Z()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Of(r/1960))-r,10e?16:e,yt===null)var r=!1;else{if(e=yt,yt=null,al=0,F&6)throw Error(C(331));var l=F;for(F|=4,z=e.current;z!==null;){var i=z,s=i.child;if(z.flags&16){var u=i.deletions;if(u!==null){for(var a=0;aZ()-Ds?It(e,0):Os|=n),Ne(e,t)}function oc(e,t){t===0&&(e.mode&1?(t=gr,gr<<=1,!(gr&130023424)&&(gr=4194304)):t=1);var n=pe();e=ut(e,t),e!==null&&(ar(e,t,n),Ne(e,n))}function Uf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),oc(e,n)}function Af(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(C(314))}r!==null&&r.delete(t),oc(e,n)}var uc;uc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ke.current)Se=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return Se=!1,_f(e,t,n);Se=!!(e.flags&131072)}else Se=!1,Q&&t.flags&1048576&&fa(t,el,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Ir(e,t),e=t.pendingProps;var l=hn(t,ce.current);dn(t,n),l=_s(null,t,r,e,l,n);var i=Ps();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ce(r)?(i=!0,qr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ks(t),l.updater=xl,t.stateNode=l,l._reactInternals=t,Di(t,r,e,n),t=Fi(null,t,r,!0,i,n)):(t.tag=0,Q&&i&&ms(t),fe(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Ir(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Vf(r),e=We(r,e),l){case 0:t=$i(null,t,r,e,n);break e;case 1:t=Uo(null,t,r,e,n);break e;case 11:t=Fo(null,t,r,e,n);break e;case 14:t=Io(null,t,r,We(r.type,e),n);break e}throw Error(C(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:We(r,l),$i(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:We(r,l),Uo(e,t,r,l,n);case 3:e:{if(Qa(t),e===null)throw Error(C(387));r=t.pendingProps,i=t.memoizedState,l=i.element,ga(e,t),rl(t,r,null,n);var s=t.memoizedState;if(r=s.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=gn(Error(C(423)),t),t=Ao(e,t,r,n,l);break e}else if(r!==l){l=gn(Error(C(424)),t),t=Ao(e,t,r,n,l);break e}else for(_e=St(t.stateNode.containerInfo.firstChild),Pe=t,Q=!0,Ke=null,n=va(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(mn(),r===l){t=at(e,t,n);break e}fe(e,t,r,n)}t=t.child}return t;case 5:return wa(t),e===null&&Li(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,s=l.children,Ei(r,l)?s=null:i!==null&&Ei(r,i)&&(t.flags|=32),Wa(e,t),fe(e,t,s,n),t.child;case 6:return e===null&&Li(t),null;case 13:return Ka(e,t,n);case 4:return Cs(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=vn(t,null,r,n):fe(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:We(r,l),Fo(e,t,r,l,n);case 7:return fe(e,t,t.pendingProps,n),t.child;case 8:return fe(e,t,t.pendingProps.children,n),t.child;case 12:return fe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,s=l.value,V(tl,r._currentValue),r._currentValue=s,i!==null)if(Ge(i.value,s)){if(i.children===l.children&&!ke.current){t=at(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){s=i.child;for(var a=u.firstContext;a!==null;){if(a.context===r){if(i.tag===1){a=it(-1,n&-n),a.tag=2;var d=i.updateQueue;if(d!==null){d=d.shared;var v=d.pending;v===null?a.next=a:(a.next=v.next,v.next=a),d.pending=a}}i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),Ri(i.return,n,t),u.lanes|=n;break}a=a.next}}else if(i.tag===10)s=i.type===t.type?null:i.child;else if(i.tag===18){if(s=i.return,s===null)throw Error(C(341));s.lanes|=n,u=s.alternate,u!==null&&(u.lanes|=n),Ri(s,n,t),s=i.sibling}else s=i.child;if(s!==null)s.return=i;else for(s=i;s!==null;){if(s===t){s=null;break}if(i=s.sibling,i!==null){i.return=s.return,s=i;break}s=s.return}i=s}fe(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,dn(t,n),l=Fe(l),r=r(l),t.flags|=1,fe(e,t,r,n),t.child;case 14:return r=t.type,l=We(r,t.pendingProps),l=We(r.type,l),Io(e,t,r,l,n);case 15:return Va(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:We(r,l),Ir(e,t),t.tag=1,Ce(r)?(e=!0,qr(t)):e=!1,dn(t,n),Ua(t,r,l),Di(t,r,l,n),Fi(null,t,r,!0,e,n);case 19:return Ya(e,t,n);case 22:return Ha(e,t,n)}throw Error(C(156,t.tag))};function ac(e,t){return $u(e,t)}function Bf(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Me(e,t,n,r){return new Bf(e,t,n,r)}function Is(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Vf(e){if(typeof e=="function")return Is(e)?1:0;if(e!=null){if(e=e.$$typeof,e===rs)return 11;if(e===ls)return 14}return 2}function jt(e,t){var n=e.alternate;return n===null?(n=Me(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Br(e,t,n,r,l,i){var s=2;if(r=e,typeof e=="function")Is(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case Gt:return Ut(n.children,l,i,t);case ns:s=8,l|=8;break;case li:return e=Me(12,n,t,l|2),e.elementType=li,e.lanes=i,e;case ii:return e=Me(13,n,t,l),e.elementType=ii,e.lanes=i,e;case si:return e=Me(19,n,t,l),e.elementType=si,e.lanes=i,e;case wu:return Cl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case yu:s=10;break e;case gu:s=9;break e;case rs:s=11;break e;case ls:s=14;break e;case ft:s=16,r=null;break e}throw Error(C(130,e==null?e:typeof e,""))}return t=Me(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Ut(e,t,n,r){return e=Me(7,e,r,t),e.lanes=n,e}function Cl(e,t,n,r){return e=Me(22,e,r,t),e.elementType=wu,e.lanes=n,e.stateNode={isHidden:!1},e}function bl(e,t,n){return e=Me(6,e,null,t),e.lanes=n,e}function ei(e,t,n){return t=Me(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Hf(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Dl(0),this.expirationTimes=Dl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Dl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Us(e,t,n,r,l,i,s,u,a){return e=new Hf(e,t,n,u,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Me(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ks(i),e}function Wf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(pc)}catch(e){console.error(e)}}pc(),pu.exports=ze;var Gf=pu.exports,hc,qo=Gf;hc=qo.createRoot,qo.hydrateRoot;async function ve(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function Jf(){return ve("/api/archives")}async function Zf(e){return ve(`/api/archives/${e}/entries`)}async function qf(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),ve(`/api/archives/${e}/entries/search?${r}`)}async function bf(e,t){return ve(`/api/archives/${e}/entries/${t}`)}async function ti(e,t){return ve(`/api/archives/${e}/entries/${t}/tags`)}async function ep(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tag_path:n})});if(!r.ok)throw new Error(`Failed to add tag (${r.status})`)}async function tp(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags/${n}`,{method:"DELETE"});if(!r.ok)throw new Error(`Remove failed (${r.status})`)}async function bo(e){return ve(`/api/archives/${e}/runs`)}async function ni(e){return ve(`/api/archives/${e}/tags`)}async function np(e,t){const n=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locator:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function rp(e,t){return ve(`/api/archives/${e}/capture_jobs/${t}`)}async function lp(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function ip(e,t){const n=await fetch("/api/auth/setup",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Setup failed");return n.json()}async function sp(e,t){const n=await fetch("/api/auth/login",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Login failed");return n.json()}async function op(){await fetch("/api/auth/logout",{method:"POST"})}async function up(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function ap(e){const t=await fetch("/api/auth/me",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({display_name:e})});if(!t.ok)throw new Error(await t.text())}async function cp(e,t){const n=await fetch("/api/auth/me",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({current_password:e,new_password:t})});if(!n.ok){let r=await n.text();try{r=JSON.parse(r).error??r}catch{}throw new Error(r)}}async function dp(){return ve("/api/auth/tokens")}async function fp(e){const t=await fetch("/api/auth/tokens",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e})});if(!t.ok)throw new Error(await t.text());return t.json()}async function pp(e){const t=await fetch(`/api/auth/tokens/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(await t.text())}async function hp(){return ve("/api/admin/instance-settings")}async function mp(e){const t=await fetch("/api/admin/instance-settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok)throw new Error(await t.text())}async function vp(){return ve("/api/admin/users")}async function yp(e,t,n){const r=await fetch("/api/admin/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:t,email:n||void 0})});if(!r.ok){const l=await r.json().catch(()=>({}));throw new Error(l.error||`HTTP ${r.status}`)}return r.json()}async function gp(e,t){const n=await fetch(`/api/admin/users/${e}/status`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function wp(){return ve("/api/admin/roles")}async function xp(e,t){const n=await fetch("/api/admin/roles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:e,name:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function Sp(e){return ve(`/api/archives/${e}/collections`)}async function kp(e,t,n,r=2){const l=await fetch(`/api/archives/${e}/collections`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:t,slug:n,default_visibility_bits:r})});if(!l.ok){const i=await l.json().catch(()=>({error:l.statusText}));throw new Error(i.error||l.statusText)}return l.json()}async function Cp(e,t){return ve(`/api/archives/${e}/collections/${t}`)}async function Np(e,t,n,r=2){const l=await fetch(`/api/archives/${e}/collections/${t}/entries`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({entry_uid:n,visibility_bits:r})});if(!l.ok){const i=await l.json().catch(()=>({error:l.statusText}));throw new Error(i.error||l.statusText)}}async function jp(e,t,n){const r=await fetch(`/api/archives/${e}/collections/${t}/entries/${n}`,{method:"DELETE"});if(!r.ok){const l=await r.json().catch(()=>({error:r.statusText}));throw new Error(l.error||r.statusText)}}async function Ep(e,t,n,r){const l=await fetch(`/api/archives/${e}/collections/${t}/entries/${n}`,{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({visibility_bits:r})});if(!l.ok){const i=await l.json().catch(()=>({error:l.statusText}));throw new Error(i.error||l.statusText)}}async function _p(e,t){return ve(`/api/archives/${e}/entries/${t}/collections`)}async function eu(e,t,n){const r=await fetch(`/api/archives/${e}/collections/${t}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!r.ok){const l=await r.json().catch(()=>({}));throw new Error(l.error??r.statusText)}}async function Pp(e,t){const n=await fetch(`/api/archives/${e}/collections/${t}`,{method:"DELETE"});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error??n.statusText)}}const Tp=window.fetch;window.fetch=async(...e)=>{var n;const t=await Tp(...e);return t.status===401&&((typeof e[0]=="string"?e[0]:((n=e[0])==null?void 0:n.url)??"").includes("/api/auth/")||window.dispatchEvent(new CustomEvent("auth:expired"))),t};function zp({onLogin:e}){const[t,n]=y.useState(""),[r,l]=y.useState(""),[i,s]=y.useState(null),[u,a]=y.useState(!1);async function d(v){v.preventDefault(),s(null),a(!0);try{const m=await sp(t,r);e(m)}catch(m){s(m.message)}finally{a(!1)}}return o.jsxs("div",{className:"login-page",children:[o.jsx("h1",{children:"Archivr"}),o.jsxs("form",{onSubmit:d,children:[o.jsxs("label",{children:["Username",o.jsx("input",{type:"text",value:t,onChange:v=>n(v.target.value),autoFocus:!0,required:!0})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:r,onChange:v=>l(v.target.value),required:!0})]}),i&&o.jsx("p",{className:"error",children:i}),o.jsx("button",{type:"submit",disabled:u,children:u?"Logging in…":"Log in"})]})]})}function Lp({onComplete:e}){const[t,n]=y.useState(""),[r,l]=y.useState(""),[i,s]=y.useState(""),[u,a]=y.useState(null),[d,v]=y.useState(!1);async function m(h){if(h.preventDefault(),r!==i){a("Passwords do not match");return}if(r.length<8){a("Password must be at least 8 characters");return}a(null),v(!0);try{await ip(t,r),e()}catch(w){a(w.message)}finally{v(!1)}}return o.jsxs("div",{className:"setup-page",children:[o.jsx("h1",{children:"Welcome to Archivr"}),o.jsx("p",{children:"Create your owner account to get started."}),o.jsxs("form",{onSubmit:m,children:[o.jsxs("label",{children:["Username",o.jsx("input",{type:"text",value:t,onChange:h=>n(h.target.value),autoFocus:!0,required:!0})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:r,onChange:h=>l(h.target.value),required:!0})]}),o.jsxs("label",{children:["Confirm password",o.jsx("input",{type:"password",value:i,onChange:h=>s(h.target.value),required:!0})]}),u&&o.jsx("p",{className:"error",children:u}),o.jsx("button",{type:"submit",disabled:d,children:d?"Creating account…":"Create account"})]})]})}function Rp({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:s,setCurrentUser:u}=y.useContext(Pl)??{},[a,d]=y.useState(!1);async function v(){d(!0),await op(),u(null),window.location.reload()}return o.jsxs("header",{className:"topbar",children:[o.jsx("div",{className:"brand",children:"Archivr"}),o.jsx("select",{className:"archive-switcher","aria-label":"Select archive",value:t??"",onChange:m=>n(m.target.value),children:e.map(m=>o.jsx("option",{value:m.id,children:m.label},m.id))}),o.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","runs","admin","tags","collections","settings"].map(m=>o.jsx("button",{className:`nav-link${r===m?" is-active":""}`,onClick:()=>l(m),children:m.charAt(0).toUpperCase()+m.slice(1)},m))}),o.jsx("button",{className:"capture-button",onClick:i,children:"+ Capture"}),s&&o.jsxs("div",{className:"user-menu",children:[o.jsx("span",{className:"username",children:s.display_name||s.username}),o.jsx("button",{className:"nav-link",onClick:()=>l("settings"),style:{color:"#d7cdbf",fontSize:13},children:"Settings"}),o.jsx("button",{onClick:v,disabled:a,className:"logout-btn",children:a?"Logging out…":"Log out"})]})]})}function Op({open:e,archiveId:t,onClose:n,onCaptured:r}){const l=y.useRef(null),[i,s]=y.useState(""),[u,a]=y.useState(null),[d,v]=y.useState(!1),[m,h]=y.useState(null),w=y.useRef(null);y.useEffect(()=>{const j=l.current;if(!j)return;const f=()=>{clearInterval(w.current),n()};return j.addEventListener("close",f),()=>j.removeEventListener("close",f)},[n]),y.useEffect(()=>{const j=l.current;j&&(e?(s(""),a(null),h(null),v(!1),clearInterval(w.current),j.open||j.showModal()):j.open&&j.close())},[e]);async function x(){if(!i.trim()){a("Enter a locator.");return}v(!0),a(null),h(null);try{const j=await np(t,i.trim());h("running"),w.current=setInterval(async()=>{var f;try{const c=await rp(t,j.job_uid);c.status==="completed"?(clearInterval(w.current),w.current=null,v(!1),h("completed"),(f=l.current)==null||f.close(),r()):c.status==="failed"&&(clearInterval(w.current),w.current=null,v(!1),h("failed"),a(c.error_text||"Capture failed."))}catch(c){clearInterval(w.current),w.current=null,v(!1),a(c.message)}},500)}catch(j){a(j.message),v(!1)}}function S(){return d?m==="running"?"Running…":"Capturing…":"Capture"}return o.jsx("dialog",{ref:l,className:"capture-dialog",children:o.jsxs("div",{className:"capture-dialog-inner",children:[o.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),o.jsx("label",{htmlFor:"capture-locator",className:"capture-label",children:"Locator"}),o.jsx("input",{id:"capture-locator",className:"capture-input",type:"text",placeholder:"tweet:1234567890 or https://...",value:i,onChange:j=>s(j.target.value),onKeyDown:j=>{j.key==="Enter"&&x()},autoComplete:"off"}),u&&o.jsx("div",{className:"capture-error",children:u}),o.jsxs("div",{className:"capture-actions",children:[o.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var j;return(j=l.current)==null?void 0:j.close()},children:"Cancel"}),o.jsx("button",{type:"button",className:"capture-submit",onClick:x,disabled:d,children:S()})]})]})})}function mc(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&rString(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const tu={youtube:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function Dp(e){return tu[e]??tu.other}function Mp({entry:e,archiveId:t,isSelected:n,onSelect:r}){const[l,i]=y.useState(!1),u=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!l?o.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>i(!0),style:{objectFit:"contain"}}):o.jsx("span",{dangerouslySetInnerHTML:{__html:Dp(e.source_kind)}});return o.jsxs("div",{className:n?"is-selected":void 0,tabIndex:0,"data-entry-uid":e.entry_uid,onClick:r,onKeyDown:a=>{a.key==="Enter"&&r()},children:[o.jsx("div",{className:"col-added",children:vc(e.archived_at)}),o.jsxs("div",{className:"col-title",children:[o.jsx("span",{className:"source-icon",children:u}),o.jsx("span",{className:"entry-title",children:Ft(e.title)||Ft(e.entry_uid)})]}),o.jsx("div",{className:"col-type",children:o.jsx("span",{className:"type-pill",children:Ft(e.entity_kind)})}),o.jsx("div",{className:"col-size",children:mc(e.total_artifact_bytes)}),o.jsx("div",{className:"url-cell col-url",children:Ft(e.original_url)})]})}function $p({entries:e,selectedEntryUid:t,onSelectEntry:n,archiveId:r}){return o.jsx("section",{id:"archive-view",className:"view is-active",children:o.jsxs("div",{className:"entry-table",children:[o.jsxs("div",{className:"entry-header-row",children:[o.jsx("div",{className:"col-added",children:"Added"}),o.jsx("div",{className:"col-title",children:"Title"}),o.jsx("div",{className:"col-type",children:"Type"}),o.jsx("div",{className:"col-size",children:"Size"}),o.jsx("div",{className:"col-url",children:"Original URL"})]}),o.jsx("div",{id:"entries-body",children:e.map(l=>o.jsx(Mp,{entry:l,archiveId:r,isSelected:l.entry_uid===t,onSelect:()=>n(l)},l.entry_uid))})]})})}function Fp({runs:e}){return o.jsx("section",{id:"runs-view",className:"view is-active",children:o.jsxs("table",{className:"entry-table",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"Started"}),o.jsx("th",{children:"Status"}),o.jsx("th",{children:"Requested"}),o.jsx("th",{children:"Completed"}),o.jsx("th",{children:"Failed"})]})}),o.jsx("tbody",{children:e.map((t,n)=>o.jsxs("tr",{children:[o.jsx("td",{children:t.started_at??""}),o.jsx("td",{children:t.status??""}),o.jsx("td",{children:t.requested_count??""}),o.jsx("td",{children:t.completed_count??""}),o.jsx("td",{children:t.failed_count??""})]},n))})]})})}const Ip=4;function Up({archives:e}){const{currentUser:t}=y.useContext(Pl)??{},n=t&&(t.role_bits&Ip)!==0,[r,l]=y.useState("users"),[i,s]=y.useState([]),[u,a]=y.useState([]),[d,v]=y.useState(!1),[m,h]=y.useState(null),[w,x]=y.useState(""),[S,j]=y.useState(""),[f,c]=y.useState(""),[p,g]=y.useState(null),[N,T]=y.useState(!1),[P,L]=y.useState(""),[U,D]=y.useState(""),[J,je]=y.useState(null),[ye,ge]=y.useState(!1),Ue=y.useCallback(async()=>{if(n){v(!0),h(null);try{const[k,R]=await Promise.all([vp(),wp()]);s(k),a(R)}catch(k){h(k.message)}finally{v(!1)}}},[n]);y.useEffect(()=>{Ue()},[Ue]);async function Ae(k){const R=k.status==="active"?"disabled":"active";try{await gp(k.user_uid,R),s(A=>A.map(O=>O.user_uid===k.user_uid?{...O,status:R}:O))}catch(A){h(A.message)}}async function Be(k){if(k.preventDefault(),!w.trim()||!S){g("Username and password required");return}T(!0),g(null);try{await yp(w.trim(),S,f.trim()||void 0),x(""),j(""),c(""),await Ue()}catch(R){g(R.message)}finally{T(!1)}}async function E(k){if(k.preventDefault(),!P.trim()||!U.trim()){je("Slug and name required");return}ge(!0),je(null);try{await xp(P.trim(),U.trim()),L(""),D(""),await Ue()}catch(R){je(R.message)}finally{ge(!1)}}return n?o.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[o.jsx("h1",{children:"Admin"}),o.jsxs("div",{className:"admin-tabs",children:[o.jsx("button",{className:`admin-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),o.jsx("button",{className:`admin-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),o.jsx("button",{className:`admin-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),m&&o.jsx("div",{className:"capture-error",children:m}),r==="users"&&o.jsxs("div",{className:"admin-section",children:[o.jsx("h2",{children:"Users"}),d?o.jsx("p",{className:"muted",children:"Loading…"}):o.jsxs("table",{className:"admin-table",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"Username"}),o.jsx("th",{children:"Email"}),o.jsx("th",{children:"Roles"}),o.jsx("th",{children:"Status"}),o.jsx("th",{children:"Actions"})]})}),o.jsx("tbody",{children:i.map(k=>o.jsxs("tr",{className:k.status==="disabled"?"admin-row-disabled":"",children:[o.jsx("td",{children:k.username}),o.jsx("td",{className:"muted",children:k.email||"—"}),o.jsx("td",{children:k.role_slugs.join(", ")||"—"}),o.jsx("td",{children:o.jsx("span",{className:`status-badge status-${k.status}`,children:k.status})}),o.jsx("td",{children:o.jsx("button",{className:"admin-action-btn",onClick:()=>Ae(k),children:k.status==="active"?"Ban":"Unban"})})]},k.user_uid))})]}),o.jsx("h3",{children:"Create User"}),o.jsxs("form",{className:"admin-form",onSubmit:Be,children:[o.jsx("input",{className:"admin-input",placeholder:"Username",value:w,onChange:k=>x(k.target.value),required:!0}),o.jsx("input",{className:"admin-input",type:"password",placeholder:"Password (min 8 chars)",value:S,onChange:k=>j(k.target.value),required:!0}),o.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:f,onChange:k=>c(k.target.value)}),p&&o.jsx("div",{className:"capture-error",children:p}),o.jsx("button",{className:"capture-submit",type:"submit",disabled:N,children:N?"Creating…":"Create User"})]})]}),r==="roles"&&o.jsxs("div",{className:"admin-section",children:[o.jsx("h2",{children:"Roles"}),d?o.jsx("p",{className:"muted",children:"Loading…"}):o.jsxs("table",{className:"admin-table",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"Slug"}),o.jsx("th",{children:"Name"}),o.jsx("th",{children:"Level"}),o.jsx("th",{children:"Bit"}),o.jsx("th",{children:"Built-in"})]})}),o.jsx("tbody",{children:u.map(k=>o.jsxs("tr",{children:[o.jsx("td",{children:o.jsx("code",{children:k.slug})}),o.jsx("td",{children:k.name}),o.jsx("td",{children:k.level}),o.jsx("td",{children:k.bit_position}),o.jsx("td",{children:k.is_builtin?"✓":""})]},k.role_uid))})]}),o.jsx("h3",{children:"Create Custom Role"}),o.jsxs("form",{className:"admin-form",onSubmit:E,children:[o.jsx("input",{className:"admin-input",placeholder:"Slug (e.g. moderator)",value:P,onChange:k=>L(k.target.value),required:!0}),o.jsx("input",{className:"admin-input",placeholder:"Display Name (e.g. Moderator)",value:U,onChange:k=>D(k.target.value),required:!0}),J&&o.jsx("div",{className:"capture-error",children:J}),o.jsx("button",{className:"capture-submit",type:"submit",disabled:ye,children:ye?"Creating…":"Create Role"})]})]}),r==="archives"&&o.jsxs("div",{className:"admin-section",children:[o.jsx("h2",{children:"Mounted Archives"}),o.jsx("div",{className:"admin-list",children:e.map(k=>o.jsxs("div",{className:"admin-archive",children:[o.jsx("strong",{children:k.label}),o.jsx("div",{className:"muted",children:k.archive_path})]},k.id))})]})]}):o.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[o.jsx("h1",{children:"Admin"}),o.jsx("p",{className:"muted",children:"You need admin privileges to access this panel."}),o.jsx("h2",{children:"Mounted Archives"}),o.jsx("div",{className:"admin-list",children:e.map(k=>o.jsxs("div",{className:"admin-archive",children:[o.jsx("strong",{children:k.label}),o.jsx("div",{className:"muted",children:k.archive_path})]},k.id))})]})}function yc({node:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){var s;const l=t===e.tag.full_path;function i(){const u=l?null:e.tag.full_path;n(u),r("archive")}return o.jsxs("li",{children:[o.jsx("button",{className:`tag-node-btn${l?" is-active":""}`,title:e.tag.full_path,onClick:i,children:e.tag.name}),((s=e.children)==null?void 0:s.length)>0&&o.jsx("div",{className:"tag-children",children:o.jsx("ul",{className:"tag-tree-list",children:e.children.map(u=>o.jsx(yc,{node:u,tagFilter:t,onTagFilterSet:n,onViewChange:r},u.tag.tag_uid))})})]})}function Ap({tagNodes:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){return o.jsx("section",{id:"tags-view",className:"view is-active",children:o.jsx("div",{className:"tag-tree",children:e.length===0?o.jsx("div",{children:"No tags yet."}):o.jsx("ul",{className:"tag-tree-list",children:e.map(l=>o.jsx(yc,{node:l,tagFilter:t,onTagFilterSet:n,onViewChange:r},l.tag.tag_uid))})})})}const Mn=[{value:0,label:"Private"},{value:2,label:"Users only"},{value:3,label:"Public"}],Bp=e=>{var t;return((t=Mn.find(n=>n.value===e))==null?void 0:t.label)??String(e)};function Vp({archiveId:e}){const[t,n]=y.useState([]),[r,l]=y.useState(!1),[i,s]=y.useState(null),[u,a]=y.useState(null),[d,v]=y.useState(null),[m,h]=y.useState(!1),[w,x]=y.useState(null),[S,j]=y.useState(""),[f,c]=y.useState(""),[p,g]=y.useState(2),[N,T]=y.useState(!1),[P,L]=y.useState(null),[U,D]=y.useState(""),[J,je]=y.useState(2),[ye,ge]=y.useState(!1),[Ue,Ae]=y.useState(null),[Be,E]=y.useState(!1),[k,R]=y.useState(""),A=y.useRef(null),O=t.find(_=>_.collection_uid===u)??null,de=(O==null?void 0:O.slug)==="_default_",M=y.useCallback(async()=>{if(e){l(!0),s(null);try{const _=await Sp(e);n(_)}catch(_){s(_.message)}finally{l(!1)}}},[e]),se=y.useCallback(async _=>{if(!_){v(null);return}h(!0),x(null);try{const I=await Cp(e,_);v(I)}catch(I){x(I.message)}finally{h(!1)}},[e]);y.useEffect(()=>{M()},[M]),y.useEffect(()=>{se(u)},[u,se]),y.useEffect(()=>{Be&&A.current&&A.current.focus()},[Be]);async function Re(_){_.preventDefault();const I=S.trim(),Ve=f.trim();if(!(!I||!Ve)){T(!0),L(null);try{const Lt=await kp(e,I,Ve,p);j(""),c(""),g(2),await M(),a(Lt.collection_uid)}catch(Lt){L(Lt.message)}finally{T(!1)}}}async function we(){const _=k.trim();if(!_||!O){E(!1);return}try{await eu(e,O.collection_uid,{name:_}),await M(),v(I=>I&&{...I,name:_})}catch(I){s(I.message)}finally{E(!1)}}async function gc(_){if(O)try{await eu(e,O.collection_uid,{default_visibility_bits:_}),await M(),v(I=>I&&{...I,default_visibility_bits:_})}catch(I){s(I.message)}}async function wc(){if(O&&window.confirm(`Delete collection "${O.name}"? Entries will not be deleted.`))try{await Pp(e,O.collection_uid),a(null),v(null),await M()}catch(_){s(_.message)}}async function xc(_){_.preventDefault();const I=U.trim();if(!(!I||!O)){ge(!0),Ae(null);try{await Np(e,O.collection_uid,I,J),D(""),await se(O.collection_uid)}catch(Ve){Ae(Ve.message)}finally{ge(!1)}}}async function Sc(_){if(O)try{await jp(e,O.collection_uid,_),await se(O.collection_uid)}catch(I){x(I.message)}}async function kc(_,I){if(O)try{await Ep(e,O.collection_uid,_,I),v(Ve=>Ve&&{...Ve,entries:Ve.entries.map(Lt=>Lt.entry_uid===_?{...Lt,collection_visibility_bits:I}:Lt)})}catch(Ve){x(Ve.message)}}return e?o.jsxs("div",{className:"collections-view",children:[o.jsx("h2",{className:"collections-heading",children:"Collections"}),r&&o.jsx("div",{className:"muted",children:"Loading…"}),i&&o.jsxs("div",{className:"collections-error",children:[i," ",o.jsx("button",{onClick:()=>s(null),className:"coll-dismiss",children:"×"})]}),o.jsxs("div",{className:"collections-layout",children:[o.jsxs("div",{className:"collections-sidebar",children:[t.map(_=>o.jsxs("button",{className:`coll-row${u===_.collection_uid?" is-active":""}`,onClick:()=>a(_.collection_uid),children:[o.jsx("span",{className:"coll-row-name",children:_.name}),o.jsx("span",{className:"coll-row-meta",children:Bp(_.default_visibility_bits)})]},_.collection_uid)),t.length===0&&!r&&o.jsx("div",{className:"muted",style:{padding:"8px 12px"},children:"No collections yet."})]}),O?o.jsxs("div",{className:"coll-detail",children:[o.jsxs("div",{className:"coll-detail-header",children:[Be?o.jsx("input",{ref:A,className:"coll-rename-input",value:k,onChange:_=>R(_.target.value),onBlur:we,onKeyDown:_=>{_.key==="Enter"&&we(),_.key==="Escape"&&E(!1)}}):o.jsxs("h3",{className:`coll-detail-name${de?"":" coll-detail-name--editable"}`,title:de?void 0:"Click to rename",onClick:()=>{de||(R(O.name),E(!0))},children:[(d==null?void 0:d.name)??O.name,!de&&o.jsx("span",{className:"coll-edit-hint",children:" ✎"})]}),!de&&o.jsx("button",{className:"coll-delete-btn",onClick:wc,title:"Delete collection",children:"Delete"})]}),o.jsxs("div",{className:"coll-detail-vis",children:[o.jsx("span",{className:"coll-vis-label",children:"Default visibility"}),o.jsx("select",{className:"coll-vis-select",value:(d==null?void 0:d.default_visibility_bits)??O.default_visibility_bits,onChange:_=>gc(Number(_.target.value)),disabled:de,children:Mn.map(_=>o.jsx("option",{value:_.value,children:_.label},_.value))})]}),o.jsxs("div",{className:"coll-entries-section",children:[o.jsx("div",{className:"coll-section-heading",children:"Entries"}),m&&o.jsx("div",{className:"muted",children:"Loading…"}),w&&o.jsx("div",{className:"collections-error",children:w}),!m&&d&&(d.entries.length===0?o.jsx("div",{className:"muted",children:"No entries in this collection."}):o.jsx("ul",{className:"coll-entries-list",children:d.entries.map(_=>o.jsxs("li",{className:"coll-entry-row",children:[o.jsxs("div",{className:"coll-entry-info",children:[o.jsx("span",{className:"coll-entry-title",children:_.title||_.entry_uid}),o.jsx("span",{className:"coll-entry-kind muted",children:_.source_kind})]}),o.jsxs("div",{className:"coll-entry-actions",children:[o.jsx("select",{className:"coll-entry-vis-select",value:_.collection_visibility_bits,onChange:I=>kc(_.entry_uid,Number(I.target.value)),children:Mn.map(I=>o.jsx("option",{value:I.value,children:I.label},I.value))}),!de&&o.jsx("button",{className:"coll-entry-remove",onClick:()=>Sc(_.entry_uid),title:"Remove from collection",children:"×"})]})]},_.entry_uid))}))]}),!de&&o.jsxs("form",{className:"coll-add-entry-form",onSubmit:xc,children:[o.jsx("div",{className:"coll-section-heading",children:"Add entry"}),o.jsxs("div",{className:"coll-add-entry-row",children:[o.jsx("input",{className:"coll-add-entry-input",type:"text",value:U,onChange:_=>D(_.target.value),placeholder:"entry_uid",required:!0}),o.jsx("select",{className:"coll-vis-select",value:J,onChange:_=>je(Number(_.target.value)),children:Mn.map(_=>o.jsx("option",{value:_.value,children:_.label},_.value))}),o.jsx("button",{className:"coll-add-btn",type:"submit",disabled:ye,children:ye?"…":"Add"})]}),Ue&&o.jsx("div",{className:"collections-error",style:{marginTop:4},children:Ue})]})]}):o.jsx("div",{className:"coll-detail coll-detail--empty",children:o.jsx("div",{className:"muted",children:"Select a collection to view details."})})]}),o.jsxs("details",{className:"coll-create-details",style:{marginTop:"1.5rem"},children:[o.jsx("summary",{style:{cursor:"pointer",fontWeight:600},children:"Create collection"}),o.jsxs("form",{onSubmit:Re,style:{marginTop:"0.75rem",display:"flex",flexDirection:"column",gap:"0.5rem",maxWidth:400},children:[o.jsxs("label",{children:["Name",o.jsx("input",{className:"capture-input",type:"text",value:S,onChange:_=>{j(_.target.value),f||c(_.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),o.jsxs("label",{children:["Slug",o.jsx("input",{className:"capture-input",type:"text",value:f,onChange:_=>c(_.target.value),placeholder:"my-collection",required:!0})]}),o.jsxs("label",{children:["Default visibility",o.jsx("select",{className:"capture-input",style:{height:42},value:p,onChange:_=>g(Number(_.target.value)),children:Mn.map(_=>o.jsx("option",{value:_.value,children:_.label},_.value))})]}),P&&o.jsx("div",{className:"collections-error",children:P}),o.jsx("button",{className:"capture-submit",type:"submit",disabled:N,style:{alignSelf:"flex-start",padding:"8px 20px"},children:N?"Creating…":"Create"})]})]})]}):o.jsx("div",{className:"view-placeholder",children:"Select an archive."})}const Hp=4;function Wp(){const{currentUser:e,setCurrentUser:t}=y.useContext(Pl)??{},n=e&&(e.role_bits&Hp)!==0,[r,l]=y.useState("profile");return o.jsxs("section",{className:"admin-view",children:[o.jsx("h1",{children:"Settings"}),o.jsx("div",{className:"admin-tabs",style:{display:"flex",gap:12,marginBottom:20},children:["profile","tokens",...n?["instance"]:[]].map(i=>o.jsx("button",{className:`nav-link${r===i?" is-active":""}`,style:{borderBottom:r===i?"2px solid var(--accent)":void 0,color:"var(--ink)"},onClick:()=>l(i),children:i==="profile"?"Profile":i==="tokens"?"API Tokens":"Instance"},i))}),r==="profile"&&o.jsx(Qp,{currentUser:e,setCurrentUser:t}),r==="tokens"&&o.jsx(Kp,{}),r==="instance"&&n&&o.jsx(Yp,{})]})}function Qp({currentUser:e,setCurrentUser:t}){const[n,r]=y.useState((e==null?void 0:e.display_name)??""),[l,i]=y.useState(!1),[s,u]=y.useState(null),[a,d]=y.useState(""),[v,m]=y.useState(""),[h,w]=y.useState(""),[x,S]=y.useState(!1),[j,f]=y.useState(null);async function c(g){g.preventDefault(),i(!0),u(null);try{await ap(n),t(N=>({...N,display_name:n||null})),u({ok:!0,text:"Saved."})}catch(N){u({ok:!1,text:N.message})}finally{i(!1)}}async function p(g){if(g.preventDefault(),v!==h){f({ok:!1,text:"Passwords do not match."});return}S(!0),f(null);try{await cp(a,v),d(""),m(""),w(""),f({ok:!0,text:"Password changed."})}catch(N){f({ok:!1,text:N.message})}finally{S(!1)}}return o.jsxs("div",{style:{maxWidth:480},children:[o.jsxs("div",{className:"admin-section",children:[o.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"Display Name"}),o.jsxs("form",{className:"admin-form",onSubmit:c,children:[o.jsx("input",{className:"admin-input",placeholder:(e==null?void 0:e.username)??"",value:n,onChange:g=>r(g.target.value)}),s&&o.jsx("div",{className:s.ok?"muted":"capture-error",children:s.text}),o.jsx("button",{className:"capture-submit",type:"submit",disabled:l,children:l?"Saving…":"Save"})]})]}),o.jsxs("div",{className:"admin-section",style:{marginTop:28},children:[o.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"Change Password"}),o.jsxs("form",{className:"admin-form",onSubmit:p,children:[o.jsx("input",{className:"admin-input",type:"password",placeholder:"Current password",value:a,onChange:g=>d(g.target.value),required:!0}),o.jsx("input",{className:"admin-input",type:"password",placeholder:"New password",value:v,onChange:g=>m(g.target.value),required:!0}),o.jsx("input",{className:"admin-input",type:"password",placeholder:"Confirm new password",value:h,onChange:g=>w(g.target.value),required:!0}),j&&o.jsx("div",{className:j.ok?"muted":"capture-error",children:j.text}),o.jsx("button",{className:"capture-submit",type:"submit",disabled:x,children:x?"Changing…":"Change Password"})]})]})]})}function Kp(){const[e,t]=y.useState([]),[n,r]=y.useState(!0),[l,i]=y.useState(null),[s,u]=y.useState(""),[a,d]=y.useState(!1),[v,m]=y.useState(null),h=y.useCallback(async()=>{r(!0),i(null);try{t(await dp())}catch(S){i(S.message)}finally{r(!1)}},[]);y.useEffect(()=>{h()},[h]);async function w(S){if(S.preventDefault(),!!s.trim()){d(!0);try{const j=await fp(s.trim());m(j),u(""),h()}catch(j){i(j.message)}finally{d(!1)}}}async function x(S){try{await pp(S),t(j=>j.filter(f=>f.token_uid!==S))}catch(j){i(j.message)}}return o.jsxs("div",{style:{maxWidth:640},children:[o.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"API Tokens"}),v&&o.jsxs("div",{style:{background:"#e8f5e9",border:"1px solid #a5d6a7",padding:"12px 14px",marginBottom:16,fontSize:13},children:[o.jsx("strong",{children:"Token created."})," Copy it now \\u2014 it will not be shown again.",o.jsx("br",{}),o.jsx("code",{style:{wordBreak:"break-all",display:"block",marginTop:6,padding:"6px 8px",background:"#f5f5f5",border:"1px solid #ddd"},children:v.raw_token}),o.jsx("button",{style:{marginTop:8,fontSize:12,border:"1px solid #aaa",background:"none",cursor:"pointer"},onClick:()=>m(null),children:"Dismiss"})]}),o.jsxs("form",{className:"admin-form",onSubmit:w,style:{display:"flex",gap:8,marginBottom:16},children:[o.jsx("input",{className:"admin-input",placeholder:"Token name",value:s,onChange:S=>u(S.target.value),style:{flex:1},required:!0}),o.jsx("button",{className:"capture-submit",type:"submit",disabled:a,children:a?"Creating…":"Create"})]}),l&&o.jsx("div",{className:"capture-error",children:l}),n?o.jsx("div",{className:"muted",children:"Loading\\u2026"}):o.jsxs("div",{className:"admin-list",children:[e.length===0&&o.jsx("div",{className:"muted",children:"No tokens yet."}),e.map(S=>o.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",border:"1px solid var(--line)",background:"var(--paper-3)",padding:"10px 12px"},children:[o.jsxs("div",{children:[o.jsx("strong",{style:{fontSize:14},children:S.name}),o.jsxs("div",{className:"muted",style:{fontSize:12,marginTop:2},children:["Created ",S.created_at.slice(0,10),S.last_used_at&&` · Last used ${S.last_used_at.slice(0,10)}`]})]}),o.jsx("button",{onClick:()=>x(S.token_uid),style:{border:"1px solid var(--line)",background:"none",cursor:"pointer",color:"var(--accent)",fontSize:12,padding:"4px 10px"},children:"Revoke"})]},S.token_uid))]})]})}function Yp(){const[e,t]=y.useState(null),[n,r]=y.useState(!0),[l,i]=y.useState(null),[s,u]=y.useState(!1),[a,d]=y.useState(null);y.useEffect(()=>{(async()=>{try{t(await hp())}catch(m){i(m.message)}finally{r(!1)}})()},[]);async function v(m){m.preventDefault(),u(!0),d(null);try{await mp(e),d({ok:!0,text:"Saved."})}catch(h){d({ok:!1,text:h.message})}finally{u(!1)}}return n?o.jsx("div",{className:"muted",children:"Loading\\u2026"}):l?o.jsx("div",{className:"capture-error",children:l}):e?o.jsxs("div",{style:{maxWidth:480},children:[o.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"Instance Settings"}),o.jsxs("form",{onSubmit:v,children:[o.jsxs("div",{className:"admin-section",children:[[["public_index_enabled","Public index (unauthenticated browsing)"],["public_entry_content_enabled","Public entry content"],["open_registration_enabled","Open registration"]].map(([m,h])=>o.jsxs("label",{style:{display:"flex",alignItems:"center",gap:10,marginBottom:12,cursor:"pointer"},children:[o.jsx("input",{type:"checkbox",checked:!!e[m],onChange:w=>t(x=>({...x,[m]:w.target.checked}))}),h]},m)),o.jsxs("div",{style:{marginBottom:12},children:[o.jsx("label",{style:{display:"block",fontWeight:600,marginBottom:4,fontSize:13},children:"Default entry visibility"}),o.jsxs("select",{className:"admin-input",value:e.default_entry_visibility,onChange:m=>t(h=>({...h,default_entry_visibility:Number(m.target.value)})),children:[o.jsx("option",{value:0,children:"Private (0)"}),o.jsx("option",{value:2,children:"Unlisted (2)"}),o.jsx("option",{value:3,children:"Public (3)"})]})]})]}),a&&o.jsx("div",{className:a.ok?"muted":"capture-error",children:a.text}),o.jsx("button",{className:"capture-submit",type:"submit",disabled:s,children:s?"Saving…":"Save Settings"})]})]}):null}function Xp({archiveId:e,selectedEntry:t,onTagFilterSet:n,tagNodes:r,onTagsRefresh:l}){const[i,s]=y.useState(null),[u,a]=y.useState([]),[d,v]=y.useState(""),[m,h]=y.useState([]),[w,x]=y.useState(""),S=y.useRef(0);y.useEffect(()=>{if(!t||!e){s(null),a([]),h([]);return}const c=++S.current;s(null),a([]),Promise.all([bf(e,t.entry_uid),ti(e,t.entry_uid),_p(e,t.entry_uid)]).then(([p,g,N])=>{c===S.current&&(s(p),a(g),h(N))}).catch(()=>{})},[t,e]);async function j(){const c=d.trim();if(!(!c||!t))try{await ep(e,t.entry_uid,c),v(""),x("");const p=await ti(e,t.entry_uid);a(p),l()}catch(p){x(p.message)}}async function f(c){try{await tp(e,t.entry_uid,c);const p=await ti(e,t.entry_uid);a(p),l()}catch{}}return o.jsxs("aside",{className:"context-rail",children:[o.jsx("div",{className:"rail-title",children:"Context"}),t?i?o.jsxs("div",{className:"rail-body",children:[o.jsx("strong",{className:"rail-entry-title",children:Ft(i.summary.title)||Ft(i.summary.entry_uid)}),o.jsxs("div",{className:"rail-section",children:[i.summary.original_url&&o.jsxs("div",{className:"rail-item",children:[o.jsx("span",{className:"rail-label",children:"Original URL"}),":"," ",o.jsx("a",{href:i.summary.original_url,target:"_blank",rel:"noopener noreferrer",className:"rail-url-link",children:i.summary.original_url})]}),[["Added",vc(i.summary.archived_at)],["Source",i.summary.source_kind],["Type",i.summary.entity_kind],["Visibility",i.summary.visibility],["Structured root",i.structured_root_relpath]].map(([c,p])=>o.jsxs("div",{className:"rail-item",children:[o.jsx("span",{className:"rail-label",children:c}),": ",Ft(p)]},c))]}),i.artifacts.length>0?o.jsxs("div",{className:"rail-section",children:[o.jsxs("div",{className:"rail-section-heading",children:["Artifacts (",i.artifacts.length,")"]}),o.jsx("ul",{className:"artifact-list",children:i.artifacts.map((c,p)=>o.jsx("li",{children:o.jsxs("a",{href:`/api/archives/${e}/entries/${i.summary.entry_uid}/artifacts/${p}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[c.artifact_role.replace(/_/g," "),c.byte_size!=null?` (${mc(c.byte_size)})`:""]})},p))})]}):o.jsx("div",{className:"rail-item muted",children:"No artifacts."})]}):o.jsx("div",{className:"rail-body",children:"Loading…"}):o.jsx("div",{className:"rail-body",children:"Select an entry."}),t&&o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"entry-tags",children:u.length===0?o.jsx("span",{className:"muted",children:"No tags."}):u.map(c=>o.jsxs("span",{className:"tag-pill",title:c.full_path,children:[c.name,o.jsx("button",{className:"remove-tag",title:`Remove tag ${c.full_path}`,onClick:()=>f(c.tag_uid),children:"×"})]},c.tag_uid))}),o.jsxs("div",{className:"assign-tag-form",children:[o.jsx("input",{className:"assign-tag-input",type:"text",placeholder:"/science/cs",autoComplete:"off",value:d,onChange:c=>v(c.target.value),onKeyDown:c=>{c.key==="Enter"&&j()}}),o.jsx("button",{className:"assign-tag-btn",onClick:j,children:"Add tag"}),w&&o.jsx("div",{className:"muted",style:{fontSize:"0.85em",color:"var(--accent)"},children:w})]}),t&&m.length>0&&o.jsxs("div",{className:"rail-section",children:[o.jsx("div",{className:"rail-section-heading",children:"Collections"}),m.map(c=>o.jsxs("div",{className:"rail-item",children:[o.jsx("span",{className:"rail-label",children:c.collection_uid}),":"," ",o.jsx("span",{className:"muted",children:{0:"Private",1:"Public",2:"Users only",3:"Public"}[c.visibility_bits]??`bits:${c.visibility_bits}`})]},c.collection_uid))]})]})]})}const Pl=y.createContext(null);function Gp(){const[e,t]=y.useState("loading"),[n,r]=y.useState(null);y.useEffect(()=>{(async()=>{if(await lp()){t("setup");return}const se=await up();if(!se){t("login");return}r(se),t("authenticated")})()},[]),y.useEffect(()=>{const M=()=>{r(null),t("login")};return window.addEventListener("auth:expired",M),()=>window.removeEventListener("auth:expired",M)},[]);const[l,i]=y.useState([]),[s,u]=y.useState(null),[a,d]=y.useState([]),[v,m]=y.useState(null),[h,w]=y.useState(null),[x,S]=y.useState(null),[j,f]=y.useState("archive"),[c,p]=y.useState(""),[g,N]=y.useState(""),[T,P]=y.useState(!1),[L,U]=y.useState([]),[D,J]=y.useState([]),[je,ye]=y.useState(!1),ge=y.useCallback(async(M,se,Re)=>{if(M){P(!0);try{let we;se||Re?we=await qf(M,se,Re):we=await Zf(M),d(we),N(we.length===0?"No results":`${we.length} result${we.length===1?"":"s"}`)}catch{d([]),N("Search failed. Try again.")}finally{P(!1)}}},[]);y.useEffect(()=>{Jf().then(M=>{if(i(M),M.length>0){const se=M[0].id;u(se)}})},[]),y.useEffect(()=>{s&&(S(null),w(null),m(null),Promise.all([ge(s,"",null),bo(s).then(U),ni(s).then(J)]))},[s]),y.useEffect(()=>{if(s===null)return;const M=setTimeout(()=>{ge(s,c,x)},300);return()=>clearTimeout(M)},[c,s]),y.useEffect(()=>{s!==null&&(f("archive"),ge(s,c,x))},[x,s]);const Ue=y.useCallback(M=>{u(M)},[]),Ae=y.useCallback(M=>{f(M),M==="tags"&&s&&ni(s).then(J)},[s]),Be=y.useCallback(M=>{m(M?M.entry_uid:null),w(M)},[]),E=y.useCallback(M=>{S(M)},[]),k=y.useCallback(()=>{S(null)},[]),R=y.useCallback(()=>{s&&ni(s).then(J)},[s]),A=y.useCallback(()=>{ye(!0)},[]),O=y.useCallback(()=>{ye(!1)},[]),de=y.useCallback(()=>{s&&Promise.all([ge(s,c,x),bo(s).then(U)])},[s,c,x,ge]);return e==="loading"?o.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?o.jsx(Lp,{onComplete:()=>t("login")}):e==="login"?o.jsx(zp,{onLogin:M=>{r(M),t("authenticated")}}):o.jsx(Pl.Provider,{value:{currentUser:n,setCurrentUser:r},children:o.jsxs(o.Fragment,{children:[o.jsx(Rp,{archives:l,archiveId:s,onArchiveChange:Ue,view:j,onViewChange:Ae,onCaptureClick:A}),o.jsxs("main",{className:"app-shell",children:[o.jsxs("div",{className:"workspace",children:[j==="archive"&&o.jsxs("div",{className:"search-row",children:[o.jsx("input",{className:"search-input",type:"search","aria-label":"Search archive","aria-busy":T,value:c,onChange:M=>p(M.target.value)}),o.jsxs("div",{className:"result-count",children:[g,x&&o.jsxs("button",{className:"tag-filter-badge",onClick:k,children:["× ",x]})]})]}),j==="archive"&&o.jsx($p,{entries:a,selectedEntryUid:v,onSelectEntry:Be,archiveId:s,tagFilter:x,onClearTagFilter:k,searchQuery:c,onSearchChange:p,resultCount:g,searchBusy:T}),j==="runs"&&o.jsx(Fp,{runs:L}),j==="admin"&&o.jsx(Up,{archives:l}),j==="tags"&&o.jsx(Ap,{tagNodes:D,tagFilter:x,onTagFilterSet:E,onViewChange:Ae}),j==="collections"&&o.jsx(Vp,{archiveId:s}),j==="settings"&&o.jsx(Wp,{})]}),o.jsx(Xp,{archiveId:s,selectedEntry:h,onTagFilterSet:E,tagNodes:D,onTagsRefresh:R})]}),o.jsx(Op,{open:je,archiveId:s,onClose:O,onCaptured:de})]})})}hc(document.getElementById("root")).render(o.jsx(y.StrictMode,{children:o.jsx(Gp,{})})); diff --git a/crates/archivr-server/static/assets/index-D40QFUPh.css b/crates/archivr-server/static/assets/index-D40QFUPh.css deleted file mode 100644 index 04f6484..0000000 --- a/crates/archivr-server/static/assets/index-D40QFUPh.css +++ /dev/null @@ -1 +0,0 @@ -:root{color-scheme:light;--ink: #20251f;--muted: #666a61;--paper: #f5f0e7;--paper-2: #e9e1d2;--paper-3: #fffaf0;--line: #d2c6b5;--line-soft: #e5dccd;--accent: #8d3f30;--accent-2: #b78342;--link: #245f72;--top: #141d18}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:var(--paper);color:var(--ink);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}button,input,select{font:inherit}.topbar{height:56px;display:grid;grid-template-columns:auto minmax(180px,250px) 1fr auto;align-items:center;gap:18px;padding:0 18px;background:var(--top);color:#f7eedf;border-bottom:3px solid var(--accent)}.brand{font-family:Georgia,Times New Roman,serif;font-size:28px;line-height:1}.archive-switcher{width:100%;min-width:0;border:1px solid rgba(247,238,223,.35);background:#202b25;color:#f7eedf;padding:7px 9px}.nav{display:flex;gap:16px;justify-content:flex-end;min-width:0}.nav-link{border:0;background:transparent;color:#d7cdbf;cursor:pointer;padding:8px 0}.nav-link.is-active{color:#fffaf0;border-bottom:1px solid #fffaf0}.capture-button{border:0;background:#f7eedf;color:var(--top);padding:9px 12px;cursor:pointer}.app-shell{height:calc(100vh - 56px);display:grid;grid-template-columns:minmax(0,1fr) 320px}.workspace{min-width:0;overflow:auto}.search-row{min-height:76px;display:grid;grid-template-columns:minmax(0,1fr) max-content;gap:18px;align-items:center;padding:14px 16px;background:var(--paper-2);border-bottom:1px solid var(--line)}.search-input{width:100%;height:46px;border:2px solid var(--ink);background:var(--paper-3);color:var(--ink);padding:0 14px;font-size:16px;outline-offset:3px}.result-count{min-width:76px;color:var(--muted);font-size:13px;text-align:right}.view{display:none}.view.is-active{display:block}.entry-table{width:100%;font-size:13px;overflow:auto}.entry-header-row{display:flex;align-items:stretch;position:sticky;top:0;z-index:1;background:#ded5c7;border-bottom:1px solid #cec4b5;color:#5d625e;font-size:11px;text-transform:uppercase}.entry-header-row>div{padding:10px;flex-shrink:0}#entries-body>div{display:flex;align-items:baseline;cursor:default;border-bottom:1px solid var(--line-soft)}#entries-body>div>div{padding:10px;flex-shrink:0;overflow:hidden}#entries-body>div:nth-child(2n){background:#f2ede5}#entries-body>div:nth-child(odd){background:var(--paper-3)}#entries-body>div.is-selected{background:#eee2d2;outline:2px solid var(--accent);outline-offset:-2px}.col-added{width:168px}.col-title{flex:1 1 0;min-width:0;overflow:hidden}.col-type{width:120px}.col-size{width:100px}.col-url{flex:0 0 30%;min-width:0;overflow:hidden}#runs-view .entry-table{border-collapse:collapse;table-layout:auto}#runs-view .entry-table th{position:sticky;top:0;z-index:1;text-align:left;padding:10px;background:#ded5c7;color:#5d625e;border-bottom:1px solid #cec4b5;font-size:11px;text-transform:uppercase}#runs-view .entry-table td{padding:10px;border-bottom:1px solid var(--line-soft);vertical-align:top}#runs-view .entry-table tr:nth-child(2n) td{background:#f2ede5}#runs-view .entry-table tr:nth-child(odd) td{background:var(--paper-3)}.entry-title{color:var(--link);font-weight:750}.source-icon{display:inline-flex;align-items:center;width:1em;height:1em;margin-right:.35em;vertical-align:middle;flex-shrink:0}.source-icon svg{width:100%;height:100%}.url-cell{color:#555b55;word-break:break-all}.type-pill{display:inline-block;padding:2px 6px;background:#d8e3df;color:#275a5f;border:1px solid #bfd0ca}.context-rail{border-left:1px solid var(--line);background:#efe7dc;padding:18px;overflow:auto}.rail-title{color:var(--accent);font-weight:800;margin-bottom:10px;text-transform:uppercase;font-size:12px;letter-spacing:0}.rail-body{color:var(--muted);font-size:14px;line-height:1.6}.rail-body strong{color:var(--ink)}.rail-item{padding:10px 0;border-bottom:1px solid var(--line)}.admin-view{padding:22px}.admin-view h1{margin:0 0 16px;font-size:18px}.admin-list{display:grid;gap:10px;max-width:780px}.admin-archive{border:1px solid var(--line);background:var(--paper-3);padding:12px}.muted{color:var(--muted)}@media (max-width: 900px){.topbar{grid-template-columns:1fr auto;height:auto;min-height:56px;padding:12px}.archive-switcher,.nav{grid-column:1 / -1}.nav{justify-content:flex-start;overflow-x:auto}.app-shell{height:auto;grid-template-columns:1fr}.search-row{grid-template-columns:1fr}.result-count{text-align:left}.context-rail{border-left:0;border-top:1px solid var(--line)}.entry-table{min-width:860px}}.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:.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}.search-input[aria-busy=true]{cursor:progress}.tag-tree{padding:12px}.tag-tree-list{list-style:none;margin:0;padding:0}.tag-children{padding-left:16px}.tag-node-btn{border:0;background:transparent;color:var(--link);cursor:pointer;padding:3px 0;text-align:left;font-size:13px;display:block;width:100%}.tag-node-btn:hover{text-decoration:underline}.tag-node-btn.is-active{font-weight:600;color:var(--ink)}.entry-tags{display:flex;flex-wrap:wrap;gap:6px;padding:8px 0}.tag-pill{display:inline-flex;align-items:center;gap:4px;background:var(--paper-2);border:1px solid var(--line);border-radius:3px;font-size:12px;padding:2px 6px;color:var(--ink)}.remove-tag{border:0;background:none;cursor:pointer;color:var(--muted);padding:0;font-size:14px;line-height:1}.remove-tag:hover{color:var(--accent)}.assign-tag-form{display:flex;gap:6px;padding:6px 0 8px;border-top:1px solid var(--line-soft);margin-top:4px}.assign-tag-input{flex:1;min-width:0;border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:4px 7px;font-size:12px}.assign-tag-btn{border:1px solid var(--line);background:var(--paper-2);color:var(--ink);cursor:pointer;padding:4px 10px;font-size:12px;white-space:nowrap}.assign-tag-btn:hover{background:var(--paper)}.tag-filter-badge{display:inline-flex;align-items:center;gap:4px;background:var(--accent);color:#fff;border:0;border-radius:3px;font-size:12px;padding:2px 8px;cursor:pointer;margin-left:8px}.tag-filter-badge:hover{opacity:.85}.capture-dialog{border:2px solid var(--ink);background:var(--paper);padding:0;min-width:360px;max-width:520px;box-shadow:4px 4px 0 var(--ink)}.capture-dialog::backdrop{background:#00000073}.capture-dialog-inner{padding:28px}.capture-dialog-title{margin:0 0 16px;font-size:18px;font-family:Georgia,Times New Roman,serif}.capture-label{display:block;font-size:13px;font-weight:600;margin-bottom:6px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.capture-input{width:100%;height:42px;border:2px solid var(--ink);background:var(--paper-3);color:var(--ink);padding:0 12px;font-size:15px;margin-bottom:10px;outline-offset:3px}.capture-error{color:var(--accent);font-size:13px;margin-bottom:10px;min-height:18px}.capture-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:16px}.capture-cancel{border:1px solid var(--line);background:none;color:var(--ink);padding:8px 18px;cursor:pointer}.capture-cancel:hover{background:var(--paper-2)}.capture-submit{border:0;background:var(--ink);color:var(--paper);padding:8px 20px;cursor:pointer;font-weight:600}.capture-submit:hover{opacity:.85}.capture-submit:disabled{opacity:.45;cursor:default}.collections-view{padding:24px}.collections-heading{margin:0 0 16px;font-size:22px;font-family:Georgia,Times New Roman,serif}.collections-error{color:var(--accent);font-size:13px;margin-bottom:8px}.coll-dismiss{background:none;border:none;cursor:pointer;color:var(--accent);font-size:16px;line-height:1;padding:0 4px}.collections-layout{display:grid;grid-template-columns:220px 1fr;gap:0;border:1px solid var(--line);min-height:340px}.collections-sidebar{border-right:1px solid var(--line);overflow-y:auto}.coll-row{display:flex;flex-direction:column;width:100%;padding:10px 14px;border:none;border-bottom:1px solid var(--line-soft);background:none;cursor:pointer;text-align:left;gap:2px}.coll-row:hover{background:var(--paper-2)}.coll-row.is-active{background:var(--paper-2);border-left:3px solid var(--accent)}.coll-row-name{font-size:14px;font-weight:600;color:var(--ink)}.coll-row-meta{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.coll-detail{padding:20px 24px;display:flex;flex-direction:column;gap:16px}.coll-detail--empty{justify-content:center;align-items:center}.coll-detail-header{display:flex;align-items:center;gap:12px}.coll-detail-name{margin:0;font-size:18px;font-family:Georgia,Times New Roman,serif;flex:1}.coll-detail-name--editable{cursor:pointer}.coll-detail-name--editable:hover{color:var(--link)}.coll-edit-hint{font-size:14px;opacity:.5;margin-left:4px}.coll-rename-input{flex:1;font-size:16px;font-family:Georgia,Times New Roman,serif;border:2px solid var(--ink);padding:4px 8px;background:var(--paper-3);color:var(--ink)}.coll-delete-btn{border:1px solid var(--accent);background:none;color:var(--accent);padding:5px 12px;font-size:13px;cursor:pointer}.coll-delete-btn:hover{background:var(--accent);color:var(--paper)}.coll-detail-vis{display:flex;align-items:center;gap:10px}.coll-vis-label{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}.coll-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:4px 8px;font-size:13px}.coll-section-heading{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}.coll-entries-section{flex:1}.coll-entries-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}.coll-entry-row{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid var(--line-soft)}.coll-entry-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.coll-entry-title{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.coll-entry-kind{font-size:11px;text-transform:uppercase;letter-spacing:.04em}.coll-entry-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.coll-entry-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:3px 6px;font-size:12px}.coll-entry-remove{border:none;background:none;cursor:pointer;color:var(--muted);font-size:18px;line-height:1;padding:0 4px}.coll-entry-remove:hover{color:var(--accent)}.coll-add-entry-form{border-top:1px solid var(--line-soft);padding-top:12px}.coll-add-entry-row{display:flex;gap:8px;align-items:center}.coll-add-entry-input{flex:1;border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:6px 10px;font-size:13px;min-width:0}.coll-add-btn{border:none;background:var(--ink);color:var(--paper);padding:6px 14px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap}.coll-add-btn:hover{opacity:.85}.coll-add-btn:disabled{opacity:.45;cursor:default}.coll-create-details summary{font-weight:600;cursor:pointer} diff --git a/crates/archivr-server/static/assets/index-DRRKzKIq.js b/crates/archivr-server/static/assets/index-DRRKzKIq.js deleted file mode 100644 index a5d6cd2..0000000 --- a/crates/archivr-server/static/assets/index-DRRKzKIq.js +++ /dev/null @@ -1,40 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var Zo={exports:{}},ol={},qo={exports:{}},D={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var rr=Symbol.for("react.element"),pc=Symbol.for("react.portal"),hc=Symbol.for("react.fragment"),mc=Symbol.for("react.strict_mode"),vc=Symbol.for("react.profiler"),gc=Symbol.for("react.provider"),yc=Symbol.for("react.context"),wc=Symbol.for("react.forward_ref"),xc=Symbol.for("react.suspense"),Sc=Symbol.for("react.memo"),kc=Symbol.for("react.lazy"),Is=Symbol.iterator;function Cc(e){return e===null||typeof e!="object"?null:(e=Is&&e[Is]||e["@@iterator"],typeof e=="function"?e:null)}var bo={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},eu=Object.assign,tu={};function vn(e,t,n){this.props=e,this.context=t,this.refs=tu,this.updater=n||bo}vn.prototype.isReactComponent={};vn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};vn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function nu(){}nu.prototype=vn.prototype;function Wi(e,t,n){this.props=e,this.context=t,this.refs=tu,this.updater=n||bo}var Qi=Wi.prototype=new nu;Qi.constructor=Wi;eu(Qi,vn.prototype);Qi.isPureReactComponent=!0;var Us=Array.isArray,ru=Object.prototype.hasOwnProperty,Ki={current:null},lu={key:!0,ref:!0,__self:!0,__source:!0};function iu(e,t,n){var r,l={},i=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(i=""+t.key),t)ru.call(t,r)&&!lu.hasOwnProperty(r)&&(l[r]=t[r]);var o=arguments.length-2;if(o===1)l.children=n;else if(1>>1,V=E[I];if(0>>1;Il(Oe,R))Vel(De,Oe)?(E[I]=De,E[Ve]=R,I=Ve):(E[I]=Oe,E[M]=R,I=M);else if(Vel(De,R))E[I]=De,E[Ve]=R,I=Ve;else break e}}return C}function l(E,C){var R=E.sortIndex-C.sortIndex;return R!==0?R:E.id-C.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var s=Date,o=s.now();e.unstable_now=function(){return s.now()-o}}var a=[],f=[],v=1,m=null,h=3,w=!1,S=!1,x=!1,j=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,c=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(E){for(var C=n(f);C!==null;){if(C.callback===null)r(f);else if(C.startTime<=E)r(f),C.sortIndex=C.expirationTime,t(a,C);else break;C=n(f)}}function g(E){if(x=!1,p(E),!S)if(n(a)!==null)S=!0,it(N);else{var C=n(f);C!==null&&Et(g,C.startTime-E)}}function N(E,C){S=!1,x&&(x=!1,d(z),z=-1),w=!0;var R=h;try{for(p(C),m=n(a);m!==null&&(!(m.expirationTime>C)||E&&!X());){var I=m.callback;if(typeof I=="function"){m.callback=null,h=m.priorityLevel;var V=I(m.expirationTime<=C);C=e.unstable_now(),typeof V=="function"?m.callback=V:m===n(a)&&r(a),p(C)}else r(a);m=n(a)}if(m!==null)var Vt=!0;else{var M=n(f);M!==null&&Et(g,M.startTime-C),Vt=!1}return Vt}finally{m=null,h=R,w=!1}}var P=!1,_=null,z=-1,L=5,O=-1;function X(){return!(e.unstable_now()-OE||125I?(E.sortIndex=R,t(f,E),n(a)===null&&E===n(f)&&(x?(d(z),z=-1):x=!0,Et(g,R-I))):(E.sortIndex=V,t(a,E),S||w||(S=!0,it(N))),E},e.unstable_shouldYield=X,e.unstable_wrapCallback=function(E){var C=h;return function(){var R=h;h=C;try{return E.apply(this,arguments)}finally{h=R}}}})(cu);au.exports=cu;var Dc=au.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Mc=y,ke=Dc;function k(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ql=Object.prototype.hasOwnProperty,Fc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Bs={},Vs={};function $c(e){return ql.call(Vs,e)?!0:ql.call(Bs,e)?!1:Fc.test(e)?Vs[e]=!0:(Bs[e]=!0,!1)}function Ic(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Uc(e,t,n,r){if(t===null||typeof t>"u"||Ic(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function fe(e,t,n,r,l,i,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=s}var re={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){re[e]=new fe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];re[t]=new fe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){re[e]=new fe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){re[e]=new fe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){re[e]=new fe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){re[e]=new fe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){re[e]=new fe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){re[e]=new fe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){re[e]=new fe(e,5,!1,e.toLowerCase(),null,!1,!1)});var Xi=/[\-:]([a-z])/g;function Gi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Xi,Gi);re[t]=new fe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Xi,Gi);re[t]=new fe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Xi,Gi);re[t]=new fe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){re[e]=new fe(e,1,!1,e.toLowerCase(),null,!1,!1)});re.xlinkHref=new fe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){re[e]=new fe(e,1,!1,e.toLowerCase(),null,!0,!0)});function Ji(e,t,n,r){var l=re.hasOwnProperty(t)?re[t]:null;(l!==null?l.type!==0:r||!(2o||l[s]!==i[o]){var a=` -`+l[s].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=s&&0<=o);break}}}finally{El=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?_n(e):""}function Ac(e){switch(e.tag){case 5:return _n(e.type);case 16:return _n("Lazy");case 13:return _n("Suspense");case 19:return _n("SuspenseList");case 0:case 2:case 15:return e=_l(e.type,!1),e;case 11:return e=_l(e.type.render,!1),e;case 1:return e=_l(e.type,!0),e;default:return""}}function ni(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Qt:return"Fragment";case Wt:return"Portal";case bl:return"Profiler";case Zi:return"StrictMode";case ei:return"Suspense";case ti:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case pu:return(e.displayName||"Context")+".Consumer";case du:return(e._context.displayName||"Context")+".Provider";case qi:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case bi:return t=e.displayName||null,t!==null?t:ni(e.type)||"Memo";case ot:t=e._payload,e=e._init;try{return ni(e(t))}catch{}}return null}function Bc(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ni(t);case 8:return t===Zi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function St(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function mu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Vc(e){var t=mu(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(s){r=""+s,i.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function cr(e){e._valueTracker||(e._valueTracker=Vc(e))}function vu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=mu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $r(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ri(e,t){var n=t.checked;return K({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Ws(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=St(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function gu(e,t){t=t.checked,t!=null&&Ji(e,"checked",t,!1)}function li(e,t){gu(e,t);var n=St(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ii(e,t.type,n):t.hasOwnProperty("defaultValue")&&ii(e,t.type,St(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Qs(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ii(e,t,n){(t!=="number"||$r(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Pn=Array.isArray;function nn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=fr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Bn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ln={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Hc=["Webkit","ms","Moz","O"];Object.keys(Ln).forEach(function(e){Hc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ln[t]=Ln[e]})});function Su(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Ln.hasOwnProperty(e)&&Ln[e]?(""+t).trim():t+"px"}function ku(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Su(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Wc=K({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ui(e,t){if(t){if(Wc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(k(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(k(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(k(61))}if(t.style!=null&&typeof t.style!="object")throw Error(k(62))}}function ai(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ci=null;function es(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var fi=null,rn=null,ln=null;function Xs(e){if(e=sr(e)){if(typeof fi!="function")throw Error(k(280));var t=e.stateNode;t&&(t=dl(t),fi(e.stateNode,e.type,t))}}function Cu(e){rn?ln?ln.push(e):ln=[e]:rn=e}function Nu(){if(rn){var e=rn,t=ln;if(ln=rn=null,Xs(e),t)for(e=0;e>>=0,e===0?32:31-(tf(e)/nf|0)|0}var dr=64,pr=4194304;function Tn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Br(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,s=n&268435455;if(s!==0){var o=s&~l;o!==0?r=Tn(o):(i&=s,i!==0&&(r=Tn(i)))}else s=n&~l,s!==0?r=Tn(s):i!==0&&(r=Tn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function lr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ue(t),e[t]=n}function of(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=On),ro=" ",lo=!1;function Wu(e,t){switch(e){case"keyup":return Mf.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Qu(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Kt=!1;function $f(e,t){switch(e){case"compositionend":return Qu(t);case"keypress":return t.which!==32?null:(lo=!0,ro);case"textInput":return e=t.data,e===ro&&lo?null:e;default:return null}}function If(e,t){if(Kt)return e==="compositionend"||!us&&Wu(e,t)?(e=Vu(),Pr=is=ft=null,Kt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=uo(n)}}function Gu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Gu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ju(){for(var e=window,t=$r();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=$r(e.document)}return t}function as(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Yf(e){var t=Ju(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Gu(n.ownerDocument.documentElement,n)){if(r!==null&&as(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=ao(n,i);var s=ao(n,r);l&&s&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Yt=null,gi=null,Mn=null,yi=!1;function co(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;yi||Yt==null||Yt!==$r(r)||(r=Yt,"selectionStart"in r&&as(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Mn&&Yn(Mn,r)||(Mn=r,r=Wr(gi,"onSelect"),0Jt||(e.current=Ni[Jt],Ni[Jt]=null,Jt--)}function U(e,t){Jt++,Ni[Jt]=e.current,e.current=t}var kt={},oe=Nt(kt),me=Nt(!1),Mt=kt;function cn(e,t){var n=e.type.contextTypes;if(!n)return kt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ve(e){return e=e.childContextTypes,e!=null}function Kr(){B(me),B(oe)}function yo(e,t,n){if(oe.current!==kt)throw Error(k(168));U(oe,t),U(me,n)}function ia(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(k(108,Bc(e)||"Unknown",l));return K({},n,r)}function Yr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||kt,Mt=oe.current,U(oe,e),U(me,me.current),!0}function wo(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=ia(e,t,Mt),r.__reactInternalMemoizedMergedChildContext=e,B(me),B(oe),U(oe,e)):B(me),U(me,n)}var Ge=null,pl=!1,Bl=!1;function sa(e){Ge===null?Ge=[e]:Ge.push(e)}function id(e){pl=!0,sa(e)}function jt(){if(!Bl&&Ge!==null){Bl=!0;var e=0,t=$;try{var n=Ge;for($=1;e>=s,l-=s,Je=1<<32-Ue(t)+l|n<z?(L=_,_=null):L=_.sibling;var O=h(d,_,p[z],g);if(O===null){_===null&&(_=L);break}e&&_&&O.alternate===null&&t(d,_),c=i(O,c,z),P===null?N=O:P.sibling=O,P=O,_=L}if(z===p.length)return n(d,_),H&&_t(d,z),N;if(_===null){for(;zz?(L=_,_=null):L=_.sibling;var X=h(d,_,O.value,g);if(X===null){_===null&&(_=L);break}e&&_&&X.alternate===null&&t(d,_),c=i(X,c,z),P===null?N=X:P.sibling=X,P=X,_=L}if(O.done)return n(d,_),H&&_t(d,z),N;if(_===null){for(;!O.done;z++,O=p.next())O=m(d,O.value,g),O!==null&&(c=i(O,c,z),P===null?N=O:P.sibling=O,P=O);return H&&_t(d,z),N}for(_=r(d,_);!O.done;z++,O=p.next())O=w(_,d,z,O.value,g),O!==null&&(e&&O.alternate!==null&&_.delete(O.key===null?z:O.key),c=i(O,c,z),P===null?N=O:P.sibling=O,P=O);return e&&_.forEach(function(ye){return t(d,ye)}),H&&_t(d,z),N}function j(d,c,p,g){if(typeof p=="object"&&p!==null&&p.type===Qt&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case ar:e:{for(var N=p.key,P=c;P!==null;){if(P.key===N){if(N=p.type,N===Qt){if(P.tag===7){n(d,P.sibling),c=l(P,p.props.children),c.return=d,d=c;break e}}else if(P.elementType===N||typeof N=="object"&&N!==null&&N.$$typeof===ot&&ko(N)===P.type){n(d,P.sibling),c=l(P,p.props),c.ref=Nn(d,P,p),c.return=d,d=c;break e}n(d,P);break}else t(d,P);P=P.sibling}p.type===Qt?(c=Dt(p.props.children,d.mode,g,p.key),c.return=d,d=c):(g=Fr(p.type,p.key,p.props,null,d.mode,g),g.ref=Nn(d,c,p),g.return=d,d=g)}return s(d);case Wt:e:{for(P=p.key;c!==null;){if(c.key===P)if(c.tag===4&&c.stateNode.containerInfo===p.containerInfo&&c.stateNode.implementation===p.implementation){n(d,c.sibling),c=l(c,p.children||[]),c.return=d,d=c;break e}else{n(d,c);break}else t(d,c);c=c.sibling}c=Gl(p,d.mode,g),c.return=d,d=c}return s(d);case ot:return P=p._init,j(d,c,P(p._payload),g)}if(Pn(p))return S(d,c,p,g);if(wn(p))return x(d,c,p,g);xr(d,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,c!==null&&c.tag===6?(n(d,c.sibling),c=l(c,p),c.return=d,d=c):(n(d,c),c=Xl(p,d.mode,g),c.return=d,d=c),s(d)):n(d,c)}return j}var dn=ca(!0),fa=ca(!1),Jr=Nt(null),Zr=null,bt=null,ps=null;function hs(){ps=bt=Zr=null}function ms(e){var t=Jr.current;B(Jr),e._currentValue=t}function _i(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function on(e,t){Zr=e,ps=bt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(he=!0),e.firstContext=null)}function Te(e){var t=e._currentValue;if(ps!==e)if(e={context:e,memoizedValue:t,next:null},bt===null){if(Zr===null)throw Error(k(308));bt=e,Zr.dependencies={lanes:0,firstContext:e}}else bt=bt.next=e;return t}var zt=null;function vs(e){zt===null?zt=[e]:zt.push(e)}function da(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,vs(t)):(n.next=l.next,l.next=n),t.interleaved=n,tt(e,r)}function tt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var ut=!1;function gs(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function pa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function qe(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function gt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,F&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,tt(e,n)}return l=r.interleaved,l===null?(t.next=t,vs(r)):(t.next=l.next,l.next=t),r.interleaved=t,tt(e,n)}function zr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ns(e,n)}}function Co(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=s:i=i.next=s,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function qr(e,t,n,r){var l=e.updateQueue;ut=!1;var i=l.firstBaseUpdate,s=l.lastBaseUpdate,o=l.shared.pending;if(o!==null){l.shared.pending=null;var a=o,f=a.next;a.next=null,s===null?i=f:s.next=f,s=a;var v=e.alternate;v!==null&&(v=v.updateQueue,o=v.lastBaseUpdate,o!==s&&(o===null?v.firstBaseUpdate=f:o.next=f,v.lastBaseUpdate=a))}if(i!==null){var m=l.baseState;s=0,v=f=a=null,o=i;do{var h=o.lane,w=o.eventTime;if((r&h)===h){v!==null&&(v=v.next={eventTime:w,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var S=e,x=o;switch(h=t,w=n,x.tag){case 1:if(S=x.payload,typeof S=="function"){m=S.call(w,m,h);break e}m=S;break e;case 3:S.flags=S.flags&-65537|128;case 0:if(S=x.payload,h=typeof S=="function"?S.call(w,m,h):S,h==null)break e;m=K({},m,h);break e;case 2:ut=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[o]:h.push(o))}else w={eventTime:w,lane:h,tag:o.tag,payload:o.payload,callback:o.callback,next:null},v===null?(f=v=w,a=m):v=v.next=w,s|=h;if(o=o.next,o===null){if(o=l.shared.pending,o===null)break;h=o,o=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(v===null&&(a=m),l.baseState=a,l.firstBaseUpdate=f,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do s|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);It|=s,e.lanes=s,e.memoizedState=m}}function No(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Hl.transition;Hl.transition={};try{e(!1),t()}finally{$=n,Hl.transition=r}}function za(){return ze().memoizedState}function ad(e,t,n){var r=wt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},La(e))Ra(t,n);else if(n=da(e,t,n,r),n!==null){var l=ae();Ae(n,e,r,l),Oa(n,t,r)}}function cd(e,t,n){var r=wt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(La(e))Ra(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,o=i(s,n);if(l.hasEagerState=!0,l.eagerState=o,Be(o,s)){var a=t.interleaved;a===null?(l.next=l,vs(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}finally{}n=da(e,t,l,r),n!==null&&(l=ae(),Ae(n,e,r,l),Oa(n,t,r))}}function La(e){var t=e.alternate;return e===Q||t!==null&&t===Q}function Ra(e,t){Fn=el=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Oa(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ns(e,n)}}var tl={readContext:Te,useCallback:le,useContext:le,useEffect:le,useImperativeHandle:le,useInsertionEffect:le,useLayoutEffect:le,useMemo:le,useReducer:le,useRef:le,useState:le,useDebugValue:le,useDeferredValue:le,useTransition:le,useMutableSource:le,useSyncExternalStore:le,useId:le,unstable_isNewReconciler:!1},fd={readContext:Te,useCallback:function(e,t){return We().memoizedState=[e,t===void 0?null:t],e},useContext:Te,useEffect:Eo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Rr(4194308,4,ja.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Rr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Rr(4,2,e,t)},useMemo:function(e,t){var n=We();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=We();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=ad.bind(null,Q,e),[r.memoizedState,e]},useRef:function(e){var t=We();return e={current:e},t.memoizedState=e},useState:jo,useDebugValue:js,useDeferredValue:function(e){return We().memoizedState=e},useTransition:function(){var e=jo(!1),t=e[0];return e=ud.bind(null,e[1]),We().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Q,l=We();if(H){if(n===void 0)throw Error(k(407));n=n()}else{if(n=t(),ee===null)throw Error(k(349));$t&30||ga(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Eo(wa.bind(null,r,i,e),[e]),r.flags|=2048,tr(9,ya.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=We(),t=ee.identifierPrefix;if(H){var n=Ze,r=Je;n=(r&~(1<<32-Ue(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=bn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[Qe]=t,e[Jn]=r,Ha(e,t,!1,!1),t.stateNode=e;e:{switch(s=ai(n,r),n){case"dialog":A("cancel",e),A("close",e),l=r;break;case"iframe":case"object":case"embed":A("load",e),l=r;break;case"video":case"audio":for(l=0;lmn&&(t.flags|=128,r=!0,jn(i,!1),t.lanes=4194304)}else{if(!r)if(e=br(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),jn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!s.alternate&&!H)return ie(t),null}else 2*G()-i.renderingStartTime>mn&&n!==1073741824&&(t.flags|=128,r=!0,jn(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(n=i.last,n!==null?n.sibling=s:t.child=s,i.last=s)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=G(),t.sibling=null,n=W.current,U(W,r?n&1|2:n&1),t):(ie(t),null);case 22:case 23:return Ls(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?we&1073741824&&(ie(t),t.subtreeFlags&6&&(t.flags|=8192)):ie(t),null;case 24:return null;case 25:return null}throw Error(k(156,t.tag))}function wd(e,t){switch(fs(t),t.tag){case 1:return ve(t.type)&&Kr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return pn(),B(me),B(oe),xs(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return ws(t),null;case 13:if(B(W),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(k(340));fn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return B(W),null;case 4:return pn(),null;case 10:return ms(t.type._context),null;case 22:case 23:return Ls(),null;case 24:return null;default:return null}}var kr=!1,se=!1,xd=typeof WeakSet=="function"?WeakSet:Set,T=null;function en(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Y(e,t,r)}else n.current=null}function Fi(e,t,n){try{n()}catch(r){Y(e,t,r)}}var $o=!1;function Sd(e,t){if(wi=Vr,e=Ju(),as(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var s=0,o=-1,a=-1,f=0,v=0,m=e,h=null;t:for(;;){for(var w;m!==n||l!==0&&m.nodeType!==3||(o=s+l),m!==i||r!==0&&m.nodeType!==3||(a=s+r),m.nodeType===3&&(s+=m.nodeValue.length),(w=m.firstChild)!==null;)h=m,m=w;for(;;){if(m===e)break t;if(h===n&&++f===l&&(o=s),h===i&&++v===r&&(a=s),(w=m.nextSibling)!==null)break;m=h,h=m.parentNode}m=w}n=o===-1||a===-1?null:{start:o,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(xi={focusedElem:e,selectionRange:n},Vr=!1,T=t;T!==null;)if(t=T,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,T=e;else for(;T!==null;){t=T;try{var S=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var x=S.memoizedProps,j=S.memoizedState,d=t.stateNode,c=d.getSnapshotBeforeUpdate(t.elementType===t.type?x:Fe(t.type,x),j);d.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(k(163))}}catch(g){Y(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,T=e;break}T=t.return}return S=$o,$o=!1,S}function $n(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Fi(t,n,i)}l=l.next}while(l!==r)}}function vl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function $i(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Ka(e){var t=e.alternate;t!==null&&(e.alternate=null,Ka(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Qe],delete t[Jn],delete t[Ci],delete t[rd],delete t[ld])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Ya(e){return e.tag===5||e.tag===3||e.tag===4}function Io(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ya(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ii(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Qr));else if(r!==4&&(e=e.child,e!==null))for(Ii(e,t,n),e=e.sibling;e!==null;)Ii(e,t,n),e=e.sibling}function Ui(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Ui(e,t,n),e=e.sibling;e!==null;)Ui(e,t,n),e=e.sibling}var te=null,$e=!1;function st(e,t,n){for(n=n.child;n!==null;)Xa(e,t,n),n=n.sibling}function Xa(e,t,n){if(Ke&&typeof Ke.onCommitFiberUnmount=="function")try{Ke.onCommitFiberUnmount(ul,n)}catch{}switch(n.tag){case 5:se||en(n,t);case 6:var r=te,l=$e;te=null,st(e,t,n),te=r,$e=l,te!==null&&($e?(e=te,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):te.removeChild(n.stateNode));break;case 18:te!==null&&($e?(e=te,n=n.stateNode,e.nodeType===8?Al(e.parentNode,n):e.nodeType===1&&Al(e,n),Qn(e)):Al(te,n.stateNode));break;case 4:r=te,l=$e,te=n.stateNode.containerInfo,$e=!0,st(e,t,n),te=r,$e=l;break;case 0:case 11:case 14:case 15:if(!se&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,s=i.destroy;i=i.tag,s!==void 0&&(i&2||i&4)&&Fi(n,t,s),l=l.next}while(l!==r)}st(e,t,n);break;case 1:if(!se&&(en(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){Y(n,t,o)}st(e,t,n);break;case 21:st(e,t,n);break;case 22:n.mode&1?(se=(r=se)||n.memoizedState!==null,st(e,t,n),se=r):st(e,t,n);break;default:st(e,t,n)}}function Uo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new xd),t.forEach(function(r){var l=zd.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Me(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=s),r&=~i}if(r=l,r=G()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Cd(r/1960))-r,10e?16:e,dt===null)var r=!1;else{if(e=dt,dt=null,ll=0,F&6)throw Error(k(331));var l=F;for(F|=4,T=e.current;T!==null;){var i=T,s=i.child;if(T.flags&16){var o=i.deletions;if(o!==null){for(var a=0;aG()-Ts?Ot(e,0):Ps|=n),ge(e,t)}function nc(e,t){t===0&&(e.mode&1?(t=pr,pr<<=1,!(pr&130023424)&&(pr=4194304)):t=1);var n=ae();e=tt(e,t),e!==null&&(lr(e,t,n),ge(e,n))}function Td(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),nc(e,n)}function zd(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(k(314))}r!==null&&r.delete(t),nc(e,n)}var rc;rc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||me.current)he=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return he=!1,gd(e,t,n);he=!!(e.flags&131072)}else he=!1,H&&t.flags&1048576&&oa(t,Gr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Or(e,t),e=t.pendingProps;var l=cn(t,oe.current);on(t,n),l=ks(null,t,r,e,l,n);var i=Cs();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ve(r)?(i=!0,Yr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,gs(t),l.updater=ml,t.stateNode=l,l._reactInternals=t,Ti(t,r,e,n),t=Ri(null,t,r,!0,i,n)):(t.tag=0,H&&i&&cs(t),ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Or(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Rd(r),e=Fe(r,e),l){case 0:t=Li(null,t,r,e,n);break e;case 1:t=Do(null,t,r,e,n);break e;case 11:t=Ro(null,t,r,e,n);break e;case 14:t=Oo(null,t,r,Fe(r.type,e),n);break e}throw Error(k(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Li(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Do(e,t,r,l,n);case 3:e:{if(Aa(t),e===null)throw Error(k(387));r=t.pendingProps,i=t.memoizedState,l=i.element,pa(e,t),qr(t,r,null,n);var s=t.memoizedState;if(r=s.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=hn(Error(k(423)),t),t=Mo(e,t,r,n,l);break e}else if(r!==l){l=hn(Error(k(424)),t),t=Mo(e,t,r,n,l);break e}else for(xe=vt(t.stateNode.containerInfo.firstChild),Se=t,H=!0,Ie=null,n=fa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(fn(),r===l){t=nt(e,t,n);break e}ue(e,t,r,n)}t=t.child}return t;case 5:return ha(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,s=l.children,Si(r,l)?s=null:i!==null&&Si(r,i)&&(t.flags|=32),Ua(e,t),ue(e,t,s,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return Ba(e,t,n);case 4:return ys(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=dn(t,null,r,n):ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Ro(e,t,r,l,n);case 7:return ue(e,t,t.pendingProps,n),t.child;case 8:return ue(e,t,t.pendingProps.children,n),t.child;case 12:return ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,s=l.value,U(Jr,r._currentValue),r._currentValue=s,i!==null)if(Be(i.value,s)){if(i.children===l.children&&!me.current){t=nt(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var o=i.dependencies;if(o!==null){s=i.child;for(var a=o.firstContext;a!==null;){if(a.context===r){if(i.tag===1){a=qe(-1,n&-n),a.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var v=f.pending;v===null?a.next=a:(a.next=v.next,v.next=a),f.pending=a}}i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),_i(i.return,n,t),o.lanes|=n;break}a=a.next}}else if(i.tag===10)s=i.type===t.type?null:i.child;else if(i.tag===18){if(s=i.return,s===null)throw Error(k(341));s.lanes|=n,o=s.alternate,o!==null&&(o.lanes|=n),_i(s,n,t),s=i.sibling}else s=i.child;if(s!==null)s.return=i;else for(s=i;s!==null;){if(s===t){s=null;break}if(i=s.sibling,i!==null){i.return=s.return,s=i;break}s=s.return}i=s}ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,on(t,n),l=Te(l),r=r(l),t.flags|=1,ue(e,t,r,n),t.child;case 14:return r=t.type,l=Fe(r,t.pendingProps),l=Fe(r.type,l),Oo(e,t,r,l,n);case 15:return $a(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Fe(r,l),Or(e,t),t.tag=1,ve(r)?(e=!0,Yr(t)):e=!1,on(t,n),Da(t,r,l),Ti(t,r,l,n),Ri(null,t,r,!0,e,n);case 19:return Va(e,t,n);case 22:return Ia(e,t,n)}throw Error(k(156,t.tag))};function lc(e,t){return Lu(e,t)}function Ld(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function _e(e,t,n,r){return new Ld(e,t,n,r)}function Os(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Rd(e){if(typeof e=="function")return Os(e)?1:0;if(e!=null){if(e=e.$$typeof,e===qi)return 11;if(e===bi)return 14}return 2}function xt(e,t){var n=e.alternate;return n===null?(n=_e(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Fr(e,t,n,r,l,i){var s=2;if(r=e,typeof e=="function")Os(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case Qt:return Dt(n.children,l,i,t);case Zi:s=8,l|=8;break;case bl:return e=_e(12,n,t,l|2),e.elementType=bl,e.lanes=i,e;case ei:return e=_e(13,n,t,l),e.elementType=ei,e.lanes=i,e;case ti:return e=_e(19,n,t,l),e.elementType=ti,e.lanes=i,e;case hu:return yl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case du:s=10;break e;case pu:s=9;break e;case qi:s=11;break e;case bi:s=14;break e;case ot:s=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return t=_e(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Dt(e,t,n,r){return e=_e(7,e,r,t),e.lanes=n,e}function yl(e,t,n,r){return e=_e(22,e,r,t),e.elementType=hu,e.lanes=n,e.stateNode={isHidden:!1},e}function Xl(e,t,n){return e=_e(6,e,null,t),e.lanes=n,e}function Gl(e,t,n){return t=_e(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Od(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Tl(0),this.expirationTimes=Tl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Tl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ds(e,t,n,r,l,i,s,o,a){return e=new Od(e,t,n,o,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=_e(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},gs(i),e}function Dd(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(uc)}catch(e){console.error(e)}}uc(),uu.exports=Ce;var Ud=uu.exports,ac,Yo=Ud;ac=Yo.createRoot,Yo.hydrateRoot;async function de(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function Ad(){return de("/api/archives")}async function Bd(e){return de(`/api/archives/${e}/entries`)}async function Vd(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),de(`/api/archives/${e}/entries/search?${r}`)}async function Hd(e,t){return de(`/api/archives/${e}/entries/${t}`)}async function Jl(e,t){return de(`/api/archives/${e}/entries/${t}/tags`)}async function Wd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tag_path:n})});if(!r.ok)throw new Error(`Failed to add tag (${r.status})`)}async function Qd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags/${n}`,{method:"DELETE"});if(!r.ok)throw new Error(`Remove failed (${r.status})`)}async function Xo(e){return de(`/api/archives/${e}/runs`)}async function Zl(e){return de(`/api/archives/${e}/tags`)}async function Kd(e,t){const n=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locator:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function Yd(e,t){return de(`/api/archives/${e}/capture_jobs/${t}`)}async function Xd(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function Gd(e,t){const n=await fetch("/api/auth/setup",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Setup failed");return n.json()}async function Jd(e,t){const n=await fetch("/api/auth/login",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Login failed");return n.json()}async function Zd(){await fetch("/api/auth/logout",{method:"POST"})}async function qd(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function bd(e){const t=await fetch("/api/auth/me",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({display_name:e})});if(!t.ok)throw new Error(await t.text())}async function ep(e,t){const n=await fetch("/api/auth/me",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({current_password:e,new_password:t})});if(!n.ok){let r=await n.text();try{r=JSON.parse(r).error??r}catch{}throw new Error(r)}}async function tp(){return de("/api/auth/tokens")}async function np(e){const t=await fetch("/api/auth/tokens",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e})});if(!t.ok)throw new Error(await t.text());return t.json()}async function rp(e){const t=await fetch(`/api/auth/tokens/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(await t.text())}async function lp(){return de("/api/admin/instance-settings")}async function ip(e){const t=await fetch("/api/admin/instance-settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok)throw new Error(await t.text())}async function sp(){return de("/api/admin/users")}async function op(e,t,n){const r=await fetch("/api/admin/users",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:t,email:n||void 0})});if(!r.ok){const l=await r.json().catch(()=>({}));throw new Error(l.error||`HTTP ${r.status}`)}return r.json()}async function up(e,t){const n=await fetch(`/api/admin/users/${e}/status`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function ap(){return de("/api/admin/roles")}async function cp(e,t){const n=await fetch("/api/admin/roles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:e,name:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function fp(e){return de(`/api/archives/${e}/collections`)}async function dp(e,t,n,r=2){const l=await fetch(`/api/archives/${e}/collections`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:t,slug:n,default_visibility_bits:r})});if(!l.ok){const i=await l.json().catch(()=>({error:l.statusText}));throw new Error(i.error||l.statusText)}return l.json()}async function pp(e,t){return de(`/api/archives/${e}/collections/${t}`)}async function hp(e,t){return de(`/api/archives/${e}/entries/${t}/collections`)}const mp=window.fetch;window.fetch=async(...e)=>{var n;const t=await mp(...e);return t.status===401&&((typeof e[0]=="string"?e[0]:((n=e[0])==null?void 0:n.url)??"").includes("/api/auth/")||window.dispatchEvent(new CustomEvent("auth:expired"))),t};function vp({onLogin:e}){const[t,n]=y.useState(""),[r,l]=y.useState(""),[i,s]=y.useState(null),[o,a]=y.useState(!1);async function f(v){v.preventDefault(),s(null),a(!0);try{const m=await Jd(t,r);e(m)}catch(m){s(m.message)}finally{a(!1)}}return u.jsxs("div",{className:"login-page",children:[u.jsx("h1",{children:"Archivr"}),u.jsxs("form",{onSubmit:f,children:[u.jsxs("label",{children:["Username",u.jsx("input",{type:"text",value:t,onChange:v=>n(v.target.value),autoFocus:!0,required:!0})]}),u.jsxs("label",{children:["Password",u.jsx("input",{type:"password",value:r,onChange:v=>l(v.target.value),required:!0})]}),i&&u.jsx("p",{className:"error",children:i}),u.jsx("button",{type:"submit",disabled:o,children:o?"Logging in…":"Log in"})]})]})}function gp({onComplete:e}){const[t,n]=y.useState(""),[r,l]=y.useState(""),[i,s]=y.useState(""),[o,a]=y.useState(null),[f,v]=y.useState(!1);async function m(h){if(h.preventDefault(),r!==i){a("Passwords do not match");return}if(r.length<8){a("Password must be at least 8 characters");return}a(null),v(!0);try{await Gd(t,r),e()}catch(w){a(w.message)}finally{v(!1)}}return u.jsxs("div",{className:"setup-page",children:[u.jsx("h1",{children:"Welcome to Archivr"}),u.jsx("p",{children:"Create your owner account to get started."}),u.jsxs("form",{onSubmit:m,children:[u.jsxs("label",{children:["Username",u.jsx("input",{type:"text",value:t,onChange:h=>n(h.target.value),autoFocus:!0,required:!0})]}),u.jsxs("label",{children:["Password",u.jsx("input",{type:"password",value:r,onChange:h=>l(h.target.value),required:!0})]}),u.jsxs("label",{children:["Confirm password",u.jsx("input",{type:"password",value:i,onChange:h=>s(h.target.value),required:!0})]}),o&&u.jsx("p",{className:"error",children:o}),u.jsx("button",{type:"submit",disabled:f,children:f?"Creating account…":"Create account"})]})]})}function yp({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:s,setCurrentUser:o}=y.useContext(Cl)??{},[a,f]=y.useState(!1);async function v(){f(!0),await Zd(),o(null),window.location.reload()}return u.jsxs("header",{className:"topbar",children:[u.jsx("div",{className:"brand",children:"Archivr"}),u.jsx("select",{className:"archive-switcher","aria-label":"Select archive",value:t??"",onChange:m=>n(m.target.value),children:e.map(m=>u.jsx("option",{value:m.id,children:m.label},m.id))}),u.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","runs","admin","tags","collections","settings"].map(m=>u.jsx("button",{className:`nav-link${r===m?" is-active":""}`,onClick:()=>l(m),children:m.charAt(0).toUpperCase()+m.slice(1)},m))}),u.jsx("button",{className:"capture-button",onClick:i,children:"+ Capture"}),s&&u.jsxs("div",{className:"user-menu",children:[u.jsx("span",{className:"username",children:s.display_name||s.username}),u.jsx("button",{className:"nav-link",onClick:()=>l("settings"),style:{color:"#d7cdbf",fontSize:13},children:"Settings"}),u.jsx("button",{onClick:v,disabled:a,className:"logout-btn",children:a?"Logging out…":"Log out"})]})]})}function wp({open:e,archiveId:t,onClose:n,onCaptured:r}){const l=y.useRef(null),[i,s]=y.useState(""),[o,a]=y.useState(null),[f,v]=y.useState(!1),[m,h]=y.useState(null),w=y.useRef(null);y.useEffect(()=>{const j=l.current;if(!j)return;const d=()=>{clearInterval(w.current),n()};return j.addEventListener("close",d),()=>j.removeEventListener("close",d)},[n]),y.useEffect(()=>{const j=l.current;j&&(e?(s(""),a(null),h(null),v(!1),clearInterval(w.current),j.open||j.showModal()):j.open&&j.close())},[e]);async function S(){if(!i.trim()){a("Enter a locator.");return}v(!0),a(null),h(null);try{const j=await Kd(t,i.trim());h("running"),w.current=setInterval(async()=>{var d;try{const c=await Yd(t,j.job_uid);c.status==="completed"?(clearInterval(w.current),w.current=null,v(!1),h("completed"),(d=l.current)==null||d.close(),r()):c.status==="failed"&&(clearInterval(w.current),w.current=null,v(!1),h("failed"),a(c.error_text||"Capture failed."))}catch(c){clearInterval(w.current),w.current=null,v(!1),a(c.message)}},500)}catch(j){a(j.message),v(!1)}}function x(){return f?m==="running"?"Running…":"Capturing…":"Capture"}return u.jsx("dialog",{ref:l,className:"capture-dialog",children:u.jsxs("div",{className:"capture-dialog-inner",children:[u.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),u.jsx("label",{htmlFor:"capture-locator",className:"capture-label",children:"Locator"}),u.jsx("input",{id:"capture-locator",className:"capture-input",type:"text",placeholder:"tweet:1234567890 or https://...",value:i,onChange:j=>s(j.target.value),onKeyDown:j=>{j.key==="Enter"&&S()},autoComplete:"off"}),o&&u.jsx("div",{className:"capture-error",children:o}),u.jsxs("div",{className:"capture-actions",children:[u.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var j;return(j=l.current)==null?void 0:j.close()},children:"Cancel"}),u.jsx("button",{type:"button",className:"capture-submit",onClick:S,disabled:f,children:x()})]})]})})}function cc(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&rString(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const Go={youtube:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function xp(e){return Go[e]??Go.other}function Sp({entry:e,archiveId:t,isSelected:n,onSelect:r}){const[l,i]=y.useState(!1),o=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!l?u.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>i(!0),style:{objectFit:"contain"}}):u.jsx("span",{dangerouslySetInnerHTML:{__html:xp(e.source_kind)}});return u.jsxs("div",{className:n?"is-selected":void 0,tabIndex:0,"data-entry-uid":e.entry_uid,onClick:r,onKeyDown:a=>{a.key==="Enter"&&r()},children:[u.jsx("div",{className:"col-added",children:fc(e.archived_at)}),u.jsxs("div",{className:"col-title",children:[u.jsx("span",{className:"source-icon",children:o}),u.jsx("span",{className:"entry-title",children:Rt(e.title)||Rt(e.entry_uid)})]}),u.jsx("div",{className:"col-type",children:u.jsx("span",{className:"type-pill",children:Rt(e.entity_kind)})}),u.jsx("div",{className:"col-size",children:cc(e.total_artifact_bytes)}),u.jsx("div",{className:"url-cell col-url",children:Rt(e.original_url)})]})}function kp({entries:e,selectedEntryUid:t,onSelectEntry:n,archiveId:r}){return u.jsx("section",{id:"archive-view",className:"view is-active",children:u.jsxs("div",{className:"entry-table",children:[u.jsxs("div",{className:"entry-header-row",children:[u.jsx("div",{className:"col-added",children:"Added"}),u.jsx("div",{className:"col-title",children:"Title"}),u.jsx("div",{className:"col-type",children:"Type"}),u.jsx("div",{className:"col-size",children:"Size"}),u.jsx("div",{className:"col-url",children:"Original URL"})]}),u.jsx("div",{id:"entries-body",children:e.map(l=>u.jsx(Sp,{entry:l,archiveId:r,isSelected:l.entry_uid===t,onSelect:()=>n(l)},l.entry_uid))})]})})}function Cp({runs:e}){return u.jsx("section",{id:"runs-view",className:"view is-active",children:u.jsxs("table",{className:"entry-table",children:[u.jsx("thead",{children:u.jsxs("tr",{children:[u.jsx("th",{children:"Started"}),u.jsx("th",{children:"Status"}),u.jsx("th",{children:"Requested"}),u.jsx("th",{children:"Completed"}),u.jsx("th",{children:"Failed"})]})}),u.jsx("tbody",{children:e.map((t,n)=>u.jsxs("tr",{children:[u.jsx("td",{children:t.started_at??""}),u.jsx("td",{children:t.status??""}),u.jsx("td",{children:t.requested_count??""}),u.jsx("td",{children:t.completed_count??""}),u.jsx("td",{children:t.failed_count??""})]},n))})]})})}const Np=4;function jp({archives:e}){const{currentUser:t}=y.useContext(Cl)??{},n=t&&(t.role_bits&Np)!==0,[r,l]=y.useState("users"),[i,s]=y.useState([]),[o,a]=y.useState([]),[f,v]=y.useState(!1),[m,h]=y.useState(null),[w,S]=y.useState(""),[x,j]=y.useState(""),[d,c]=y.useState(""),[p,g]=y.useState(null),[N,P]=y.useState(!1),[_,z]=y.useState(""),[L,O]=y.useState(""),[X,ye]=y.useState(null),[Le,Re]=y.useState(!1),lt=y.useCallback(async()=>{if(n){v(!0),h(null);try{const[C,R]=await Promise.all([sp(),ap()]);s(C),a(R)}catch(C){h(C.message)}finally{v(!1)}}},[n]);y.useEffect(()=>{lt()},[lt]);async function it(C){const R=C.status==="active"?"disabled":"active";try{await up(C.user_uid,R),s(I=>I.map(V=>V.user_uid===C.user_uid?{...V,status:R}:V))}catch(I){h(I.message)}}async function Et(C){if(C.preventDefault(),!w.trim()||!x){g("Username and password required");return}P(!0),g(null);try{await op(w.trim(),x,d.trim()||void 0),S(""),j(""),c(""),await lt()}catch(R){g(R.message)}finally{P(!1)}}async function E(C){if(C.preventDefault(),!_.trim()||!L.trim()){ye("Slug and name required");return}Re(!0),ye(null);try{await cp(_.trim(),L.trim()),z(""),O(""),await lt()}catch(R){ye(R.message)}finally{Re(!1)}}return n?u.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[u.jsx("h1",{children:"Admin"}),u.jsxs("div",{className:"admin-tabs",children:[u.jsx("button",{className:`admin-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),u.jsx("button",{className:`admin-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),u.jsx("button",{className:`admin-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),m&&u.jsx("div",{className:"capture-error",children:m}),r==="users"&&u.jsxs("div",{className:"admin-section",children:[u.jsx("h2",{children:"Users"}),f?u.jsx("p",{className:"muted",children:"Loading…"}):u.jsxs("table",{className:"admin-table",children:[u.jsx("thead",{children:u.jsxs("tr",{children:[u.jsx("th",{children:"Username"}),u.jsx("th",{children:"Email"}),u.jsx("th",{children:"Roles"}),u.jsx("th",{children:"Status"}),u.jsx("th",{children:"Actions"})]})}),u.jsx("tbody",{children:i.map(C=>u.jsxs("tr",{className:C.status==="disabled"?"admin-row-disabled":"",children:[u.jsx("td",{children:C.username}),u.jsx("td",{className:"muted",children:C.email||"—"}),u.jsx("td",{children:C.role_slugs.join(", ")||"—"}),u.jsx("td",{children:u.jsx("span",{className:`status-badge status-${C.status}`,children:C.status})}),u.jsx("td",{children:u.jsx("button",{className:"admin-action-btn",onClick:()=>it(C),children:C.status==="active"?"Ban":"Unban"})})]},C.user_uid))})]}),u.jsx("h3",{children:"Create User"}),u.jsxs("form",{className:"admin-form",onSubmit:Et,children:[u.jsx("input",{className:"admin-input",placeholder:"Username",value:w,onChange:C=>S(C.target.value),required:!0}),u.jsx("input",{className:"admin-input",type:"password",placeholder:"Password (min 8 chars)",value:x,onChange:C=>j(C.target.value),required:!0}),u.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:d,onChange:C=>c(C.target.value)}),p&&u.jsx("div",{className:"capture-error",children:p}),u.jsx("button",{className:"capture-submit",type:"submit",disabled:N,children:N?"Creating…":"Create User"})]})]}),r==="roles"&&u.jsxs("div",{className:"admin-section",children:[u.jsx("h2",{children:"Roles"}),f?u.jsx("p",{className:"muted",children:"Loading…"}):u.jsxs("table",{className:"admin-table",children:[u.jsx("thead",{children:u.jsxs("tr",{children:[u.jsx("th",{children:"Slug"}),u.jsx("th",{children:"Name"}),u.jsx("th",{children:"Level"}),u.jsx("th",{children:"Bit"}),u.jsx("th",{children:"Built-in"})]})}),u.jsx("tbody",{children:o.map(C=>u.jsxs("tr",{children:[u.jsx("td",{children:u.jsx("code",{children:C.slug})}),u.jsx("td",{children:C.name}),u.jsx("td",{children:C.level}),u.jsx("td",{children:C.bit_position}),u.jsx("td",{children:C.is_builtin?"✓":""})]},C.role_uid))})]}),u.jsx("h3",{children:"Create Custom Role"}),u.jsxs("form",{className:"admin-form",onSubmit:E,children:[u.jsx("input",{className:"admin-input",placeholder:"Slug (e.g. moderator)",value:_,onChange:C=>z(C.target.value),required:!0}),u.jsx("input",{className:"admin-input",placeholder:"Display Name (e.g. Moderator)",value:L,onChange:C=>O(C.target.value),required:!0}),X&&u.jsx("div",{className:"capture-error",children:X}),u.jsx("button",{className:"capture-submit",type:"submit",disabled:Le,children:Le?"Creating…":"Create Role"})]})]}),r==="archives"&&u.jsxs("div",{className:"admin-section",children:[u.jsx("h2",{children:"Mounted Archives"}),u.jsx("div",{className:"admin-list",children:e.map(C=>u.jsxs("div",{className:"admin-archive",children:[u.jsx("strong",{children:C.label}),u.jsx("div",{className:"muted",children:C.archive_path})]},C.id))})]})]}):u.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[u.jsx("h1",{children:"Admin"}),u.jsx("p",{className:"muted",children:"You need admin privileges to access this panel."}),u.jsx("h2",{children:"Mounted Archives"}),u.jsx("div",{className:"admin-list",children:e.map(C=>u.jsxs("div",{className:"admin-archive",children:[u.jsx("strong",{children:C.label}),u.jsx("div",{className:"muted",children:C.archive_path})]},C.id))})]})}function dc({node:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){var s;const l=t===e.tag.full_path;function i(){const o=l?null:e.tag.full_path;n(o),r("archive")}return u.jsxs("li",{children:[u.jsx("button",{className:`tag-node-btn${l?" is-active":""}`,title:e.tag.full_path,onClick:i,children:e.tag.name}),((s=e.children)==null?void 0:s.length)>0&&u.jsx("div",{className:"tag-children",children:u.jsx("ul",{className:"tag-tree-list",children:e.children.map(o=>u.jsx(dc,{node:o,tagFilter:t,onTagFilterSet:n,onViewChange:r},o.tag.tag_uid))})})]})}function Ep({tagNodes:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){return u.jsx("section",{id:"tags-view",className:"view is-active",children:u.jsx("div",{className:"tag-tree",children:e.length===0?u.jsx("div",{children:"No tags yet."}):u.jsx("ul",{className:"tag-tree-list",children:e.map(l=>u.jsx(dc,{node:l,tagFilter:t,onTagFilterSet:n,onViewChange:r},l.tag.tag_uid))})})})}const Jo={0:"Private",1:"Public",2:"Users only",3:"Public"};function _p({archiveId:e}){const[t,n]=y.useState([]),[r,l]=y.useState(!1),[i,s]=y.useState(null),[o,a]=y.useState(null),[f,v]=y.useState(null),[m,h]=y.useState(!1),[w,S]=y.useState(""),[x,j]=y.useState(""),[d,c]=y.useState(2),[p,g]=y.useState(!1),[N,P]=y.useState(null),_=y.useCallback(async()=>{if(e){l(!0),s(null);try{const L=await fp(e);n(L)}catch(L){s(L.message)}finally{l(!1)}}},[e]);y.useEffect(()=>{_()},[_]),y.useEffect(()=>{if(!o){v(null);return}h(!0),pp(e,o.collection_uid).then(L=>v(L)).catch(L=>s(L.message)).finally(()=>h(!1))},[o,e]);async function z(L){L.preventDefault();const O=w.trim(),X=x.trim();if(!(!O||!X)){g(!0),P(null);try{await dp(e,O,X,d),S(""),j(""),c(2),await _()}catch(ye){P(ye.message)}finally{g(!1)}}}return e?u.jsxs("div",{className:"collections-view",children:[u.jsx("h2",{className:"view-heading",children:"Collections"}),r&&u.jsx("div",{className:"muted",children:"Loading…"}),i&&u.jsx("div",{className:"error-text",children:i}),u.jsxs("div",{className:"collections-layout",children:[u.jsxs("div",{className:"collections-list",children:[t.map(L=>u.jsxs("div",{className:`collection-row${(o==null?void 0:o.collection_uid)===L.collection_uid?" is-selected":""}`,onClick:()=>a(L),children:[u.jsx("span",{className:"collection-name",children:L.name}),u.jsx("span",{className:"muted",style:{fontSize:"0.8em"},children:Jo[L.default_visibility_bits]??L.default_visibility_bits})]},L.collection_uid)),t.length===0&&!r&&u.jsx("div",{className:"muted",children:"No collections yet."})]}),o&&u.jsxs("div",{className:"collection-detail",children:[u.jsx("h3",{className:"collection-detail-name",children:o.name}),u.jsxs("div",{className:"muted",style:{marginBottom:"0.75rem"},children:["Default visibility: ",Jo[o.default_visibility_bits]??o.default_visibility_bits]}),m?u.jsx("div",{className:"muted",children:"Loading entries…"}):f?f.entries.length===0?u.jsx("div",{className:"muted",children:"No entries visible to you in this collection."}):u.jsx("ul",{className:"collection-entries-list",children:f.entries.map(L=>u.jsxs("li",{className:"collection-entry-item",children:[u.jsx("span",{className:"entry-title",children:L.title||L.entry_uid}),u.jsx("span",{className:"muted",style:{fontSize:"0.8em"},children:L.source_kind})]},L.entry_uid))}):null]})]}),u.jsxs("details",{className:"create-collection-form",style:{marginTop:"1.5rem"},children:[u.jsx("summary",{style:{cursor:"pointer",fontWeight:600},children:"Create collection"}),u.jsxs("form",{onSubmit:z,style:{marginTop:"0.75rem",display:"flex",flexDirection:"column",gap:"0.5rem",maxWidth:400},children:[u.jsxs("label",{children:["Name",u.jsx("input",{className:"form-input",type:"text",value:w,onChange:L=>{S(L.target.value),x||j(L.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),u.jsxs("label",{children:["Slug",u.jsx("input",{className:"form-input",type:"text",value:x,onChange:L=>j(L.target.value),placeholder:"my-collection",required:!0})]}),u.jsxs("label",{children:["Default visibility",u.jsxs("select",{className:"form-input",value:d,onChange:L=>c(Number(L.target.value)),children:[u.jsx("option",{value:0,children:"Private (admin/owner only)"}),u.jsx("option",{value:2,children:"Users only (logged in)"}),u.jsx("option",{value:3,children:"Public"})]})]}),N&&u.jsx("div",{className:"muted",style:{color:"var(--accent)"},children:N}),u.jsx("button",{className:"btn",type:"submit",disabled:p,children:p?"Creating…":"Create"})]})]})]}):u.jsx("div",{className:"view-placeholder",children:"Select an archive."})}const Pp=4;function Tp(){const{currentUser:e,setCurrentUser:t}=y.useContext(Cl)??{},n=e&&(e.role_bits&Pp)!==0,[r,l]=y.useState("profile");return u.jsxs("section",{className:"admin-view",children:[u.jsx("h1",{children:"Settings"}),u.jsx("div",{className:"admin-tabs",style:{display:"flex",gap:12,marginBottom:20},children:["profile","tokens",...n?["instance"]:[]].map(i=>u.jsx("button",{className:`nav-link${r===i?" is-active":""}`,style:{borderBottom:r===i?"2px solid var(--accent)":void 0,color:"var(--ink)"},onClick:()=>l(i),children:i==="profile"?"Profile":i==="tokens"?"API Tokens":"Instance"},i))}),r==="profile"&&u.jsx(zp,{currentUser:e,setCurrentUser:t}),r==="tokens"&&u.jsx(Lp,{}),r==="instance"&&n&&u.jsx(Rp,{})]})}function zp({currentUser:e,setCurrentUser:t}){const[n,r]=y.useState((e==null?void 0:e.display_name)??""),[l,i]=y.useState(!1),[s,o]=y.useState(null),[a,f]=y.useState(""),[v,m]=y.useState(""),[h,w]=y.useState(""),[S,x]=y.useState(!1),[j,d]=y.useState(null);async function c(g){g.preventDefault(),i(!0),o(null);try{await bd(n),t(N=>({...N,display_name:n||null})),o({ok:!0,text:"Saved."})}catch(N){o({ok:!1,text:N.message})}finally{i(!1)}}async function p(g){if(g.preventDefault(),v!==h){d({ok:!1,text:"Passwords do not match."});return}x(!0),d(null);try{await ep(a,v),f(""),m(""),w(""),d({ok:!0,text:"Password changed."})}catch(N){d({ok:!1,text:N.message})}finally{x(!1)}}return u.jsxs("div",{style:{maxWidth:480},children:[u.jsxs("div",{className:"admin-section",children:[u.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"Display Name"}),u.jsxs("form",{className:"admin-form",onSubmit:c,children:[u.jsx("input",{className:"admin-input",placeholder:(e==null?void 0:e.username)??"",value:n,onChange:g=>r(g.target.value)}),s&&u.jsx("div",{className:s.ok?"muted":"capture-error",children:s.text}),u.jsx("button",{className:"capture-submit",type:"submit",disabled:l,children:l?"Saving…":"Save"})]})]}),u.jsxs("div",{className:"admin-section",style:{marginTop:28},children:[u.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"Change Password"}),u.jsxs("form",{className:"admin-form",onSubmit:p,children:[u.jsx("input",{className:"admin-input",type:"password",placeholder:"Current password",value:a,onChange:g=>f(g.target.value),required:!0}),u.jsx("input",{className:"admin-input",type:"password",placeholder:"New password",value:v,onChange:g=>m(g.target.value),required:!0}),u.jsx("input",{className:"admin-input",type:"password",placeholder:"Confirm new password",value:h,onChange:g=>w(g.target.value),required:!0}),j&&u.jsx("div",{className:j.ok?"muted":"capture-error",children:j.text}),u.jsx("button",{className:"capture-submit",type:"submit",disabled:S,children:S?"Changing…":"Change Password"})]})]})]})}function Lp(){const[e,t]=y.useState([]),[n,r]=y.useState(!0),[l,i]=y.useState(null),[s,o]=y.useState(""),[a,f]=y.useState(!1),[v,m]=y.useState(null),h=y.useCallback(async()=>{r(!0),i(null);try{t(await tp())}catch(x){i(x.message)}finally{r(!1)}},[]);y.useEffect(()=>{h()},[h]);async function w(x){if(x.preventDefault(),!!s.trim()){f(!0);try{const j=await np(s.trim());m(j),o(""),h()}catch(j){i(j.message)}finally{f(!1)}}}async function S(x){try{await rp(x),t(j=>j.filter(d=>d.token_uid!==x))}catch(j){i(j.message)}}return u.jsxs("div",{style:{maxWidth:640},children:[u.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"API Tokens"}),v&&u.jsxs("div",{style:{background:"#e8f5e9",border:"1px solid #a5d6a7",padding:"12px 14px",marginBottom:16,fontSize:13},children:[u.jsx("strong",{children:"Token created."})," Copy it now \\u2014 it will not be shown again.",u.jsx("br",{}),u.jsx("code",{style:{wordBreak:"break-all",display:"block",marginTop:6,padding:"6px 8px",background:"#f5f5f5",border:"1px solid #ddd"},children:v.raw_token}),u.jsx("button",{style:{marginTop:8,fontSize:12,border:"1px solid #aaa",background:"none",cursor:"pointer"},onClick:()=>m(null),children:"Dismiss"})]}),u.jsxs("form",{className:"admin-form",onSubmit:w,style:{display:"flex",gap:8,marginBottom:16},children:[u.jsx("input",{className:"admin-input",placeholder:"Token name",value:s,onChange:x=>o(x.target.value),style:{flex:1},required:!0}),u.jsx("button",{className:"capture-submit",type:"submit",disabled:a,children:a?"Creating…":"Create"})]}),l&&u.jsx("div",{className:"capture-error",children:l}),n?u.jsx("div",{className:"muted",children:"Loading\\u2026"}):u.jsxs("div",{className:"admin-list",children:[e.length===0&&u.jsx("div",{className:"muted",children:"No tokens yet."}),e.map(x=>u.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",border:"1px solid var(--line)",background:"var(--paper-3)",padding:"10px 12px"},children:[u.jsxs("div",{children:[u.jsx("strong",{style:{fontSize:14},children:x.name}),u.jsxs("div",{className:"muted",style:{fontSize:12,marginTop:2},children:["Created ",x.created_at.slice(0,10),x.last_used_at&&` · Last used ${x.last_used_at.slice(0,10)}`]})]}),u.jsx("button",{onClick:()=>S(x.token_uid),style:{border:"1px solid var(--line)",background:"none",cursor:"pointer",color:"var(--accent)",fontSize:12,padding:"4px 10px"},children:"Revoke"})]},x.token_uid))]})]})}function Rp(){const[e,t]=y.useState(null),[n,r]=y.useState(!0),[l,i]=y.useState(null),[s,o]=y.useState(!1),[a,f]=y.useState(null);y.useEffect(()=>{(async()=>{try{t(await lp())}catch(m){i(m.message)}finally{r(!1)}})()},[]);async function v(m){m.preventDefault(),o(!0),f(null);try{await ip(e),f({ok:!0,text:"Saved."})}catch(h){f({ok:!1,text:h.message})}finally{o(!1)}}return n?u.jsx("div",{className:"muted",children:"Loading\\u2026"}):l?u.jsx("div",{className:"capture-error",children:l}):e?u.jsxs("div",{style:{maxWidth:480},children:[u.jsx("h2",{style:{fontSize:15,marginBottom:12},children:"Instance Settings"}),u.jsxs("form",{onSubmit:v,children:[u.jsxs("div",{className:"admin-section",children:[[["public_index_enabled","Public index (unauthenticated browsing)"],["public_entry_content_enabled","Public entry content"],["open_registration_enabled","Open registration"]].map(([m,h])=>u.jsxs("label",{style:{display:"flex",alignItems:"center",gap:10,marginBottom:12,cursor:"pointer"},children:[u.jsx("input",{type:"checkbox",checked:!!e[m],onChange:w=>t(S=>({...S,[m]:w.target.checked}))}),h]},m)),u.jsxs("div",{style:{marginBottom:12},children:[u.jsx("label",{style:{display:"block",fontWeight:600,marginBottom:4,fontSize:13},children:"Default entry visibility"}),u.jsxs("select",{className:"admin-input",value:e.default_entry_visibility,onChange:m=>t(h=>({...h,default_entry_visibility:Number(m.target.value)})),children:[u.jsx("option",{value:0,children:"Private (0)"}),u.jsx("option",{value:2,children:"Unlisted (2)"}),u.jsx("option",{value:3,children:"Public (3)"})]})]})]}),a&&u.jsx("div",{className:a.ok?"muted":"capture-error",children:a.text}),u.jsx("button",{className:"capture-submit",type:"submit",disabled:s,children:s?"Saving…":"Save Settings"})]})]}):null}function Op({archiveId:e,selectedEntry:t,onTagFilterSet:n,tagNodes:r,onTagsRefresh:l}){const[i,s]=y.useState(null),[o,a]=y.useState([]),[f,v]=y.useState(""),[m,h]=y.useState([]),[w,S]=y.useState(""),x=y.useRef(0);y.useEffect(()=>{if(!t||!e){s(null),a([]),h([]);return}const c=++x.current;s(null),a([]),Promise.all([Hd(e,t.entry_uid),Jl(e,t.entry_uid),hp(e,t.entry_uid)]).then(([p,g,N])=>{c===x.current&&(s(p),a(g),h(N))}).catch(()=>{})},[t,e]);async function j(){const c=f.trim();if(!(!c||!t))try{await Wd(e,t.entry_uid,c),v(""),S("");const p=await Jl(e,t.entry_uid);a(p),l()}catch(p){S(p.message)}}async function d(c){try{await Qd(e,t.entry_uid,c);const p=await Jl(e,t.entry_uid);a(p),l()}catch{}}return u.jsxs("aside",{className:"context-rail",children:[u.jsx("div",{className:"rail-title",children:"Context"}),t?i?u.jsxs("div",{className:"rail-body",children:[u.jsx("strong",{className:"rail-entry-title",children:Rt(i.summary.title)||Rt(i.summary.entry_uid)}),u.jsxs("div",{className:"rail-section",children:[i.summary.original_url&&u.jsxs("div",{className:"rail-item",children:[u.jsx("span",{className:"rail-label",children:"Original URL"}),":"," ",u.jsx("a",{href:i.summary.original_url,target:"_blank",rel:"noopener noreferrer",className:"rail-url-link",children:i.summary.original_url})]}),[["Added",fc(i.summary.archived_at)],["Source",i.summary.source_kind],["Type",i.summary.entity_kind],["Visibility",i.summary.visibility],["Structured root",i.structured_root_relpath]].map(([c,p])=>u.jsxs("div",{className:"rail-item",children:[u.jsx("span",{className:"rail-label",children:c}),": ",Rt(p)]},c))]}),i.artifacts.length>0?u.jsxs("div",{className:"rail-section",children:[u.jsxs("div",{className:"rail-section-heading",children:["Artifacts (",i.artifacts.length,")"]}),u.jsx("ul",{className:"artifact-list",children:i.artifacts.map((c,p)=>u.jsx("li",{children:u.jsxs("a",{href:`/api/archives/${e}/entries/${i.summary.entry_uid}/artifacts/${p}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[c.artifact_role.replace(/_/g," "),c.byte_size!=null?` (${cc(c.byte_size)})`:""]})},p))})]}):u.jsx("div",{className:"rail-item muted",children:"No artifacts."})]}):u.jsx("div",{className:"rail-body",children:"Loading…"}):u.jsx("div",{className:"rail-body",children:"Select an entry."}),t&&u.jsxs(u.Fragment,{children:[u.jsx("div",{className:"entry-tags",children:o.length===0?u.jsx("span",{className:"muted",children:"No tags."}):o.map(c=>u.jsxs("span",{className:"tag-pill",title:c.full_path,children:[c.name,u.jsx("button",{className:"remove-tag",title:`Remove tag ${c.full_path}`,onClick:()=>d(c.tag_uid),children:"×"})]},c.tag_uid))}),u.jsxs("div",{className:"assign-tag-form",children:[u.jsx("input",{className:"assign-tag-input",type:"text",placeholder:"/science/cs",autoComplete:"off",value:f,onChange:c=>v(c.target.value),onKeyDown:c=>{c.key==="Enter"&&j()}}),u.jsx("button",{className:"assign-tag-btn",onClick:j,children:"Add tag"}),w&&u.jsx("div",{className:"muted",style:{fontSize:"0.85em",color:"var(--accent)"},children:w})]}),t&&m.length>0&&u.jsxs("div",{className:"rail-section",children:[u.jsx("div",{className:"rail-section-heading",children:"Collections"}),m.map(c=>u.jsxs("div",{className:"rail-item",children:[u.jsx("span",{className:"rail-label",children:c.collection_uid}),":"," ",u.jsx("span",{className:"muted",children:{0:"Private",1:"Public",2:"Users only",3:"Public"}[c.visibility_bits]??`bits:${c.visibility_bits}`})]},c.collection_uid))]})]})]})}const Cl=y.createContext(null);function Dp(){const[e,t]=y.useState("loading"),[n,r]=y.useState(null);y.useEffect(()=>{(async()=>{if(await Xd()){t("setup");return}const Oe=await qd();if(!Oe){t("login");return}r(Oe),t("authenticated")})()},[]),y.useEffect(()=>{const M=()=>{r(null),t("login")};return window.addEventListener("auth:expired",M),()=>window.removeEventListener("auth:expired",M)},[]);const[l,i]=y.useState([]),[s,o]=y.useState(null),[a,f]=y.useState([]),[v,m]=y.useState(null),[h,w]=y.useState(null),[S,x]=y.useState(null),[j,d]=y.useState("archive"),[c,p]=y.useState(""),[g,N]=y.useState(""),[P,_]=y.useState(!1),[z,L]=y.useState([]),[O,X]=y.useState([]),[ye,Le]=y.useState(!1),Re=y.useCallback(async(M,Oe,Ve)=>{if(M){_(!0);try{let De;Oe||Ve?De=await Vd(M,Oe,Ve):De=await Bd(M),f(De),N(De.length===0?"No results":`${De.length} result${De.length===1?"":"s"}`)}catch{f([]),N("Search failed. Try again.")}finally{_(!1)}}},[]);y.useEffect(()=>{Ad().then(M=>{if(i(M),M.length>0){const Oe=M[0].id;o(Oe)}})},[]),y.useEffect(()=>{s&&(x(null),w(null),m(null),Promise.all([Re(s,"",null),Xo(s).then(L),Zl(s).then(X)]))},[s]),y.useEffect(()=>{if(s===null)return;const M=setTimeout(()=>{Re(s,c,S)},300);return()=>clearTimeout(M)},[c,s]),y.useEffect(()=>{s!==null&&(d("archive"),Re(s,c,S))},[S,s]);const lt=y.useCallback(M=>{o(M)},[]),it=y.useCallback(M=>{d(M),M==="tags"&&s&&Zl(s).then(X)},[s]),Et=y.useCallback(M=>{m(M?M.entry_uid:null),w(M)},[]),E=y.useCallback(M=>{x(M)},[]),C=y.useCallback(()=>{x(null)},[]),R=y.useCallback(()=>{s&&Zl(s).then(X)},[s]),I=y.useCallback(()=>{Le(!0)},[]),V=y.useCallback(()=>{Le(!1)},[]),Vt=y.useCallback(()=>{s&&Promise.all([Re(s,c,S),Xo(s).then(L)])},[s,c,S,Re]);return e==="loading"?u.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?u.jsx(gp,{onComplete:()=>t("login")}):e==="login"?u.jsx(vp,{onLogin:M=>{r(M),t("authenticated")}}):u.jsx(Cl.Provider,{value:{currentUser:n,setCurrentUser:r},children:u.jsxs(u.Fragment,{children:[u.jsx(yp,{archives:l,archiveId:s,onArchiveChange:lt,view:j,onViewChange:it,onCaptureClick:I}),u.jsxs("main",{className:"app-shell",children:[u.jsxs("div",{className:"workspace",children:[j==="archive"&&u.jsxs("div",{className:"search-row",children:[u.jsx("input",{className:"search-input",type:"search","aria-label":"Search archive","aria-busy":P,value:c,onChange:M=>p(M.target.value)}),u.jsxs("div",{className:"result-count",children:[g,S&&u.jsxs("button",{className:"tag-filter-badge",onClick:C,children:["× ",S]})]})]}),j==="archive"&&u.jsx(kp,{entries:a,selectedEntryUid:v,onSelectEntry:Et,archiveId:s,tagFilter:S,onClearTagFilter:C,searchQuery:c,onSearchChange:p,resultCount:g,searchBusy:P}),j==="runs"&&u.jsx(Cp,{runs:z}),j==="admin"&&u.jsx(jp,{archives:l}),j==="tags"&&u.jsx(Ep,{tagNodes:O,tagFilter:S,onTagFilterSet:E,onViewChange:it}),j==="collections"&&u.jsx(_p,{archiveId:s}),j==="settings"&&u.jsx(Tp,{})]}),u.jsx(Op,{archiveId:s,selectedEntry:h,onTagFilterSet:E,tagNodes:O,onTagsRefresh:R})]}),u.jsx(wp,{open:ye,archiveId:s,onClose:V,onCaptured:Vt})]})})}ac(document.getElementById("root")).render(u.jsx(y.StrictMode,{children:u.jsx(Dp,{})})); diff --git a/crates/archivr-server/static/assets/index-Davzo15o.js b/crates/archivr-server/static/assets/index-Davzo15o.js deleted file mode 100644 index cf5673d..0000000 --- a/crates/archivr-server/static/assets/index-Davzo15o.js +++ /dev/null @@ -1,40 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const u of i.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var Go={exports:{}},ul={},Zo={exports:{}},O={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var nr=Symbol.for("react.element"),dc=Symbol.for("react.portal"),pc=Symbol.for("react.fragment"),hc=Symbol.for("react.strict_mode"),mc=Symbol.for("react.profiler"),vc=Symbol.for("react.provider"),gc=Symbol.for("react.context"),yc=Symbol.for("react.forward_ref"),wc=Symbol.for("react.suspense"),Sc=Symbol.for("react.memo"),kc=Symbol.for("react.lazy"),Iu=Symbol.iterator;function xc(e){return e===null||typeof e!="object"?null:(e=Iu&&e[Iu]||e["@@iterator"],typeof e=="function"?e:null)}var Jo={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},qo=Object.assign,bo={};function hn(e,t,n){this.props=e,this.context=t,this.refs=bo,this.updater=n||Jo}hn.prototype.isReactComponent={};hn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};hn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function es(){}es.prototype=hn.prototype;function Hi(e,t,n){this.props=e,this.context=t,this.refs=bo,this.updater=n||Jo}var Wi=Hi.prototype=new es;Wi.constructor=Hi;qo(Wi,hn.prototype);Wi.isPureReactComponent=!0;var $u=Array.isArray,ts=Object.prototype.hasOwnProperty,Qi={current:null},ns={key:!0,ref:!0,__self:!0,__source:!0};function rs(e,t,n){var r,l={},i=null,u=null;if(t!=null)for(r in t.ref!==void 0&&(u=t.ref),t.key!==void 0&&(i=""+t.key),t)ts.call(t,r)&&!ns.hasOwnProperty(r)&&(l[r]=t[r]);var o=arguments.length-2;if(o===1)l.children=n;else if(1>>1,X=E[B];if(0>>1;Bl(ze,L))$el(Te,ze)?(E[B]=Te,E[$e]=L,B=$e):(E[B]=ze,E[F]=L,B=F);else if($el(Te,L))E[B]=Te,E[$e]=L,B=$e;else break e}}return T}function l(E,T){var L=E.sortIndex-T.sortIndex;return L!==0?L:E.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var u=Date,o=u.now();e.unstable_now=function(){return u.now()-o}}var s=[],f=[],v=1,m=null,p=3,w=!1,S=!1,k=!1,P=typeof setTimeout=="function"?setTimeout:null,c=typeof clearTimeout=="function"?clearTimeout:null,a=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function d(E){for(var T=n(f);T!==null;){if(T.callback===null)r(f);else if(T.startTime<=E)r(f),T.sortIndex=T.expirationTime,t(s,T);else break;T=n(f)}}function g(E){if(k=!1,d(E),!S)if(n(s)!==null)S=!0,Ut(x);else{var T=n(f);T!==null&&gn(g,T.startTime-E)}}function x(E,T){S=!1,k&&(k=!1,c(z),z=-1),w=!0;var L=p;try{for(d(T),m=n(s);m!==null&&(!(m.expirationTime>T)||E&&!oe());){var B=m.callback;if(typeof B=="function"){m.callback=null,p=m.priorityLevel;var X=B(m.expirationTime<=T);T=e.unstable_now(),typeof X=="function"?m.callback=X:m===n(s)&&r(s),d(T)}else r(s);m=n(s)}if(m!==null)var At=!0;else{var F=n(f);F!==null&&gn(g,F.startTime-T),At=!1}return At}finally{m=null,p=L,w=!1}}var _=!1,N=null,z=-1,A=5,R=-1;function oe(){return!(e.unstable_now()-RE||125B?(E.sortIndex=L,t(f,E),n(s)===null&&E===n(f)&&(k?(c(z),z=-1):k=!0,gn(g,L-B))):(E.sortIndex=X,t(s,E),S||w||(S=!0,Ut(x))),E},e.unstable_shouldYield=oe,e.unstable_wrapCallback=function(E){var T=p;return function(){var L=p;p=T;try{return E.apply(this,arguments)}finally{p=L}}}})(ss);os.exports=ss;var Oc=os.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Fc=j,Se=Oc;function y(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Jl=Object.prototype.hasOwnProperty,Mc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Au={},Vu={};function Dc(e){return Jl.call(Vu,e)?!0:Jl.call(Au,e)?!1:Mc.test(e)?Vu[e]=!0:(Au[e]=!0,!1)}function Ic(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function $c(e,t,n,r){if(t===null||typeof t>"u"||Ic(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function fe(e,t,n,r,l,i,u){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=u}var ne={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ne[e]=new fe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ne[t]=new fe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ne[e]=new fe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ne[e]=new fe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ne[e]=new fe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ne[e]=new fe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ne[e]=new fe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ne[e]=new fe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ne[e]=new fe(e,5,!1,e.toLowerCase(),null,!1,!1)});var Yi=/[\-:]([a-z])/g;function Xi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Yi,Xi);ne[t]=new fe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Yi,Xi);ne[t]=new fe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Yi,Xi);ne[t]=new fe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ne[e]=new fe(e,1,!1,e.toLowerCase(),null,!1,!1)});ne.xlinkHref=new fe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ne[e]=new fe(e,1,!1,e.toLowerCase(),null,!0,!0)});function Gi(e,t,n,r){var l=ne.hasOwnProperty(t)?ne[t]:null;(l!==null?l.type!==0:r||!(2o||l[u]!==i[o]){var s=` -`+l[u].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=u&&0<=o);break}}}finally{_l=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Nn(e):""}function Uc(e){switch(e.tag){case 5:return Nn(e.type);case 16:return Nn("Lazy");case 13:return Nn("Suspense");case 19:return Nn("SuspenseList");case 0:case 2:case 15:return e=Nl(e.type,!1),e;case 11:return e=Nl(e.type.render,!1),e;case 1:return e=Nl(e.type,!0),e;default:return""}}function ti(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ht:return"Fragment";case Bt:return"Portal";case ql:return"Profiler";case Zi:return"StrictMode";case bl:return"Suspense";case ei:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case fs:return(e.displayName||"Context")+".Consumer";case cs:return(e._context.displayName||"Context")+".Provider";case Ji:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case qi:return t=e.displayName||null,t!==null?t:ti(e.type)||"Memo";case rt:t=e._payload,e=e._init;try{return ti(e(t))}catch{}}return null}function Ac(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ti(t);case 8:return t===Zi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function gt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ps(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Vc(e){var t=ps(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(u){r=""+u,i.call(this,u)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(u){r=""+u},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ar(e){e._valueTracker||(e._valueTracker=Vc(e))}function hs(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ps(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Dr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ni(e,t){var n=t.checked;return Q({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Hu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=gt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ms(e,t){t=t.checked,t!=null&&Gi(e,"checked",t,!1)}function ri(e,t){ms(e,t);var n=gt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?li(e,t.type,n):t.hasOwnProperty("defaultValue")&&li(e,t.type,gt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Wu(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function li(e,t,n){(t!=="number"||Dr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var jn=Array.isArray;function en(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=cr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function An(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Tn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Bc=["Webkit","ms","Moz","O"];Object.keys(Tn).forEach(function(e){Bc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Tn[t]=Tn[e]})});function ws(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Tn.hasOwnProperty(e)&&Tn[e]?(""+t).trim():t+"px"}function Ss(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=ws(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Hc=Q({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function oi(e,t){if(t){if(Hc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(y(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(y(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(y(61))}if(t.style!=null&&typeof t.style!="object")throw Error(y(62))}}function si(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ai=null;function bi(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ci=null,tn=null,nn=null;function Yu(e){if(e=ir(e)){if(typeof ci!="function")throw Error(y(280));var t=e.stateNode;t&&(t=fl(t),ci(e.stateNode,e.type,t))}}function ks(e){tn?nn?nn.push(e):nn=[e]:tn=e}function xs(){if(tn){var e=tn,t=nn;if(nn=tn=null,Yu(e),t)for(e=0;e>>=0,e===0?32:31-(ef(e)/tf|0)|0}var fr=64,dr=4194304;function Pn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ar(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,u=n&268435455;if(u!==0){var o=u&~l;o!==0?r=Pn(o):(i&=u,i!==0&&(r=Pn(i)))}else u=n&~l,u!==0?r=Pn(u):i!==0&&(r=Pn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function rr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Me(t),e[t]=n}function uf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Rn),no=" ",ro=!1;function Bs(e,t){switch(e){case"keyup":return Ff.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Hs(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Wt=!1;function Df(e,t){switch(e){case"compositionend":return Hs(t);case"keypress":return t.which!==32?null:(ro=!0,no);case"textInput":return e=t.data,e===no&&ro?null:e;default:return null}}function If(e,t){if(Wt)return e==="compositionend"||!ou&&Bs(e,t)?(e=As(),jr=lu=ot=null,Wt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=oo(n)}}function Ys(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ys(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Xs(){for(var e=window,t=Dr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Dr(e.document)}return t}function su(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Kf(e){var t=Xs(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Ys(n.ownerDocument.documentElement,n)){if(r!==null&&su(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=so(n,i);var u=so(n,r);l&&u&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==u.node||e.focusOffset!==u.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(u.node,u.offset)):(t.setEnd(u.node,u.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Qt=null,vi=null,Fn=null,gi=!1;function ao(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;gi||Qt==null||Qt!==Dr(r)||(r=Qt,"selectionStart"in r&&su(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Fn&&Kn(Fn,r)||(Fn=r,r=Hr(vi,"onSelect"),0Xt||(e.current=Ei[Xt],Ei[Xt]=null,Xt--)}function I(e,t){Xt++,Ei[Xt]=e.current,e.current=t}var yt={},ue=St(yt),he=St(!1),Lt=yt;function sn(e,t){var n=e.type.contextTypes;if(!n)return yt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Qr(){U(he),U(ue)}function go(e,t,n){if(ue.current!==yt)throw Error(y(168));I(ue,t),I(he,n)}function ra(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(y(108,Ac(e)||"Unknown",l));return Q({},n,r)}function Kr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||yt,Lt=ue.current,I(ue,e),I(he,he.current),!0}function yo(e,t,n){var r=e.stateNode;if(!r)throw Error(y(169));n?(e=ra(e,t,Lt),r.__reactInternalMemoizedMergedChildContext=e,U(he),U(ue),I(ue,e)):U(he),I(he,n)}var Qe=null,dl=!1,Al=!1;function la(e){Qe===null?Qe=[e]:Qe.push(e)}function ld(e){dl=!0,la(e)}function kt(){if(!Al&&Qe!==null){Al=!0;var e=0,t=D;try{var n=Qe;for(D=1;e>=u,l-=u,Ke=1<<32-Me(t)+l|n<z?(A=N,N=null):A=N.sibling;var R=p(c,N,d[z],g);if(R===null){N===null&&(N=A);break}e&&N&&R.alternate===null&&t(c,N),a=i(R,a,z),_===null?x=R:_.sibling=R,_=R,N=A}if(z===d.length)return n(c,N),V&&Et(c,z),x;if(N===null){for(;zz?(A=N,N=null):A=N.sibling;var oe=p(c,N,R.value,g);if(oe===null){N===null&&(N=A);break}e&&N&&oe.alternate===null&&t(c,N),a=i(oe,a,z),_===null?x=oe:_.sibling=oe,_=oe,N=A}if(R.done)return n(c,N),V&&Et(c,z),x;if(N===null){for(;!R.done;z++,R=d.next())R=m(c,R.value,g),R!==null&&(a=i(R,a,z),_===null?x=R:_.sibling=R,_=R);return V&&Et(c,z),x}for(N=r(c,N);!R.done;z++,R=d.next())R=w(N,c,z,R.value,g),R!==null&&(e&&R.alternate!==null&&N.delete(R.key===null?z:R.key),a=i(R,a,z),_===null?x=R:_.sibling=R,_=R);return e&&N.forEach(function(xt){return t(c,xt)}),V&&Et(c,z),x}function P(c,a,d,g){if(typeof d=="object"&&d!==null&&d.type===Ht&&d.key===null&&(d=d.props.children),typeof d=="object"&&d!==null){switch(d.$$typeof){case sr:e:{for(var x=d.key,_=a;_!==null;){if(_.key===x){if(x=d.type,x===Ht){if(_.tag===7){n(c,_.sibling),a=l(_,d.props.children),a.return=c,c=a;break e}}else if(_.elementType===x||typeof x=="object"&&x!==null&&x.$$typeof===rt&&ko(x)===_.type){n(c,_.sibling),a=l(_,d.props),a.ref=En(c,_,d),a.return=c,c=a;break e}n(c,_);break}else t(c,_);_=_.sibling}d.type===Ht?(a=Tt(d.props.children,c.mode,g,d.key),a.return=c,c=a):(g=Mr(d.type,d.key,d.props,null,c.mode,g),g.ref=En(c,a,d),g.return=c,c=g)}return u(c);case Bt:e:{for(_=d.key;a!==null;){if(a.key===_)if(a.tag===4&&a.stateNode.containerInfo===d.containerInfo&&a.stateNode.implementation===d.implementation){n(c,a.sibling),a=l(a,d.children||[]),a.return=c,c=a;break e}else{n(c,a);break}else t(c,a);a=a.sibling}a=Xl(d,c.mode,g),a.return=c,c=a}return u(c);case rt:return _=d._init,P(c,a,_(d._payload),g)}if(jn(d))return S(c,a,d,g);if(yn(d))return k(c,a,d,g);wr(c,d)}return typeof d=="string"&&d!==""||typeof d=="number"?(d=""+d,a!==null&&a.tag===6?(n(c,a.sibling),a=l(a,d),a.return=c,c=a):(n(c,a),a=Yl(d,c.mode,g),a.return=c,c=a),u(c)):n(c,a)}return P}var cn=sa(!0),aa=sa(!1),Gr=St(null),Zr=null,Jt=null,du=null;function pu(){du=Jt=Zr=null}function hu(e){var t=Gr.current;U(Gr),e._currentValue=t}function Ni(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function ln(e,t){Zr=e,du=Jt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(pe=!0),e.firstContext=null)}function je(e){var t=e._currentValue;if(du!==e)if(e={context:e,memoizedValue:t,next:null},Jt===null){if(Zr===null)throw Error(y(308));Jt=e,Zr.dependencies={lanes:0,firstContext:e}}else Jt=Jt.next=e;return t}var Nt=null;function mu(e){Nt===null?Nt=[e]:Nt.push(e)}function ca(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,mu(t)):(n.next=l.next,l.next=n),t.interleaved=n,Je(e,r)}function Je(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var lt=!1;function vu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function fa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xe(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function pt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,M&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Je(e,n)}return l=r.interleaved,l===null?(t.next=t,mu(r)):(t.next=l.next,l.next=t),r.interleaved=t,Je(e,n)}function zr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,tu(e,n)}}function xo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var u={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=u:i=i.next=u,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Jr(e,t,n,r){var l=e.updateQueue;lt=!1;var i=l.firstBaseUpdate,u=l.lastBaseUpdate,o=l.shared.pending;if(o!==null){l.shared.pending=null;var s=o,f=s.next;s.next=null,u===null?i=f:u.next=f,u=s;var v=e.alternate;v!==null&&(v=v.updateQueue,o=v.lastBaseUpdate,o!==u&&(o===null?v.firstBaseUpdate=f:o.next=f,v.lastBaseUpdate=s))}if(i!==null){var m=l.baseState;u=0,v=f=s=null,o=i;do{var p=o.lane,w=o.eventTime;if((r&p)===p){v!==null&&(v=v.next={eventTime:w,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var S=e,k=o;switch(p=t,w=n,k.tag){case 1:if(S=k.payload,typeof S=="function"){m=S.call(w,m,p);break e}m=S;break e;case 3:S.flags=S.flags&-65537|128;case 0:if(S=k.payload,p=typeof S=="function"?S.call(w,m,p):S,p==null)break e;m=Q({},m,p);break e;case 2:lt=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,p=l.effects,p===null?l.effects=[o]:p.push(o))}else w={eventTime:w,lane:p,tag:o.tag,payload:o.payload,callback:o.callback,next:null},v===null?(f=v=w,s=m):v=v.next=w,u|=p;if(o=o.next,o===null){if(o=l.shared.pending,o===null)break;p=o,o=p.next,p.next=null,l.lastBaseUpdate=p,l.shared.pending=null}}while(!0);if(v===null&&(s=m),l.baseState=s,l.firstBaseUpdate=f,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do u|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Ft|=u,e.lanes=u,e.memoizedState=m}}function Eo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Bl.transition;Bl.transition={};try{e(!1),t()}finally{D=n,Bl.transition=r}}function Pa(){return Pe().memoizedState}function sd(e,t,n){var r=mt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},za(e))Ta(t,n);else if(n=ca(e,t,n,r),n!==null){var l=ae();De(n,e,r,l),La(n,t,r)}}function ad(e,t,n){var r=mt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(za(e))Ta(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var u=t.lastRenderedState,o=i(u,n);if(l.hasEagerState=!0,l.eagerState=o,Ie(o,u)){var s=t.interleaved;s===null?(l.next=l,mu(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=ca(e,t,l,r),n!==null&&(l=ae(),De(n,e,r,l),La(n,t,r))}}function za(e){var t=e.alternate;return e===W||t!==null&&t===W}function Ta(e,t){Mn=br=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function La(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,tu(e,n)}}var el={readContext:je,useCallback:re,useContext:re,useEffect:re,useImperativeHandle:re,useInsertionEffect:re,useLayoutEffect:re,useMemo:re,useReducer:re,useRef:re,useState:re,useDebugValue:re,useDeferredValue:re,useTransition:re,useMutableSource:re,useSyncExternalStore:re,useId:re,unstable_isNewReconciler:!1},cd={readContext:je,useCallback:function(e,t){return Ae().memoizedState=[e,t===void 0?null:t],e},useContext:je,useEffect:_o,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Lr(4194308,4,Ea.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Lr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Lr(4,2,e,t)},useMemo:function(e,t){var n=Ae();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ae();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=sd.bind(null,W,e),[r.memoizedState,e]},useRef:function(e){var t=Ae();return e={current:e},t.memoizedState=e},useState:Co,useDebugValue:Cu,useDeferredValue:function(e){return Ae().memoizedState=e},useTransition:function(){var e=Co(!1),t=e[0];return e=od.bind(null,e[1]),Ae().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=W,l=Ae();if(V){if(n===void 0)throw Error(y(407));n=n()}else{if(n=t(),b===null)throw Error(y(349));Ot&30||ma(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,_o(ga.bind(null,r,i,e),[e]),r.flags|=2048,er(9,va.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ae(),t=b.identifierPrefix;if(V){var n=Ye,r=Ke;n=(r&~(1<<32-Me(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=qn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=u.createElement(n,{is:r.is}):(e=u.createElement(n),n==="select"&&(u=e,r.multiple?u.multiple=!0:r.size&&(u.size=r.size))):e=u.createElementNS(e,n),e[Ve]=t,e[Gn]=r,Va(e,t,!1,!1),t.stateNode=e;e:{switch(u=si(n,r),n){case"dialog":$("cancel",e),$("close",e),l=r;break;case"iframe":case"object":case"embed":$("load",e),l=r;break;case"video":case"audio":for(l=0;lpn&&(t.flags|=128,r=!0,Cn(i,!1),t.lanes=4194304)}else{if(!r)if(e=qr(u),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Cn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!u.alternate&&!V)return le(t),null}else 2*Y()-i.renderingStartTime>pn&&n!==1073741824&&(t.flags|=128,r=!0,Cn(i,!1),t.lanes=4194304);i.isBackwards?(u.sibling=t.child,t.child=u):(n=i.last,n!==null?n.sibling=u:t.child=u,i.last=u)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=Y(),t.sibling=null,n=H.current,I(H,r?n&1|2:n&1),t):(le(t),null);case 22:case 23:return Tu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ge&1073741824&&(le(t),t.subtreeFlags&6&&(t.flags|=8192)):le(t),null;case 24:return null;case 25:return null}throw Error(y(156,t.tag))}function yd(e,t){switch(cu(t),t.tag){case 1:return me(t.type)&&Qr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return fn(),U(he),U(ue),wu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return yu(t),null;case 13:if(U(H),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(y(340));an()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return U(H),null;case 4:return fn(),null;case 10:return hu(t.type._context),null;case 22:case 23:return Tu(),null;case 24:return null;default:return null}}var kr=!1,ie=!1,wd=typeof WeakSet=="function"?WeakSet:Set,C=null;function qt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){K(e,t,r)}else n.current=null}function Mi(e,t,n){try{n()}catch(r){K(e,t,r)}}var Do=!1;function Sd(e,t){if(yi=Vr,e=Xs(),su(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var u=0,o=-1,s=-1,f=0,v=0,m=e,p=null;t:for(;;){for(var w;m!==n||l!==0&&m.nodeType!==3||(o=u+l),m!==i||r!==0&&m.nodeType!==3||(s=u+r),m.nodeType===3&&(u+=m.nodeValue.length),(w=m.firstChild)!==null;)p=m,m=w;for(;;){if(m===e)break t;if(p===n&&++f===l&&(o=u),p===i&&++v===r&&(s=u),(w=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=w}n=o===-1||s===-1?null:{start:o,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(wi={focusedElem:e,selectionRange:n},Vr=!1,C=t;C!==null;)if(t=C,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,C=e;else for(;C!==null;){t=C;try{var S=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var k=S.memoizedProps,P=S.memoizedState,c=t.stateNode,a=c.getSnapshotBeforeUpdate(t.elementType===t.type?k:Re(t.type,k),P);c.__reactInternalSnapshotBeforeUpdate=a}break;case 3:var d=t.stateNode.containerInfo;d.nodeType===1?d.textContent="":d.nodeType===9&&d.documentElement&&d.removeChild(d.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(y(163))}}catch(g){K(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,C=e;break}C=t.return}return S=Do,Do=!1,S}function Dn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Mi(t,n,i)}l=l.next}while(l!==r)}}function ml(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Di(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Wa(e){var t=e.alternate;t!==null&&(e.alternate=null,Wa(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ve],delete t[Gn],delete t[xi],delete t[nd],delete t[rd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Qa(e){return e.tag===5||e.tag===3||e.tag===4}function Io(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Qa(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ii(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Wr));else if(r!==4&&(e=e.child,e!==null))for(Ii(e,t,n),e=e.sibling;e!==null;)Ii(e,t,n),e=e.sibling}function $i(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for($i(e,t,n),e=e.sibling;e!==null;)$i(e,t,n),e=e.sibling}var ee=null,Oe=!1;function nt(e,t,n){for(n=n.child;n!==null;)Ka(e,t,n),n=n.sibling}function Ka(e,t,n){if(Be&&typeof Be.onCommitFiberUnmount=="function")try{Be.onCommitFiberUnmount(ol,n)}catch{}switch(n.tag){case 5:ie||qt(n,t);case 6:var r=ee,l=Oe;ee=null,nt(e,t,n),ee=r,Oe=l,ee!==null&&(Oe?(e=ee,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ee.removeChild(n.stateNode));break;case 18:ee!==null&&(Oe?(e=ee,n=n.stateNode,e.nodeType===8?Ul(e.parentNode,n):e.nodeType===1&&Ul(e,n),Wn(e)):Ul(ee,n.stateNode));break;case 4:r=ee,l=Oe,ee=n.stateNode.containerInfo,Oe=!0,nt(e,t,n),ee=r,Oe=l;break;case 0:case 11:case 14:case 15:if(!ie&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,u=i.destroy;i=i.tag,u!==void 0&&(i&2||i&4)&&Mi(n,t,u),l=l.next}while(l!==r)}nt(e,t,n);break;case 1:if(!ie&&(qt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){K(n,t,o)}nt(e,t,n);break;case 21:nt(e,t,n);break;case 22:n.mode&1?(ie=(r=ie)||n.memoizedState!==null,nt(e,t,n),ie=r):nt(e,t,n);break;default:nt(e,t,n)}}function $o(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new wd),t.forEach(function(r){var l=zd.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Le(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=u),r&=~i}if(r=l,r=Y()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*xd(r/1960))-r,10e?16:e,st===null)var r=!1;else{if(e=st,st=null,rl=0,M&6)throw Error(y(331));var l=M;for(M|=4,C=e.current;C!==null;){var i=C,u=i.child;if(C.flags&16){var o=i.deletions;if(o!==null){for(var s=0;sY()-Pu?zt(e,0):ju|=n),ve(e,t)}function ec(e,t){t===0&&(e.mode&1?(t=dr,dr<<=1,!(dr&130023424)&&(dr=4194304)):t=1);var n=ae();e=Je(e,t),e!==null&&(rr(e,t,n),ve(e,n))}function Pd(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),ec(e,n)}function zd(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(y(314))}r!==null&&r.delete(t),ec(e,n)}var tc;tc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||he.current)pe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return pe=!1,vd(e,t,n);pe=!!(e.flags&131072)}else pe=!1,V&&t.flags&1048576&&ia(t,Xr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Rr(e,t),e=t.pendingProps;var l=sn(t,ue.current);ln(t,n),l=ku(null,t,r,e,l,n);var i=xu();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,me(r)?(i=!0,Kr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,vu(t),l.updater=hl,t.stateNode=l,l._reactInternals=t,Pi(t,r,e,n),t=Li(null,t,r,!0,i,n)):(t.tag=0,V&&i&&au(t),se(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Rr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Ld(r),e=Re(r,e),l){case 0:t=Ti(null,t,r,e,n);break e;case 1:t=Oo(null,t,r,e,n);break e;case 11:t=Lo(null,t,r,e,n);break e;case 14:t=Ro(null,t,r,Re(r.type,e),n);break e}throw Error(y(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Ti(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Oo(e,t,r,l,n);case 3:e:{if($a(t),e===null)throw Error(y(387));r=t.pendingProps,i=t.memoizedState,l=i.element,fa(e,t),Jr(t,r,null,n);var u=t.memoizedState;if(r=u.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:u.cache,pendingSuspenseBoundaries:u.pendingSuspenseBoundaries,transitions:u.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=dn(Error(y(423)),t),t=Fo(e,t,r,n,l);break e}else if(r!==l){l=dn(Error(y(424)),t),t=Fo(e,t,r,n,l);break e}else for(ye=dt(t.stateNode.containerInfo.firstChild),we=t,V=!0,Fe=null,n=aa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(an(),r===l){t=qe(e,t,n);break e}se(e,t,r,n)}t=t.child}return t;case 5:return da(t),e===null&&_i(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,u=l.children,Si(r,l)?u=null:i!==null&&Si(r,i)&&(t.flags|=32),Ia(e,t),se(e,t,u,n),t.child;case 6:return e===null&&_i(t),null;case 13:return Ua(e,t,n);case 4:return gu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=cn(t,null,r,n):se(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Lo(e,t,r,l,n);case 7:return se(e,t,t.pendingProps,n),t.child;case 8:return se(e,t,t.pendingProps.children,n),t.child;case 12:return se(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,u=l.value,I(Gr,r._currentValue),r._currentValue=u,i!==null)if(Ie(i.value,u)){if(i.children===l.children&&!he.current){t=qe(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var o=i.dependencies;if(o!==null){u=i.child;for(var s=o.firstContext;s!==null;){if(s.context===r){if(i.tag===1){s=Xe(-1,n&-n),s.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var v=f.pending;v===null?s.next=s:(s.next=v.next,v.next=s),f.pending=s}}i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),Ni(i.return,n,t),o.lanes|=n;break}s=s.next}}else if(i.tag===10)u=i.type===t.type?null:i.child;else if(i.tag===18){if(u=i.return,u===null)throw Error(y(341));u.lanes|=n,o=u.alternate,o!==null&&(o.lanes|=n),Ni(u,n,t),u=i.sibling}else u=i.child;if(u!==null)u.return=i;else for(u=i;u!==null;){if(u===t){u=null;break}if(i=u.sibling,i!==null){i.return=u.return,u=i;break}u=u.return}i=u}se(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,ln(t,n),l=je(l),r=r(l),t.flags|=1,se(e,t,r,n),t.child;case 14:return r=t.type,l=Re(r,t.pendingProps),l=Re(r.type,l),Ro(e,t,r,l,n);case 15:return Ma(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Rr(e,t),t.tag=1,me(r)?(e=!0,Kr(t)):e=!1,ln(t,n),Ra(t,r,l),Pi(t,r,l,n),Li(null,t,r,!0,e,n);case 19:return Aa(e,t,n);case 22:return Da(e,t,n)}throw Error(y(156,t.tag))};function nc(e,t){return zs(e,t)}function Td(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function _e(e,t,n,r){return new Td(e,t,n,r)}function Ru(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Ld(e){if(typeof e=="function")return Ru(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ji)return 11;if(e===qi)return 14}return 2}function vt(e,t){var n=e.alternate;return n===null?(n=_e(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Mr(e,t,n,r,l,i){var u=2;if(r=e,typeof e=="function")Ru(e)&&(u=1);else if(typeof e=="string")u=5;else e:switch(e){case Ht:return Tt(n.children,l,i,t);case Zi:u=8,l|=8;break;case ql:return e=_e(12,n,t,l|2),e.elementType=ql,e.lanes=i,e;case bl:return e=_e(13,n,t,l),e.elementType=bl,e.lanes=i,e;case ei:return e=_e(19,n,t,l),e.elementType=ei,e.lanes=i,e;case ds:return gl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case cs:u=10;break e;case fs:u=9;break e;case Ji:u=11;break e;case qi:u=14;break e;case rt:u=16,r=null;break e}throw Error(y(130,e==null?e:typeof e,""))}return t=_e(u,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Tt(e,t,n,r){return e=_e(7,e,r,t),e.lanes=n,e}function gl(e,t,n,r){return e=_e(22,e,r,t),e.elementType=ds,e.lanes=n,e.stateNode={isHidden:!1},e}function Yl(e,t,n){return e=_e(6,e,null,t),e.lanes=n,e}function Xl(e,t,n){return t=_e(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Rd(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Pl(0),this.expirationTimes=Pl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Pl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ou(e,t,n,r,l,i,u,o,s){return e=new Rd(e,t,n,o,s),t===1?(t=1,i===!0&&(t|=8)):t=0,i=_e(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},vu(i),e}function Od(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(uc)}catch(e){console.error(e)}}uc(),us.exports=ke;var $d=us.exports,oc,Ko=$d;oc=Ko.createRoot,Ko.hydrateRoot;async function $t(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function Ud(){return $t("/api/archives")}async function Ad(e){return $t(`/api/archives/${e}/entries`)}async function Vd(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),$t(`/api/archives/${e}/entries/search?${r}`)}async function Bd(e,t){return $t(`/api/archives/${e}/entries/${t}`)}async function Gl(e,t){return $t(`/api/archives/${e}/entries/${t}/tags`)}async function Hd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tag_path:n})});if(!r.ok)throw new Error(`Failed to add tag (${r.status})`)}async function Wd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags/${n}`,{method:"DELETE"});if(!r.ok)throw new Error(`Remove failed (${r.status})`)}async function Yo(e){return $t(`/api/archives/${e}/runs`)}async function Zl(e){return $t(`/api/archives/${e}/tags`)}async function Qd(e,t){const n=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locator:t})});if(!n.ok){const r=await n.text();throw new Error(r||`HTTP ${n.status}`)}}async function Kd(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function Yd(e,t){const n=await fetch("/api/auth/setup",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Setup failed");return n.json()}async function Xd(e,t){const n=await fetch("/api/auth/login",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Login failed");return n.json()}async function Gd(){await fetch("/api/auth/logout",{method:"POST"})}async function Zd(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}const Jd=window.fetch;window.fetch=async(...e)=>{var n;const t=await Jd(...e);return t.status===401&&((typeof e[0]=="string"?e[0]:((n=e[0])==null?void 0:n.url)??"").includes("/api/auth/")||window.dispatchEvent(new CustomEvent("auth:expired"))),t};function qd({onLogin:e}){const[t,n]=j.useState(""),[r,l]=j.useState(""),[i,u]=j.useState(null),[o,s]=j.useState(!1);async function f(v){v.preventDefault(),u(null),s(!0);try{const m=await Xd(t,r);e(m)}catch(m){u(m.message)}finally{s(!1)}}return h.jsxs("div",{className:"login-page",children:[h.jsx("h1",{children:"Archivr"}),h.jsxs("form",{onSubmit:f,children:[h.jsxs("label",{children:["Username",h.jsx("input",{type:"text",value:t,onChange:v=>n(v.target.value),autoFocus:!0,required:!0})]}),h.jsxs("label",{children:["Password",h.jsx("input",{type:"password",value:r,onChange:v=>l(v.target.value),required:!0})]}),i&&h.jsx("p",{className:"error",children:i}),h.jsx("button",{type:"submit",disabled:o,children:o?"Logging in…":"Log in"})]})]})}function bd({onComplete:e}){const[t,n]=j.useState(""),[r,l]=j.useState(""),[i,u]=j.useState(""),[o,s]=j.useState(null),[f,v]=j.useState(!1);async function m(p){if(p.preventDefault(),r!==i){s("Passwords do not match");return}if(r.length<8){s("Password must be at least 8 characters");return}s(null),v(!0);try{await Yd(t,r),e()}catch(w){s(w.message)}finally{v(!1)}}return h.jsxs("div",{className:"setup-page",children:[h.jsx("h1",{children:"Welcome to Archivr"}),h.jsx("p",{children:"Create your owner account to get started."}),h.jsxs("form",{onSubmit:m,children:[h.jsxs("label",{children:["Username",h.jsx("input",{type:"text",value:t,onChange:p=>n(p.target.value),autoFocus:!0,required:!0})]}),h.jsxs("label",{children:["Password",h.jsx("input",{type:"password",value:r,onChange:p=>l(p.target.value),required:!0})]}),h.jsxs("label",{children:["Confirm password",h.jsx("input",{type:"password",value:i,onChange:p=>u(p.target.value),required:!0})]}),o&&h.jsx("p",{className:"error",children:o}),h.jsx("button",{type:"submit",disabled:f,children:f?"Creating account…":"Create account"})]})]})}function ep({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:u,setCurrentUser:o}=j.useContext(fc)??{},[s,f]=j.useState(!1);async function v(){f(!0),await Gd(),o(null),window.location.reload()}return h.jsxs("header",{className:"topbar",children:[h.jsx("div",{className:"brand",children:"Archivr"}),h.jsx("select",{className:"archive-switcher","aria-label":"Select archive",value:t??"",onChange:m=>n(m.target.value),children:e.map(m=>h.jsx("option",{value:m.id,children:m.label},m.id))}),h.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","runs","admin","tags"].map(m=>h.jsx("button",{className:`nav-link${r===m?" is-active":""}`,onClick:()=>l(m),children:m.charAt(0).toUpperCase()+m.slice(1)},m))}),h.jsx("button",{className:"capture-button",onClick:i,children:"+ Capture"}),u&&h.jsxs("div",{className:"user-menu",children:[h.jsx("span",{className:"username",children:u.username}),h.jsx("button",{onClick:v,disabled:s,className:"logout-btn",children:s?"Logging out…":"Log out"})]})]})}function tp({open:e,archiveId:t,onClose:n,onCaptured:r}){const l=j.useRef(null),[i,u]=j.useState(""),[o,s]=j.useState(null),[f,v]=j.useState(!1);j.useEffect(()=>{const p=l.current;if(!p)return;const w=()=>n();return p.addEventListener("close",w),()=>p.removeEventListener("close",w)},[n]),j.useEffect(()=>{const p=l.current;p&&(e?(u(""),s(null),p.open||p.showModal()):p.open&&p.close())},[e]);async function m(){var p;if(!i.trim()){s("Enter a locator.");return}v(!0),s(null);try{await Qd(t,i.trim()),(p=l.current)==null||p.close(),r()}catch(w){s(w.message)}finally{v(!1)}}return h.jsx("dialog",{ref:l,className:"capture-dialog",children:h.jsxs("div",{className:"capture-dialog-inner",children:[h.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),h.jsx("label",{htmlFor:"capture-locator",className:"capture-label",children:"Locator"}),h.jsx("input",{id:"capture-locator",className:"capture-input",type:"text",placeholder:"tweet:1234567890 or https://...",value:i,onChange:p=>u(p.target.value),onKeyDown:p=>{p.key==="Enter"&&m()},autoComplete:"off"}),o&&h.jsx("div",{className:"capture-error",children:o}),h.jsxs("div",{className:"capture-actions",children:[h.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var p;return(p=l.current)==null?void 0:p.close()},children:"Cancel"}),h.jsx("button",{type:"button",className:"capture-submit",onClick:m,disabled:f,children:f?"Capturing…":"Capture"})]})]})})}function sc(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&rString(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const Xo={youtube:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function np(e){return Xo[e]??Xo.other}function rp({entry:e,archiveId:t,isSelected:n,onSelect:r}){const[l,i]=j.useState(!1),o=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!l?h.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>i(!0),style:{objectFit:"contain"}}):h.jsx("span",{dangerouslySetInnerHTML:{__html:np(e.source_kind)}});return h.jsxs("div",{className:n?"is-selected":void 0,tabIndex:0,"data-entry-uid":e.entry_uid,onClick:r,onKeyDown:s=>{s.key==="Enter"&&r()},children:[h.jsx("div",{className:"col-added",children:ac(e.archived_at)}),h.jsxs("div",{className:"col-title",children:[h.jsx("span",{className:"source-icon",children:o}),h.jsx("span",{className:"entry-title",children:Pt(e.title)||Pt(e.entry_uid)})]}),h.jsx("div",{className:"col-type",children:h.jsx("span",{className:"type-pill",children:Pt(e.entity_kind)})}),h.jsx("div",{className:"col-size",children:sc(e.total_artifact_bytes)}),h.jsx("div",{className:"url-cell col-url",children:Pt(e.original_url)})]})}function lp({entries:e,selectedEntryUid:t,onSelectEntry:n,archiveId:r}){return h.jsx("section",{id:"archive-view",className:"view is-active",children:h.jsxs("div",{className:"entry-table",children:[h.jsxs("div",{className:"entry-header-row",children:[h.jsx("div",{className:"col-added",children:"Added"}),h.jsx("div",{className:"col-title",children:"Title"}),h.jsx("div",{className:"col-type",children:"Type"}),h.jsx("div",{className:"col-size",children:"Size"}),h.jsx("div",{className:"col-url",children:"Original URL"})]}),h.jsx("div",{id:"entries-body",children:e.map(l=>h.jsx(rp,{entry:l,archiveId:r,isSelected:l.entry_uid===t,onSelect:()=>n(l)},l.entry_uid))})]})})}function ip({runs:e}){return h.jsx("section",{id:"runs-view",className:"view is-active",children:h.jsxs("table",{className:"entry-table",children:[h.jsx("thead",{children:h.jsxs("tr",{children:[h.jsx("th",{children:"Started"}),h.jsx("th",{children:"Status"}),h.jsx("th",{children:"Requested"}),h.jsx("th",{children:"Completed"}),h.jsx("th",{children:"Failed"})]})}),h.jsx("tbody",{children:e.map((t,n)=>h.jsxs("tr",{children:[h.jsx("td",{children:t.started_at??""}),h.jsx("td",{children:t.status??""}),h.jsx("td",{children:t.requested_count??""}),h.jsx("td",{children:t.completed_count??""}),h.jsx("td",{children:t.failed_count??""})]},n))})]})})}function up({archives:e}){return h.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[h.jsx("h1",{children:"Mounted Archives"}),h.jsx("div",{className:"admin-list",children:e.map(t=>h.jsxs("div",{className:"admin-archive",children:[h.jsx("strong",{children:t.label}),h.jsx("div",{className:"muted",children:t.archive_path})]},t.id))})]})}function cc({node:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){var u;const l=t===e.tag.full_path;function i(){const o=l?null:e.tag.full_path;n(o),r("archive")}return h.jsxs("li",{children:[h.jsx("button",{className:`tag-node-btn${l?" is-active":""}`,title:e.tag.full_path,onClick:i,children:e.tag.name}),((u=e.children)==null?void 0:u.length)>0&&h.jsx("div",{className:"tag-children",children:h.jsx("ul",{className:"tag-tree-list",children:e.children.map(o=>h.jsx(cc,{node:o,tagFilter:t,onTagFilterSet:n,onViewChange:r},o.tag.tag_uid))})})]})}function op({tagNodes:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){return h.jsx("section",{id:"tags-view",className:"view is-active",children:h.jsx("div",{className:"tag-tree",children:e.length===0?h.jsx("div",{children:"No tags yet."}):h.jsx("ul",{className:"tag-tree-list",children:e.map(l=>h.jsx(cc,{node:l,tagFilter:t,onTagFilterSet:n,onViewChange:r},l.tag.tag_uid))})})})}function sp({archiveId:e,selectedEntry:t,onTagFilterSet:n,tagNodes:r,onTagsRefresh:l}){const[i,u]=j.useState(null),[o,s]=j.useState([]),[f,v]=j.useState(""),[m,p]=j.useState(""),w=j.useRef(0);j.useEffect(()=>{if(!t||!e){u(null),s([]);return}const P=++w.current;u(null),s([]),Promise.all([Bd(e,t.entry_uid),Gl(e,t.entry_uid)]).then(([c,a])=>{P===w.current&&(u(c),s(a))}).catch(()=>{})},[t,e]);async function S(){const P=f.trim();if(!(!P||!t))try{await Hd(e,t.entry_uid,P),v(""),p("");const c=await Gl(e,t.entry_uid);s(c),l()}catch(c){p(c.message)}}async function k(P){try{await Wd(e,t.entry_uid,P);const c=await Gl(e,t.entry_uid);s(c),l()}catch{}}return h.jsxs("aside",{className:"context-rail",children:[h.jsx("div",{className:"rail-title",children:"Context"}),t?i?h.jsxs("div",{className:"rail-body",children:[h.jsx("strong",{className:"rail-entry-title",children:Pt(i.summary.title)||Pt(i.summary.entry_uid)}),h.jsxs("div",{className:"rail-section",children:[i.summary.original_url&&h.jsxs("div",{className:"rail-item",children:[h.jsx("span",{className:"rail-label",children:"Original URL"}),":"," ",h.jsx("a",{href:i.summary.original_url,target:"_blank",rel:"noopener noreferrer",className:"rail-url-link",children:i.summary.original_url})]}),[["Added",ac(i.summary.archived_at)],["Source",i.summary.source_kind],["Type",i.summary.entity_kind],["Visibility",i.summary.visibility],["Structured root",i.structured_root_relpath]].map(([P,c])=>h.jsxs("div",{className:"rail-item",children:[h.jsx("span",{className:"rail-label",children:P}),": ",Pt(c)]},P))]}),i.artifacts.length>0?h.jsxs("div",{className:"rail-section",children:[h.jsxs("div",{className:"rail-section-heading",children:["Artifacts (",i.artifacts.length,")"]}),h.jsx("ul",{className:"artifact-list",children:i.artifacts.map((P,c)=>h.jsx("li",{children:h.jsxs("a",{href:`/api/archives/${e}/entries/${i.summary.entry_uid}/artifacts/${c}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[P.artifact_role.replace(/_/g," "),P.byte_size!=null?` (${sc(P.byte_size)})`:""]})},c))})]}):h.jsx("div",{className:"rail-item muted",children:"No artifacts."})]}):h.jsx("div",{className:"rail-body",children:"Loading…"}):h.jsx("div",{className:"rail-body",children:"Select an entry."}),t&&h.jsxs(h.Fragment,{children:[h.jsx("div",{className:"entry-tags",children:o.length===0?h.jsx("span",{className:"muted",children:"No tags."}):o.map(P=>h.jsxs("span",{className:"tag-pill",title:P.full_path,children:[P.name,h.jsx("button",{className:"remove-tag",title:`Remove tag ${P.full_path}`,onClick:()=>k(P.tag_uid),children:"×"})]},P.tag_uid))}),h.jsxs("div",{className:"assign-tag-form",children:[h.jsx("input",{className:"assign-tag-input",type:"text",placeholder:"/science/cs",autoComplete:"off",value:f,onChange:P=>v(P.target.value),onKeyDown:P=>{P.key==="Enter"&&S()}}),h.jsx("button",{className:"assign-tag-btn",onClick:S,children:"Add tag"}),m&&h.jsx("div",{className:"muted",style:{fontSize:"0.85em",color:"var(--accent)"},children:m})]})]})]})}const fc=j.createContext(null);function ap(){const[e,t]=j.useState("loading"),[n,r]=j.useState(null);j.useEffect(()=>{(async()=>{if(await Kd()){t("setup");return}const ze=await Zd();if(!ze){t("login");return}r(ze),t("authenticated")})()},[]),j.useEffect(()=>{const F=()=>{r(null),t("login")};return window.addEventListener("auth:expired",F),()=>window.removeEventListener("auth:expired",F)},[]);const[l,i]=j.useState([]),[u,o]=j.useState(null),[s,f]=j.useState([]),[v,m]=j.useState(null),[p,w]=j.useState(null),[S,k]=j.useState(null),[P,c]=j.useState("archive"),[a,d]=j.useState(""),[g,x]=j.useState(""),[_,N]=j.useState(!1),[z,A]=j.useState([]),[R,oe]=j.useState([]),[xt,et]=j.useState(!1),tt=j.useCallback(async(F,ze,$e)=>{if(F){N(!0);try{let Te;ze||$e?Te=await Vd(F,ze,$e):Te=await Ad(F),f(Te),x(Te.length===0?"No results":`${Te.length} result${Te.length===1?"":"s"}`)}catch{f([]),x("Search failed. Try again.")}finally{N(!1)}}},[]);j.useEffect(()=>{Ud().then(F=>{if(i(F),F.length>0){const ze=F[0].id;o(ze)}})},[]),j.useEffect(()=>{u&&(k(null),w(null),m(null),Promise.all([tt(u,"",null),Yo(u).then(A),Zl(u).then(oe)]))},[u]),j.useEffect(()=>{if(u===null)return;const F=setTimeout(()=>{tt(u,a,S)},300);return()=>clearTimeout(F)},[a,u]),j.useEffect(()=>{u!==null&&(c("archive"),tt(u,a,S))},[S,u]);const xl=j.useCallback(F=>{o(F)},[]),Ut=j.useCallback(F=>{c(F),F==="tags"&&u&&Zl(u).then(oe)},[u]),gn=j.useCallback(F=>{m(F?F.entry_uid:null),w(F)},[]),E=j.useCallback(F=>{k(F)},[]),T=j.useCallback(()=>{k(null)},[]),L=j.useCallback(()=>{u&&Zl(u).then(oe)},[u]),B=j.useCallback(()=>{et(!0)},[]),X=j.useCallback(()=>{et(!1)},[]),At=j.useCallback(()=>{u&&Promise.all([tt(u,a,S),Yo(u).then(A)])},[u,a,S,tt]);return e==="loading"?h.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?h.jsx(bd,{onComplete:()=>t("login")}):e==="login"?h.jsx(qd,{onLogin:F=>{r(F),t("authenticated")}}):h.jsx(fc.Provider,{value:{currentUser:n,setCurrentUser:r},children:h.jsxs(h.Fragment,{children:[h.jsx(ep,{archives:l,archiveId:u,onArchiveChange:xl,view:P,onViewChange:Ut,onCaptureClick:B}),h.jsxs("main",{className:"app-shell",children:[h.jsxs("div",{className:"workspace",children:[P==="archive"&&h.jsxs("div",{className:"search-row",children:[h.jsx("input",{className:"search-input",type:"search","aria-label":"Search archive","aria-busy":_,value:a,onChange:F=>d(F.target.value)}),h.jsxs("div",{className:"result-count",children:[g,S&&h.jsxs("button",{className:"tag-filter-badge",onClick:T,children:["× ",S]})]})]}),P==="archive"&&h.jsx(lp,{entries:s,selectedEntryUid:v,onSelectEntry:gn,archiveId:u,tagFilter:S,onClearTagFilter:T,searchQuery:a,onSearchChange:d,resultCount:g,searchBusy:_}),P==="runs"&&h.jsx(ip,{runs:z}),P==="admin"&&h.jsx(up,{archives:l}),P==="tags"&&h.jsx(op,{tagNodes:R,tagFilter:S,onTagFilterSet:E,onViewChange:Ut})]}),h.jsx(sp,{archiveId:u,selectedEntry:p,onTagFilterSet:E,tagNodes:R,onTagsRefresh:L})]}),h.jsx(tp,{open:xt,archiveId:u,onClose:X,onCaptured:At})]})})}oc(document.getElementById("root")).render(h.jsx(j.StrictMode,{children:h.jsx(ap,{})})); diff --git a/crates/archivr-server/static/assets/index-Dfnwsgsa.js b/crates/archivr-server/static/assets/index-Dfnwsgsa.js deleted file mode 100644 index 944e05f..0000000 --- a/crates/archivr-server/static/assets/index-Dfnwsgsa.js +++ /dev/null @@ -1,40 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const u of i.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var Go={exports:{}},ul={},Zo={exports:{}},O={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var nr=Symbol.for("react.element"),dc=Symbol.for("react.portal"),pc=Symbol.for("react.fragment"),hc=Symbol.for("react.strict_mode"),mc=Symbol.for("react.profiler"),vc=Symbol.for("react.provider"),gc=Symbol.for("react.context"),yc=Symbol.for("react.forward_ref"),wc=Symbol.for("react.suspense"),Sc=Symbol.for("react.memo"),kc=Symbol.for("react.lazy"),Iu=Symbol.iterator;function xc(e){return e===null||typeof e!="object"?null:(e=Iu&&e[Iu]||e["@@iterator"],typeof e=="function"?e:null)}var Jo={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},qo=Object.assign,bo={};function hn(e,t,n){this.props=e,this.context=t,this.refs=bo,this.updater=n||Jo}hn.prototype.isReactComponent={};hn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};hn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function es(){}es.prototype=hn.prototype;function Hi(e,t,n){this.props=e,this.context=t,this.refs=bo,this.updater=n||Jo}var Wi=Hi.prototype=new es;Wi.constructor=Hi;qo(Wi,hn.prototype);Wi.isPureReactComponent=!0;var $u=Array.isArray,ts=Object.prototype.hasOwnProperty,Qi={current:null},ns={key:!0,ref:!0,__self:!0,__source:!0};function rs(e,t,n){var r,l={},i=null,u=null;if(t!=null)for(r in t.ref!==void 0&&(u=t.ref),t.key!==void 0&&(i=""+t.key),t)ts.call(t,r)&&!ns.hasOwnProperty(r)&&(l[r]=t[r]);var o=arguments.length-2;if(o===1)l.children=n;else if(1>>1,X=C[B];if(0>>1;Bl(ze,L))$el(Te,ze)?(C[B]=Te,C[$e]=L,B=$e):(C[B]=ze,C[F]=L,B=F);else if($el(Te,L))C[B]=Te,C[$e]=L,B=$e;else break e}}return T}function l(C,T){var L=C.sortIndex-T.sortIndex;return L!==0?L:C.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var u=Date,o=u.now();e.unstable_now=function(){return u.now()-o}}var s=[],f=[],v=1,m=null,h=3,y=!1,S=!1,k=!1,x=typeof setTimeout=="function"?setTimeout:null,c=typeof clearTimeout=="function"?clearTimeout:null,a=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function d(C){for(var T=n(f);T!==null;){if(T.callback===null)r(f);else if(T.startTime<=C)r(f),T.sortIndex=T.expirationTime,t(s,T);else break;T=n(f)}}function g(C){if(k=!1,d(C),!S)if(n(s)!==null)S=!0,Ut(E);else{var T=n(f);T!==null&&gn(g,T.startTime-C)}}function E(C,T){S=!1,k&&(k=!1,c(z),z=-1),y=!0;var L=h;try{for(d(T),m=n(s);m!==null&&(!(m.expirationTime>T)||C&&!oe());){var B=m.callback;if(typeof B=="function"){m.callback=null,h=m.priorityLevel;var X=B(m.expirationTime<=T);T=e.unstable_now(),typeof X=="function"?m.callback=X:m===n(s)&&r(s),d(T)}else r(s);m=n(s)}if(m!==null)var At=!0;else{var F=n(f);F!==null&&gn(g,F.startTime-T),At=!1}return At}finally{m=null,h=L,y=!1}}var j=!1,P=null,z=-1,A=5,R=-1;function oe(){return!(e.unstable_now()-RC||125B?(C.sortIndex=L,t(f,C),n(s)===null&&C===n(f)&&(k?(c(z),z=-1):k=!0,gn(g,L-B))):(C.sortIndex=X,t(s,C),S||y||(S=!0,Ut(E))),C},e.unstable_shouldYield=oe,e.unstable_wrapCallback=function(C){var T=h;return function(){var L=h;h=T;try{return C.apply(this,arguments)}finally{h=L}}}})(ss);os.exports=ss;var Oc=os.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Fc=N,Se=Oc;function w(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Jl=Object.prototype.hasOwnProperty,Mc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Au={},Vu={};function Dc(e){return Jl.call(Vu,e)?!0:Jl.call(Au,e)?!1:Mc.test(e)?Vu[e]=!0:(Au[e]=!0,!1)}function Ic(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function $c(e,t,n,r){if(t===null||typeof t>"u"||Ic(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function fe(e,t,n,r,l,i,u){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=u}var ne={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ne[e]=new fe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ne[t]=new fe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ne[e]=new fe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ne[e]=new fe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ne[e]=new fe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ne[e]=new fe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ne[e]=new fe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ne[e]=new fe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ne[e]=new fe(e,5,!1,e.toLowerCase(),null,!1,!1)});var Yi=/[\-:]([a-z])/g;function Xi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Yi,Xi);ne[t]=new fe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Yi,Xi);ne[t]=new fe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Yi,Xi);ne[t]=new fe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ne[e]=new fe(e,1,!1,e.toLowerCase(),null,!1,!1)});ne.xlinkHref=new fe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ne[e]=new fe(e,1,!1,e.toLowerCase(),null,!0,!0)});function Gi(e,t,n,r){var l=ne.hasOwnProperty(t)?ne[t]:null;(l!==null?l.type!==0:r||!(2o||l[u]!==i[o]){var s=` -`+l[u].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=u&&0<=o);break}}}finally{_l=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Nn(e):""}function Uc(e){switch(e.tag){case 5:return Nn(e.type);case 16:return Nn("Lazy");case 13:return Nn("Suspense");case 19:return Nn("SuspenseList");case 0:case 2:case 15:return e=Nl(e.type,!1),e;case 11:return e=Nl(e.type.render,!1),e;case 1:return e=Nl(e.type,!0),e;default:return""}}function ti(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ht:return"Fragment";case Bt:return"Portal";case ql:return"Profiler";case Zi:return"StrictMode";case bl:return"Suspense";case ei:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case fs:return(e.displayName||"Context")+".Consumer";case cs:return(e._context.displayName||"Context")+".Provider";case Ji:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case qi:return t=e.displayName||null,t!==null?t:ti(e.type)||"Memo";case rt:t=e._payload,e=e._init;try{return ti(e(t))}catch{}}return null}function Ac(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ti(t);case 8:return t===Zi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function gt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ps(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Vc(e){var t=ps(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(u){r=""+u,i.call(this,u)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(u){r=""+u},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ar(e){e._valueTracker||(e._valueTracker=Vc(e))}function hs(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ps(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Dr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ni(e,t){var n=t.checked;return Q({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Hu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=gt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ms(e,t){t=t.checked,t!=null&&Gi(e,"checked",t,!1)}function ri(e,t){ms(e,t);var n=gt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?li(e,t.type,n):t.hasOwnProperty("defaultValue")&&li(e,t.type,gt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Wu(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function li(e,t,n){(t!=="number"||Dr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var jn=Array.isArray;function en(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=cr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function An(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Tn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Bc=["Webkit","ms","Moz","O"];Object.keys(Tn).forEach(function(e){Bc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Tn[t]=Tn[e]})});function ws(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Tn.hasOwnProperty(e)&&Tn[e]?(""+t).trim():t+"px"}function Ss(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=ws(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Hc=Q({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function oi(e,t){if(t){if(Hc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(w(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(w(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(w(61))}if(t.style!=null&&typeof t.style!="object")throw Error(w(62))}}function si(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ai=null;function bi(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ci=null,tn=null,nn=null;function Yu(e){if(e=ir(e)){if(typeof ci!="function")throw Error(w(280));var t=e.stateNode;t&&(t=fl(t),ci(e.stateNode,e.type,t))}}function ks(e){tn?nn?nn.push(e):nn=[e]:tn=e}function xs(){if(tn){var e=tn,t=nn;if(nn=tn=null,Yu(e),t)for(e=0;e>>=0,e===0?32:31-(ef(e)/tf|0)|0}var fr=64,dr=4194304;function Pn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ar(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,u=n&268435455;if(u!==0){var o=u&~l;o!==0?r=Pn(o):(i&=u,i!==0&&(r=Pn(i)))}else u=n&~l,u!==0?r=Pn(u):i!==0&&(r=Pn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function rr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Me(t),e[t]=n}function uf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Rn),no=" ",ro=!1;function Bs(e,t){switch(e){case"keyup":return Ff.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Hs(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Wt=!1;function Df(e,t){switch(e){case"compositionend":return Hs(t);case"keypress":return t.which!==32?null:(ro=!0,no);case"textInput":return e=t.data,e===no&&ro?null:e;default:return null}}function If(e,t){if(Wt)return e==="compositionend"||!ou&&Bs(e,t)?(e=As(),jr=lu=ot=null,Wt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=oo(n)}}function Ys(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ys(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Xs(){for(var e=window,t=Dr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Dr(e.document)}return t}function su(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Kf(e){var t=Xs(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Ys(n.ownerDocument.documentElement,n)){if(r!==null&&su(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=so(n,i);var u=so(n,r);l&&u&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==u.node||e.focusOffset!==u.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(u.node,u.offset)):(t.setEnd(u.node,u.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Qt=null,vi=null,Fn=null,gi=!1;function ao(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;gi||Qt==null||Qt!==Dr(r)||(r=Qt,"selectionStart"in r&&su(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Fn&&Kn(Fn,r)||(Fn=r,r=Hr(vi,"onSelect"),0Xt||(e.current=Ei[Xt],Ei[Xt]=null,Xt--)}function I(e,t){Xt++,Ei[Xt]=e.current,e.current=t}var yt={},ue=St(yt),he=St(!1),Rt=yt;function sn(e,t){var n=e.type.contextTypes;if(!n)return yt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Qr(){U(he),U(ue)}function go(e,t,n){if(ue.current!==yt)throw Error(w(168));I(ue,t),I(he,n)}function ra(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(w(108,Ac(e)||"Unknown",l));return Q({},n,r)}function Kr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||yt,Rt=ue.current,I(ue,e),I(he,he.current),!0}function yo(e,t,n){var r=e.stateNode;if(!r)throw Error(w(169));n?(e=ra(e,t,Rt),r.__reactInternalMemoizedMergedChildContext=e,U(he),U(ue),I(ue,e)):U(he),I(he,n)}var Qe=null,dl=!1,Al=!1;function la(e){Qe===null?Qe=[e]:Qe.push(e)}function ld(e){dl=!0,la(e)}function kt(){if(!Al&&Qe!==null){Al=!0;var e=0,t=D;try{var n=Qe;for(D=1;e>=u,l-=u,Ke=1<<32-Me(t)+l|n<z?(A=P,P=null):A=P.sibling;var R=h(c,P,d[z],g);if(R===null){P===null&&(P=A);break}e&&P&&R.alternate===null&&t(c,P),a=i(R,a,z),j===null?E=R:j.sibling=R,j=R,P=A}if(z===d.length)return n(c,P),V&&Ct(c,z),E;if(P===null){for(;zz?(A=P,P=null):A=P.sibling;var oe=h(c,P,R.value,g);if(oe===null){P===null&&(P=A);break}e&&P&&oe.alternate===null&&t(c,P),a=i(oe,a,z),j===null?E=oe:j.sibling=oe,j=oe,P=A}if(R.done)return n(c,P),V&&Ct(c,z),E;if(P===null){for(;!R.done;z++,R=d.next())R=m(c,R.value,g),R!==null&&(a=i(R,a,z),j===null?E=R:j.sibling=R,j=R);return V&&Ct(c,z),E}for(P=r(c,P);!R.done;z++,R=d.next())R=y(P,c,z,R.value,g),R!==null&&(e&&R.alternate!==null&&P.delete(R.key===null?z:R.key),a=i(R,a,z),j===null?E=R:j.sibling=R,j=R);return e&&P.forEach(function(Et){return t(c,Et)}),V&&Ct(c,z),E}function x(c,a,d,g){if(typeof d=="object"&&d!==null&&d.type===Ht&&d.key===null&&(d=d.props.children),typeof d=="object"&&d!==null){switch(d.$$typeof){case sr:e:{for(var E=d.key,j=a;j!==null;){if(j.key===E){if(E=d.type,E===Ht){if(j.tag===7){n(c,j.sibling),a=l(j,d.props.children),a.return=c,c=a;break e}}else if(j.elementType===E||typeof E=="object"&&E!==null&&E.$$typeof===rt&&ko(E)===j.type){n(c,j.sibling),a=l(j,d.props),a.ref=En(c,j,d),a.return=c,c=a;break e}n(c,j);break}else t(c,j);j=j.sibling}d.type===Ht?(a=Lt(d.props.children,c.mode,g,d.key),a.return=c,c=a):(g=Mr(d.type,d.key,d.props,null,c.mode,g),g.ref=En(c,a,d),g.return=c,c=g)}return u(c);case Bt:e:{for(j=d.key;a!==null;){if(a.key===j)if(a.tag===4&&a.stateNode.containerInfo===d.containerInfo&&a.stateNode.implementation===d.implementation){n(c,a.sibling),a=l(a,d.children||[]),a.return=c,c=a;break e}else{n(c,a);break}else t(c,a);a=a.sibling}a=Xl(d,c.mode,g),a.return=c,c=a}return u(c);case rt:return j=d._init,x(c,a,j(d._payload),g)}if(jn(d))return S(c,a,d,g);if(yn(d))return k(c,a,d,g);wr(c,d)}return typeof d=="string"&&d!==""||typeof d=="number"?(d=""+d,a!==null&&a.tag===6?(n(c,a.sibling),a=l(a,d),a.return=c,c=a):(n(c,a),a=Yl(d,c.mode,g),a.return=c,c=a),u(c)):n(c,a)}return x}var cn=sa(!0),aa=sa(!1),Gr=St(null),Zr=null,Jt=null,du=null;function pu(){du=Jt=Zr=null}function hu(e){var t=Gr.current;U(Gr),e._currentValue=t}function Ni(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function ln(e,t){Zr=e,du=Jt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(pe=!0),e.firstContext=null)}function je(e){var t=e._currentValue;if(du!==e)if(e={context:e,memoizedValue:t,next:null},Jt===null){if(Zr===null)throw Error(w(308));Jt=e,Zr.dependencies={lanes:0,firstContext:e}}else Jt=Jt.next=e;return t}var jt=null;function mu(e){jt===null?jt=[e]:jt.push(e)}function ca(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,mu(t)):(n.next=l.next,l.next=n),t.interleaved=n,Je(e,r)}function Je(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var lt=!1;function vu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function fa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xe(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function pt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,M&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Je(e,n)}return l=r.interleaved,l===null?(t.next=t,mu(r)):(t.next=l.next,l.next=t),r.interleaved=t,Je(e,n)}function zr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,tu(e,n)}}function xo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var u={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=u:i=i.next=u,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Jr(e,t,n,r){var l=e.updateQueue;lt=!1;var i=l.firstBaseUpdate,u=l.lastBaseUpdate,o=l.shared.pending;if(o!==null){l.shared.pending=null;var s=o,f=s.next;s.next=null,u===null?i=f:u.next=f,u=s;var v=e.alternate;v!==null&&(v=v.updateQueue,o=v.lastBaseUpdate,o!==u&&(o===null?v.firstBaseUpdate=f:o.next=f,v.lastBaseUpdate=s))}if(i!==null){var m=l.baseState;u=0,v=f=s=null,o=i;do{var h=o.lane,y=o.eventTime;if((r&h)===h){v!==null&&(v=v.next={eventTime:y,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var S=e,k=o;switch(h=t,y=n,k.tag){case 1:if(S=k.payload,typeof S=="function"){m=S.call(y,m,h);break e}m=S;break e;case 3:S.flags=S.flags&-65537|128;case 0:if(S=k.payload,h=typeof S=="function"?S.call(y,m,h):S,h==null)break e;m=Q({},m,h);break e;case 2:lt=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[o]:h.push(o))}else y={eventTime:y,lane:h,tag:o.tag,payload:o.payload,callback:o.callback,next:null},v===null?(f=v=y,s=m):v=v.next=y,u|=h;if(o=o.next,o===null){if(o=l.shared.pending,o===null)break;h=o,o=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(v===null&&(s=m),l.baseState=s,l.firstBaseUpdate=f,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do u|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);Mt|=u,e.lanes=u,e.memoizedState=m}}function Eo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Bl.transition;Bl.transition={};try{e(!1),t()}finally{D=n,Bl.transition=r}}function Pa(){return Pe().memoizedState}function sd(e,t,n){var r=mt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},za(e))Ta(t,n);else if(n=ca(e,t,n,r),n!==null){var l=ae();De(n,e,r,l),La(n,t,r)}}function ad(e,t,n){var r=mt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(za(e))Ta(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var u=t.lastRenderedState,o=i(u,n);if(l.hasEagerState=!0,l.eagerState=o,Ie(o,u)){var s=t.interleaved;s===null?(l.next=l,mu(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=ca(e,t,l,r),n!==null&&(l=ae(),De(n,e,r,l),La(n,t,r))}}function za(e){var t=e.alternate;return e===W||t!==null&&t===W}function Ta(e,t){Mn=br=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function La(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,tu(e,n)}}var el={readContext:je,useCallback:re,useContext:re,useEffect:re,useImperativeHandle:re,useInsertionEffect:re,useLayoutEffect:re,useMemo:re,useReducer:re,useRef:re,useState:re,useDebugValue:re,useDeferredValue:re,useTransition:re,useMutableSource:re,useSyncExternalStore:re,useId:re,unstable_isNewReconciler:!1},cd={readContext:je,useCallback:function(e,t){return Ae().memoizedState=[e,t===void 0?null:t],e},useContext:je,useEffect:_o,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Lr(4194308,4,Ea.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Lr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Lr(4,2,e,t)},useMemo:function(e,t){var n=Ae();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ae();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=sd.bind(null,W,e),[r.memoizedState,e]},useRef:function(e){var t=Ae();return e={current:e},t.memoizedState=e},useState:Co,useDebugValue:Cu,useDeferredValue:function(e){return Ae().memoizedState=e},useTransition:function(){var e=Co(!1),t=e[0];return e=od.bind(null,e[1]),Ae().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=W,l=Ae();if(V){if(n===void 0)throw Error(w(407));n=n()}else{if(n=t(),b===null)throw Error(w(349));Ft&30||ma(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,_o(ga.bind(null,r,i,e),[e]),r.flags|=2048,er(9,va.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ae(),t=b.identifierPrefix;if(V){var n=Ye,r=Ke;n=(r&~(1<<32-Me(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=qn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=u.createElement(n,{is:r.is}):(e=u.createElement(n),n==="select"&&(u=e,r.multiple?u.multiple=!0:r.size&&(u.size=r.size))):e=u.createElementNS(e,n),e[Ve]=t,e[Gn]=r,Va(e,t,!1,!1),t.stateNode=e;e:{switch(u=si(n,r),n){case"dialog":$("cancel",e),$("close",e),l=r;break;case"iframe":case"object":case"embed":$("load",e),l=r;break;case"video":case"audio":for(l=0;lpn&&(t.flags|=128,r=!0,Cn(i,!1),t.lanes=4194304)}else{if(!r)if(e=qr(u),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Cn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!u.alternate&&!V)return le(t),null}else 2*Y()-i.renderingStartTime>pn&&n!==1073741824&&(t.flags|=128,r=!0,Cn(i,!1),t.lanes=4194304);i.isBackwards?(u.sibling=t.child,t.child=u):(n=i.last,n!==null?n.sibling=u:t.child=u,i.last=u)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=Y(),t.sibling=null,n=H.current,I(H,r?n&1|2:n&1),t):(le(t),null);case 22:case 23:return Tu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ge&1073741824&&(le(t),t.subtreeFlags&6&&(t.flags|=8192)):le(t),null;case 24:return null;case 25:return null}throw Error(w(156,t.tag))}function yd(e,t){switch(cu(t),t.tag){case 1:return me(t.type)&&Qr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return fn(),U(he),U(ue),wu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return yu(t),null;case 13:if(U(H),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(w(340));an()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return U(H),null;case 4:return fn(),null;case 10:return hu(t.type._context),null;case 22:case 23:return Tu(),null;case 24:return null;default:return null}}var kr=!1,ie=!1,wd=typeof WeakSet=="function"?WeakSet:Set,_=null;function qt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){K(e,t,r)}else n.current=null}function Mi(e,t,n){try{n()}catch(r){K(e,t,r)}}var Do=!1;function Sd(e,t){if(yi=Vr,e=Xs(),su(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var u=0,o=-1,s=-1,f=0,v=0,m=e,h=null;t:for(;;){for(var y;m!==n||l!==0&&m.nodeType!==3||(o=u+l),m!==i||r!==0&&m.nodeType!==3||(s=u+r),m.nodeType===3&&(u+=m.nodeValue.length),(y=m.firstChild)!==null;)h=m,m=y;for(;;){if(m===e)break t;if(h===n&&++f===l&&(o=u),h===i&&++v===r&&(s=u),(y=m.nextSibling)!==null)break;m=h,h=m.parentNode}m=y}n=o===-1||s===-1?null:{start:o,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(wi={focusedElem:e,selectionRange:n},Vr=!1,_=t;_!==null;)if(t=_,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,_=e;else for(;_!==null;){t=_;try{var S=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var k=S.memoizedProps,x=S.memoizedState,c=t.stateNode,a=c.getSnapshotBeforeUpdate(t.elementType===t.type?k:Re(t.type,k),x);c.__reactInternalSnapshotBeforeUpdate=a}break;case 3:var d=t.stateNode.containerInfo;d.nodeType===1?d.textContent="":d.nodeType===9&&d.documentElement&&d.removeChild(d.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(w(163))}}catch(g){K(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,_=e;break}_=t.return}return S=Do,Do=!1,S}function Dn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Mi(t,n,i)}l=l.next}while(l!==r)}}function ml(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Di(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Wa(e){var t=e.alternate;t!==null&&(e.alternate=null,Wa(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ve],delete t[Gn],delete t[xi],delete t[nd],delete t[rd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Qa(e){return e.tag===5||e.tag===3||e.tag===4}function Io(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Qa(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ii(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Wr));else if(r!==4&&(e=e.child,e!==null))for(Ii(e,t,n),e=e.sibling;e!==null;)Ii(e,t,n),e=e.sibling}function $i(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for($i(e,t,n),e=e.sibling;e!==null;)$i(e,t,n),e=e.sibling}var ee=null,Oe=!1;function nt(e,t,n){for(n=n.child;n!==null;)Ka(e,t,n),n=n.sibling}function Ka(e,t,n){if(Be&&typeof Be.onCommitFiberUnmount=="function")try{Be.onCommitFiberUnmount(ol,n)}catch{}switch(n.tag){case 5:ie||qt(n,t);case 6:var r=ee,l=Oe;ee=null,nt(e,t,n),ee=r,Oe=l,ee!==null&&(Oe?(e=ee,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ee.removeChild(n.stateNode));break;case 18:ee!==null&&(Oe?(e=ee,n=n.stateNode,e.nodeType===8?Ul(e.parentNode,n):e.nodeType===1&&Ul(e,n),Wn(e)):Ul(ee,n.stateNode));break;case 4:r=ee,l=Oe,ee=n.stateNode.containerInfo,Oe=!0,nt(e,t,n),ee=r,Oe=l;break;case 0:case 11:case 14:case 15:if(!ie&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,u=i.destroy;i=i.tag,u!==void 0&&(i&2||i&4)&&Mi(n,t,u),l=l.next}while(l!==r)}nt(e,t,n);break;case 1:if(!ie&&(qt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){K(n,t,o)}nt(e,t,n);break;case 21:nt(e,t,n);break;case 22:n.mode&1?(ie=(r=ie)||n.memoizedState!==null,nt(e,t,n),ie=r):nt(e,t,n);break;default:nt(e,t,n)}}function $o(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new wd),t.forEach(function(r){var l=zd.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Le(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=u),r&=~i}if(r=l,r=Y()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*xd(r/1960))-r,10e?16:e,st===null)var r=!1;else{if(e=st,st=null,rl=0,M&6)throw Error(w(331));var l=M;for(M|=4,_=e.current;_!==null;){var i=_,u=i.child;if(_.flags&16){var o=i.deletions;if(o!==null){for(var s=0;sY()-Pu?Tt(e,0):ju|=n),ve(e,t)}function ec(e,t){t===0&&(e.mode&1?(t=dr,dr<<=1,!(dr&130023424)&&(dr=4194304)):t=1);var n=ae();e=Je(e,t),e!==null&&(rr(e,t,n),ve(e,n))}function Pd(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),ec(e,n)}function zd(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(w(314))}r!==null&&r.delete(t),ec(e,n)}var tc;tc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||he.current)pe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return pe=!1,vd(e,t,n);pe=!!(e.flags&131072)}else pe=!1,V&&t.flags&1048576&&ia(t,Xr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Rr(e,t),e=t.pendingProps;var l=sn(t,ue.current);ln(t,n),l=ku(null,t,r,e,l,n);var i=xu();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,me(r)?(i=!0,Kr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,vu(t),l.updater=hl,t.stateNode=l,l._reactInternals=t,Pi(t,r,e,n),t=Li(null,t,r,!0,i,n)):(t.tag=0,V&&i&&au(t),se(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Rr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Ld(r),e=Re(r,e),l){case 0:t=Ti(null,t,r,e,n);break e;case 1:t=Oo(null,t,r,e,n);break e;case 11:t=Lo(null,t,r,e,n);break e;case 14:t=Ro(null,t,r,Re(r.type,e),n);break e}throw Error(w(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Ti(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Oo(e,t,r,l,n);case 3:e:{if($a(t),e===null)throw Error(w(387));r=t.pendingProps,i=t.memoizedState,l=i.element,fa(e,t),Jr(t,r,null,n);var u=t.memoizedState;if(r=u.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:u.cache,pendingSuspenseBoundaries:u.pendingSuspenseBoundaries,transitions:u.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=dn(Error(w(423)),t),t=Fo(e,t,r,n,l);break e}else if(r!==l){l=dn(Error(w(424)),t),t=Fo(e,t,r,n,l);break e}else for(ye=dt(t.stateNode.containerInfo.firstChild),we=t,V=!0,Fe=null,n=aa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(an(),r===l){t=qe(e,t,n);break e}se(e,t,r,n)}t=t.child}return t;case 5:return da(t),e===null&&_i(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,u=l.children,Si(r,l)?u=null:i!==null&&Si(r,i)&&(t.flags|=32),Ia(e,t),se(e,t,u,n),t.child;case 6:return e===null&&_i(t),null;case 13:return Ua(e,t,n);case 4:return gu(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=cn(t,null,r,n):se(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Lo(e,t,r,l,n);case 7:return se(e,t,t.pendingProps,n),t.child;case 8:return se(e,t,t.pendingProps.children,n),t.child;case 12:return se(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,u=l.value,I(Gr,r._currentValue),r._currentValue=u,i!==null)if(Ie(i.value,u)){if(i.children===l.children&&!he.current){t=qe(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var o=i.dependencies;if(o!==null){u=i.child;for(var s=o.firstContext;s!==null;){if(s.context===r){if(i.tag===1){s=Xe(-1,n&-n),s.tag=2;var f=i.updateQueue;if(f!==null){f=f.shared;var v=f.pending;v===null?s.next=s:(s.next=v.next,v.next=s),f.pending=s}}i.lanes|=n,s=i.alternate,s!==null&&(s.lanes|=n),Ni(i.return,n,t),o.lanes|=n;break}s=s.next}}else if(i.tag===10)u=i.type===t.type?null:i.child;else if(i.tag===18){if(u=i.return,u===null)throw Error(w(341));u.lanes|=n,o=u.alternate,o!==null&&(o.lanes|=n),Ni(u,n,t),u=i.sibling}else u=i.child;if(u!==null)u.return=i;else for(u=i;u!==null;){if(u===t){u=null;break}if(i=u.sibling,i!==null){i.return=u.return,u=i;break}u=u.return}i=u}se(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,ln(t,n),l=je(l),r=r(l),t.flags|=1,se(e,t,r,n),t.child;case 14:return r=t.type,l=Re(r,t.pendingProps),l=Re(r.type,l),Ro(e,t,r,l,n);case 15:return Ma(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Rr(e,t),t.tag=1,me(r)?(e=!0,Kr(t)):e=!1,ln(t,n),Ra(t,r,l),Pi(t,r,l,n),Li(null,t,r,!0,e,n);case 19:return Aa(e,t,n);case 22:return Da(e,t,n)}throw Error(w(156,t.tag))};function nc(e,t){return zs(e,t)}function Td(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function _e(e,t,n,r){return new Td(e,t,n,r)}function Ru(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Ld(e){if(typeof e=="function")return Ru(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ji)return 11;if(e===qi)return 14}return 2}function vt(e,t){var n=e.alternate;return n===null?(n=_e(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Mr(e,t,n,r,l,i){var u=2;if(r=e,typeof e=="function")Ru(e)&&(u=1);else if(typeof e=="string")u=5;else e:switch(e){case Ht:return Lt(n.children,l,i,t);case Zi:u=8,l|=8;break;case ql:return e=_e(12,n,t,l|2),e.elementType=ql,e.lanes=i,e;case bl:return e=_e(13,n,t,l),e.elementType=bl,e.lanes=i,e;case ei:return e=_e(19,n,t,l),e.elementType=ei,e.lanes=i,e;case ds:return gl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case cs:u=10;break e;case fs:u=9;break e;case Ji:u=11;break e;case qi:u=14;break e;case rt:u=16,r=null;break e}throw Error(w(130,e==null?e:typeof e,""))}return t=_e(u,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Lt(e,t,n,r){return e=_e(7,e,r,t),e.lanes=n,e}function gl(e,t,n,r){return e=_e(22,e,r,t),e.elementType=ds,e.lanes=n,e.stateNode={isHidden:!1},e}function Yl(e,t,n){return e=_e(6,e,null,t),e.lanes=n,e}function Xl(e,t,n){return t=_e(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Rd(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Pl(0),this.expirationTimes=Pl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Pl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ou(e,t,n,r,l,i,u,o,s){return e=new Rd(e,t,n,o,s),t===1?(t=1,i===!0&&(t|=8)):t=0,i=_e(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},vu(i),e}function Od(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(uc)}catch(e){console.error(e)}}uc(),us.exports=ke;var $d=us.exports,oc,Ko=$d;oc=Ko.createRoot,Ko.hydrateRoot;async function xt(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function Ud(){return xt("/api/archives")}async function Ad(e){return xt(`/api/archives/${e}/entries`)}async function Vd(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),xt(`/api/archives/${e}/entries/search?${r}`)}async function Bd(e,t){return xt(`/api/archives/${e}/entries/${t}`)}async function Gl(e,t){return xt(`/api/archives/${e}/entries/${t}/tags`)}async function Hd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tag_path:n})});if(!r.ok)throw new Error(`Failed to add tag (${r.status})`)}async function Wd(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}/tags/${n}`,{method:"DELETE"});if(!r.ok)throw new Error(`Remove failed (${r.status})`)}async function Yo(e){return xt(`/api/archives/${e}/runs`)}async function Zl(e){return xt(`/api/archives/${e}/tags`)}async function Qd(e,t){const n=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locator:t})});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`HTTP ${n.status}`)}return n.json()}async function Kd(e,t){return xt(`/api/archives/${e}/capture_jobs/${t}`)}async function Yd(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function Xd(e,t){const n=await fetch("/api/auth/setup",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Setup failed");return n.json()}async function Gd(e,t){const n=await fetch("/api/auth/login",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({username:e,password:t})});if(!n.ok)throw new Error((await n.json()).error||"Login failed");return n.json()}async function Zd(){await fetch("/api/auth/logout",{method:"POST"})}async function Jd(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}const qd=window.fetch;window.fetch=async(...e)=>{var n;const t=await qd(...e);return t.status===401&&((typeof e[0]=="string"?e[0]:((n=e[0])==null?void 0:n.url)??"").includes("/api/auth/")||window.dispatchEvent(new CustomEvent("auth:expired"))),t};function bd({onLogin:e}){const[t,n]=N.useState(""),[r,l]=N.useState(""),[i,u]=N.useState(null),[o,s]=N.useState(!1);async function f(v){v.preventDefault(),u(null),s(!0);try{const m=await Gd(t,r);e(m)}catch(m){u(m.message)}finally{s(!1)}}return p.jsxs("div",{className:"login-page",children:[p.jsx("h1",{children:"Archivr"}),p.jsxs("form",{onSubmit:f,children:[p.jsxs("label",{children:["Username",p.jsx("input",{type:"text",value:t,onChange:v=>n(v.target.value),autoFocus:!0,required:!0})]}),p.jsxs("label",{children:["Password",p.jsx("input",{type:"password",value:r,onChange:v=>l(v.target.value),required:!0})]}),i&&p.jsx("p",{className:"error",children:i}),p.jsx("button",{type:"submit",disabled:o,children:o?"Logging in…":"Log in"})]})]})}function ep({onComplete:e}){const[t,n]=N.useState(""),[r,l]=N.useState(""),[i,u]=N.useState(""),[o,s]=N.useState(null),[f,v]=N.useState(!1);async function m(h){if(h.preventDefault(),r!==i){s("Passwords do not match");return}if(r.length<8){s("Password must be at least 8 characters");return}s(null),v(!0);try{await Xd(t,r),e()}catch(y){s(y.message)}finally{v(!1)}}return p.jsxs("div",{className:"setup-page",children:[p.jsx("h1",{children:"Welcome to Archivr"}),p.jsx("p",{children:"Create your owner account to get started."}),p.jsxs("form",{onSubmit:m,children:[p.jsxs("label",{children:["Username",p.jsx("input",{type:"text",value:t,onChange:h=>n(h.target.value),autoFocus:!0,required:!0})]}),p.jsxs("label",{children:["Password",p.jsx("input",{type:"password",value:r,onChange:h=>l(h.target.value),required:!0})]}),p.jsxs("label",{children:["Confirm password",p.jsx("input",{type:"password",value:i,onChange:h=>u(h.target.value),required:!0})]}),o&&p.jsx("p",{className:"error",children:o}),p.jsx("button",{type:"submit",disabled:f,children:f?"Creating account…":"Create account"})]})]})}function tp({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:u,setCurrentUser:o}=N.useContext(fc)??{},[s,f]=N.useState(!1);async function v(){f(!0),await Zd(),o(null),window.location.reload()}return p.jsxs("header",{className:"topbar",children:[p.jsx("div",{className:"brand",children:"Archivr"}),p.jsx("select",{className:"archive-switcher","aria-label":"Select archive",value:t??"",onChange:m=>n(m.target.value),children:e.map(m=>p.jsx("option",{value:m.id,children:m.label},m.id))}),p.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","runs","admin","tags"].map(m=>p.jsx("button",{className:`nav-link${r===m?" is-active":""}`,onClick:()=>l(m),children:m.charAt(0).toUpperCase()+m.slice(1)},m))}),p.jsx("button",{className:"capture-button",onClick:i,children:"+ Capture"}),u&&p.jsxs("div",{className:"user-menu",children:[p.jsx("span",{className:"username",children:u.username}),p.jsx("button",{onClick:v,disabled:s,className:"logout-btn",children:s?"Logging out…":"Log out"})]})]})}function np({open:e,archiveId:t,onClose:n,onCaptured:r}){const l=N.useRef(null),[i,u]=N.useState(""),[o,s]=N.useState(null),[f,v]=N.useState(!1),[m,h]=N.useState(null),y=N.useRef(null);N.useEffect(()=>{const x=l.current;if(!x)return;const c=()=>{clearInterval(y.current),n()};return x.addEventListener("close",c),()=>x.removeEventListener("close",c)},[n]),N.useEffect(()=>{const x=l.current;x&&(e?(u(""),s(null),h(null),v(!1),clearInterval(y.current),x.open||x.showModal()):x.open&&x.close())},[e]);async function S(){if(!i.trim()){s("Enter a locator.");return}v(!0),s(null),h(null);try{const x=await Qd(t,i.trim());h("running"),y.current=setInterval(async()=>{var c;try{const a=await Kd(t,x.job_uid);a.status==="completed"?(clearInterval(y.current),y.current=null,v(!1),h("completed"),(c=l.current)==null||c.close(),r()):a.status==="failed"&&(clearInterval(y.current),y.current=null,v(!1),h("failed"),s(a.error_text||"Capture failed."))}catch(a){clearInterval(y.current),y.current=null,v(!1),s(a.message)}},500)}catch(x){s(x.message),v(!1)}}function k(){return f?m==="running"?"Running…":"Capturing…":"Capture"}return p.jsx("dialog",{ref:l,className:"capture-dialog",children:p.jsxs("div",{className:"capture-dialog-inner",children:[p.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),p.jsx("label",{htmlFor:"capture-locator",className:"capture-label",children:"Locator"}),p.jsx("input",{id:"capture-locator",className:"capture-input",type:"text",placeholder:"tweet:1234567890 or https://...",value:i,onChange:x=>u(x.target.value),onKeyDown:x=>{x.key==="Enter"&&S()},autoComplete:"off"}),o&&p.jsx("div",{className:"capture-error",children:o}),p.jsxs("div",{className:"capture-actions",children:[p.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var x;return(x=l.current)==null?void 0:x.close()},children:"Cancel"}),p.jsx("button",{type:"button",className:"capture-submit",onClick:S,disabled:f,children:k()})]})]})})}function sc(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&rString(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const Xo={youtube:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function rp(e){return Xo[e]??Xo.other}function lp({entry:e,archiveId:t,isSelected:n,onSelect:r}){const[l,i]=N.useState(!1),o=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!l?p.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>i(!0),style:{objectFit:"contain"}}):p.jsx("span",{dangerouslySetInnerHTML:{__html:rp(e.source_kind)}});return p.jsxs("div",{className:n?"is-selected":void 0,tabIndex:0,"data-entry-uid":e.entry_uid,onClick:r,onKeyDown:s=>{s.key==="Enter"&&r()},children:[p.jsx("div",{className:"col-added",children:ac(e.archived_at)}),p.jsxs("div",{className:"col-title",children:[p.jsx("span",{className:"source-icon",children:o}),p.jsx("span",{className:"entry-title",children:zt(e.title)||zt(e.entry_uid)})]}),p.jsx("div",{className:"col-type",children:p.jsx("span",{className:"type-pill",children:zt(e.entity_kind)})}),p.jsx("div",{className:"col-size",children:sc(e.total_artifact_bytes)}),p.jsx("div",{className:"url-cell col-url",children:zt(e.original_url)})]})}function ip({entries:e,selectedEntryUid:t,onSelectEntry:n,archiveId:r}){return p.jsx("section",{id:"archive-view",className:"view is-active",children:p.jsxs("div",{className:"entry-table",children:[p.jsxs("div",{className:"entry-header-row",children:[p.jsx("div",{className:"col-added",children:"Added"}),p.jsx("div",{className:"col-title",children:"Title"}),p.jsx("div",{className:"col-type",children:"Type"}),p.jsx("div",{className:"col-size",children:"Size"}),p.jsx("div",{className:"col-url",children:"Original URL"})]}),p.jsx("div",{id:"entries-body",children:e.map(l=>p.jsx(lp,{entry:l,archiveId:r,isSelected:l.entry_uid===t,onSelect:()=>n(l)},l.entry_uid))})]})})}function up({runs:e}){return p.jsx("section",{id:"runs-view",className:"view is-active",children:p.jsxs("table",{className:"entry-table",children:[p.jsx("thead",{children:p.jsxs("tr",{children:[p.jsx("th",{children:"Started"}),p.jsx("th",{children:"Status"}),p.jsx("th",{children:"Requested"}),p.jsx("th",{children:"Completed"}),p.jsx("th",{children:"Failed"})]})}),p.jsx("tbody",{children:e.map((t,n)=>p.jsxs("tr",{children:[p.jsx("td",{children:t.started_at??""}),p.jsx("td",{children:t.status??""}),p.jsx("td",{children:t.requested_count??""}),p.jsx("td",{children:t.completed_count??""}),p.jsx("td",{children:t.failed_count??""})]},n))})]})})}function op({archives:e}){return p.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[p.jsx("h1",{children:"Mounted Archives"}),p.jsx("div",{className:"admin-list",children:e.map(t=>p.jsxs("div",{className:"admin-archive",children:[p.jsx("strong",{children:t.label}),p.jsx("div",{className:"muted",children:t.archive_path})]},t.id))})]})}function cc({node:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){var u;const l=t===e.tag.full_path;function i(){const o=l?null:e.tag.full_path;n(o),r("archive")}return p.jsxs("li",{children:[p.jsx("button",{className:`tag-node-btn${l?" is-active":""}`,title:e.tag.full_path,onClick:i,children:e.tag.name}),((u=e.children)==null?void 0:u.length)>0&&p.jsx("div",{className:"tag-children",children:p.jsx("ul",{className:"tag-tree-list",children:e.children.map(o=>p.jsx(cc,{node:o,tagFilter:t,onTagFilterSet:n,onViewChange:r},o.tag.tag_uid))})})]})}function sp({tagNodes:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){return p.jsx("section",{id:"tags-view",className:"view is-active",children:p.jsx("div",{className:"tag-tree",children:e.length===0?p.jsx("div",{children:"No tags yet."}):p.jsx("ul",{className:"tag-tree-list",children:e.map(l=>p.jsx(cc,{node:l,tagFilter:t,onTagFilterSet:n,onViewChange:r},l.tag.tag_uid))})})})}function ap({archiveId:e,selectedEntry:t,onTagFilterSet:n,tagNodes:r,onTagsRefresh:l}){const[i,u]=N.useState(null),[o,s]=N.useState([]),[f,v]=N.useState(""),[m,h]=N.useState(""),y=N.useRef(0);N.useEffect(()=>{if(!t||!e){u(null),s([]);return}const x=++y.current;u(null),s([]),Promise.all([Bd(e,t.entry_uid),Gl(e,t.entry_uid)]).then(([c,a])=>{x===y.current&&(u(c),s(a))}).catch(()=>{})},[t,e]);async function S(){const x=f.trim();if(!(!x||!t))try{await Hd(e,t.entry_uid,x),v(""),h("");const c=await Gl(e,t.entry_uid);s(c),l()}catch(c){h(c.message)}}async function k(x){try{await Wd(e,t.entry_uid,x);const c=await Gl(e,t.entry_uid);s(c),l()}catch{}}return p.jsxs("aside",{className:"context-rail",children:[p.jsx("div",{className:"rail-title",children:"Context"}),t?i?p.jsxs("div",{className:"rail-body",children:[p.jsx("strong",{className:"rail-entry-title",children:zt(i.summary.title)||zt(i.summary.entry_uid)}),p.jsxs("div",{className:"rail-section",children:[i.summary.original_url&&p.jsxs("div",{className:"rail-item",children:[p.jsx("span",{className:"rail-label",children:"Original URL"}),":"," ",p.jsx("a",{href:i.summary.original_url,target:"_blank",rel:"noopener noreferrer",className:"rail-url-link",children:i.summary.original_url})]}),[["Added",ac(i.summary.archived_at)],["Source",i.summary.source_kind],["Type",i.summary.entity_kind],["Visibility",i.summary.visibility],["Structured root",i.structured_root_relpath]].map(([x,c])=>p.jsxs("div",{className:"rail-item",children:[p.jsx("span",{className:"rail-label",children:x}),": ",zt(c)]},x))]}),i.artifacts.length>0?p.jsxs("div",{className:"rail-section",children:[p.jsxs("div",{className:"rail-section-heading",children:["Artifacts (",i.artifacts.length,")"]}),p.jsx("ul",{className:"artifact-list",children:i.artifacts.map((x,c)=>p.jsx("li",{children:p.jsxs("a",{href:`/api/archives/${e}/entries/${i.summary.entry_uid}/artifacts/${c}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[x.artifact_role.replace(/_/g," "),x.byte_size!=null?` (${sc(x.byte_size)})`:""]})},c))})]}):p.jsx("div",{className:"rail-item muted",children:"No artifacts."})]}):p.jsx("div",{className:"rail-body",children:"Loading…"}):p.jsx("div",{className:"rail-body",children:"Select an entry."}),t&&p.jsxs(p.Fragment,{children:[p.jsx("div",{className:"entry-tags",children:o.length===0?p.jsx("span",{className:"muted",children:"No tags."}):o.map(x=>p.jsxs("span",{className:"tag-pill",title:x.full_path,children:[x.name,p.jsx("button",{className:"remove-tag",title:`Remove tag ${x.full_path}`,onClick:()=>k(x.tag_uid),children:"×"})]},x.tag_uid))}),p.jsxs("div",{className:"assign-tag-form",children:[p.jsx("input",{className:"assign-tag-input",type:"text",placeholder:"/science/cs",autoComplete:"off",value:f,onChange:x=>v(x.target.value),onKeyDown:x=>{x.key==="Enter"&&S()}}),p.jsx("button",{className:"assign-tag-btn",onClick:S,children:"Add tag"}),m&&p.jsx("div",{className:"muted",style:{fontSize:"0.85em",color:"var(--accent)"},children:m})]})]})]})}const fc=N.createContext(null);function cp(){const[e,t]=N.useState("loading"),[n,r]=N.useState(null);N.useEffect(()=>{(async()=>{if(await Yd()){t("setup");return}const ze=await Jd();if(!ze){t("login");return}r(ze),t("authenticated")})()},[]),N.useEffect(()=>{const F=()=>{r(null),t("login")};return window.addEventListener("auth:expired",F),()=>window.removeEventListener("auth:expired",F)},[]);const[l,i]=N.useState([]),[u,o]=N.useState(null),[s,f]=N.useState([]),[v,m]=N.useState(null),[h,y]=N.useState(null),[S,k]=N.useState(null),[x,c]=N.useState("archive"),[a,d]=N.useState(""),[g,E]=N.useState(""),[j,P]=N.useState(!1),[z,A]=N.useState([]),[R,oe]=N.useState([]),[Et,et]=N.useState(!1),tt=N.useCallback(async(F,ze,$e)=>{if(F){P(!0);try{let Te;ze||$e?Te=await Vd(F,ze,$e):Te=await Ad(F),f(Te),E(Te.length===0?"No results":`${Te.length} result${Te.length===1?"":"s"}`)}catch{f([]),E("Search failed. Try again.")}finally{P(!1)}}},[]);N.useEffect(()=>{Ud().then(F=>{if(i(F),F.length>0){const ze=F[0].id;o(ze)}})},[]),N.useEffect(()=>{u&&(k(null),y(null),m(null),Promise.all([tt(u,"",null),Yo(u).then(A),Zl(u).then(oe)]))},[u]),N.useEffect(()=>{if(u===null)return;const F=setTimeout(()=>{tt(u,a,S)},300);return()=>clearTimeout(F)},[a,u]),N.useEffect(()=>{u!==null&&(c("archive"),tt(u,a,S))},[S,u]);const xl=N.useCallback(F=>{o(F)},[]),Ut=N.useCallback(F=>{c(F),F==="tags"&&u&&Zl(u).then(oe)},[u]),gn=N.useCallback(F=>{m(F?F.entry_uid:null),y(F)},[]),C=N.useCallback(F=>{k(F)},[]),T=N.useCallback(()=>{k(null)},[]),L=N.useCallback(()=>{u&&Zl(u).then(oe)},[u]),B=N.useCallback(()=>{et(!0)},[]),X=N.useCallback(()=>{et(!1)},[]),At=N.useCallback(()=>{u&&Promise.all([tt(u,a,S),Yo(u).then(A)])},[u,a,S,tt]);return e==="loading"?p.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?p.jsx(ep,{onComplete:()=>t("login")}):e==="login"?p.jsx(bd,{onLogin:F=>{r(F),t("authenticated")}}):p.jsx(fc.Provider,{value:{currentUser:n,setCurrentUser:r},children:p.jsxs(p.Fragment,{children:[p.jsx(tp,{archives:l,archiveId:u,onArchiveChange:xl,view:x,onViewChange:Ut,onCaptureClick:B}),p.jsxs("main",{className:"app-shell",children:[p.jsxs("div",{className:"workspace",children:[x==="archive"&&p.jsxs("div",{className:"search-row",children:[p.jsx("input",{className:"search-input",type:"search","aria-label":"Search archive","aria-busy":j,value:a,onChange:F=>d(F.target.value)}),p.jsxs("div",{className:"result-count",children:[g,S&&p.jsxs("button",{className:"tag-filter-badge",onClick:T,children:["× ",S]})]})]}),x==="archive"&&p.jsx(ip,{entries:s,selectedEntryUid:v,onSelectEntry:gn,archiveId:u,tagFilter:S,onClearTagFilter:T,searchQuery:a,onSearchChange:d,resultCount:g,searchBusy:j}),x==="runs"&&p.jsx(up,{runs:z}),x==="admin"&&p.jsx(op,{archives:l}),x==="tags"&&p.jsx(sp,{tagNodes:R,tagFilter:S,onTagFilterSet:C,onViewChange:Ut})]}),p.jsx(ap,{archiveId:u,selectedEntry:h,onTagFilterSet:C,tagNodes:R,onTagsRefresh:L})]}),p.jsx(np,{open:Et,archiveId:u,onClose:X,onCaptured:At})]})})}oc(document.getElementById("root")).render(p.jsx(N.StrictMode,{children:p.jsx(cp,{})})); diff --git a/crates/archivr-server/static/index.html b/crates/archivr-server/static/index.html index 5d3d3d6..c3da2f9 100644 --- a/crates/archivr-server/static/index.html +++ b/crates/archivr-server/static/index.html @@ -4,8 +4,8 @@ Archivr - - + +
diff --git a/docs/superpowers/plans/2026-06-25-auth-foundation.md b/docs/superpowers/plans/2026-06-25-auth-foundation.md deleted file mode 100644 index 3f9da52..0000000 --- a/docs/superpowers/plans/2026-06-25-auth-foundation.md +++ /dev/null @@ -1,1977 +0,0 @@ -# Auth Foundation 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:** Add cookie-session + API-token authentication, a role bitmask system, a first-run setup wizard, and auth-protected routes to the Archivr server. - -**Architecture:** Auth state lives in a dedicated `archivr-auth.sqlite` file next to the server config, separate from the per-archive store DBs. `AppState` gains an `auth_db_path`. Route handlers call `database::open_auth_db()` to get a connection; a new `AuthUser` Axum extractor validates session cookies and Bearer tokens for every protected handler. - -**Tech Stack:** Rust/Axum, rusqlite (existing), argon2 (Argon2id hashing), rand (token generation), axum-extra (cookie extraction), React/Vite (existing frontend) - -**Spec:** `docs/superpowers/specs/2026-06-25-auth-foundation-design.md` - ---- - -## File Map - -| File | Action | What changes | -|---|---|---| -| `Cargo.toml` | Modify | Add `argon2`, `rand`, `axum-extra` to workspace deps | -| `crates/archivr-server/Cargo.toml` | Modify | Pull `argon2`, `rand`, `axum-extra` from workspace | -| `crates/archivr-core/src/database.rs` | Modify | `initialize_auth_schema`, auth CRUD helpers, new record types | -| `crates/archivr-server/src/auth.rs` | **Create** | `AuthUser` extractor, password helpers, token generation, role constants | -| `crates/archivr-server/src/routes.rs` | Modify | `AppState` + `app()` gain `auth_db_path`; auth endpoints; route protection; `ApiError` gets JSON body + `unauthorized`/`forbidden` constructors | -| `crates/archivr-server/src/registry.rs` | Modify | `ServerRegistry` gains optional `auth_db_path` | -| `crates/archivr-server/src/main.rs` | Modify | Compute auth DB path; pass to `app()`; session cleanup task; remove non-loopback auth warning | -| `frontend/src/App.jsx` | Modify | Setup check on mount; `AuthContext`; 401 handling | -| `frontend/src/api.js` | Modify | 401 interceptor; auth helper calls | -| `frontend/src/components/LoginPage.jsx` | **Create** | Login form | -| `frontend/src/components/SetupPage.jsx` | **Create** | First-run owner creation wizard | -| `frontend/src/components/Topbar.jsx` | Modify | User menu + logout button | - ---- - -## Task 1: Add dependencies - -**Files:** -- Modify: `Cargo.toml` -- Modify: `crates/archivr-server/Cargo.toml` - -- [ ] **Step 1: Add workspace dependencies** - -Open `Cargo.toml`. In `[workspace.dependencies]`, add after the `base64` line: - -```toml -argon2 = { version = "0.5", features = ["std"] } -rand = { version = "0.8", features = ["std"] } -axum-extra = { version = "0.9", features = ["cookie"] } -``` - -- [ ] **Step 2: Pull into server crate** - -Open `crates/archivr-server/Cargo.toml`. Add to `[dependencies]`: - -```toml -argon2.workspace = true -rand.workspace = true -axum-extra.workspace = true -``` - -- [ ] **Step 3: Verify compilation** - -```bash -cd ~/personal/archivr && cargo check -p archivr-server -``` - -Expected: compiles with no errors. - -- [ ] **Step 4: Commit** - -```bash -git add Cargo.toml crates/archivr-server/Cargo.toml Cargo.lock -git commit -m "feat(auth): add argon2, rand, axum-extra dependencies" -``` - ---- - -## Task 2: Auth schema in `database.rs` - -Add `initialize_auth_schema` and update `instance_settings`. - -**Files:** -- Modify: `crates/archivr-core/src/database.rs` - -- [ ] **Step 1: Write failing test for role seeding** - -At the bottom of the `#[cfg(test)]` block in `database.rs`, add: - -```rust -#[test] -fn auth_schema_seeds_builtin_roles() { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - let count: i64 = conn - .query_row("SELECT COUNT(*) FROM roles WHERE is_builtin = 1", [], |r| r.get(0)) - .unwrap(); - assert_eq!(count, 4); - let owner_bits: i64 = conn - .query_row("SELECT bit_position FROM roles WHERE slug = 'owner'", [], |r| r.get(0)) - .unwrap(); - assert_eq!(owner_bits, 3); -} - -#[test] -fn auth_schema_is_idempotent() { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - initialize_auth_schema(&conn).unwrap(); // must not panic -} -``` - -- [ ] **Step 2: Run to confirm they fail** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core auth_schema 2>&1 | tail -5 -``` - -Expected: FAILED — `initialize_auth_schema` not found. - -- [ ] **Step 3: Add `initialize_auth_schema`** - -After the closing `}` of `initialize_schema` in `database.rs`, add: - -```rust -pub fn initialize_auth_schema(conn: &Connection) -> Result<()> { - conn.pragma_update(None, "journal_mode", "WAL")?; - conn.pragma_update(None, "foreign_keys", "ON")?; - conn.execute_batch( - r#" - CREATE TABLE IF NOT EXISTS roles ( - id INTEGER PRIMARY KEY, - role_uid TEXT NOT NULL UNIQUE, - slug TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - level INTEGER NOT NULL, - bit_position INTEGER NOT NULL UNIQUE, - is_builtin INTEGER NOT NULL DEFAULT 0 CHECK (is_builtin IN (0, 1)) - ); - - INSERT OR IGNORE INTO roles (role_uid, slug, name, level, bit_position, is_builtin) VALUES - ('role-guest', 'guest', 'Guest', 0, 0, 1), - ('role-user', 'user', 'User', 1, 1, 1), - ('role-admin', 'admin', 'Admin', 3, 2, 1), - ('role-owner', 'owner', 'Owner', 4, 3, 1); - - CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_id INTEGER NOT NULL REFERENCES roles(id), - assigned_at TEXT NOT NULL, - assigned_by_user_id INTEGER REFERENCES users(id), - PRIMARY KEY (user_id, role_id) - ); - - CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY, - session_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_bits INTEGER NOT NULL, - created_at TEXT NOT NULL, - last_seen_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - user_agent TEXT - ); - CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); - - CREATE TABLE IF NOT EXISTS api_tokens ( - id INTEGER PRIMARY KEY, - token_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - token_hash TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - created_at TEXT NOT NULL, - last_used_at TEXT, - expires_at TEXT - ); - CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id ON api_tokens(user_id); - - CREATE TABLE IF NOT EXISTS instance_settings ( - id INTEGER PRIMARY KEY CHECK (id = 1), - public_index_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_index_enabled IN (0, 1)), - public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)), - public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)), - default_entry_visibility INTEGER NOT NULL DEFAULT 2 - ); - - INSERT OR IGNORE INTO instance_settings - (id, public_index_enabled, public_entry_content_enabled, - public_archive_submission_enabled, default_entry_visibility) - VALUES (1, 0, 0, 0, 2); - - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - user_uid TEXT NOT NULL UNIQUE, - username TEXT NOT NULL UNIQUE, - email TEXT UNIQUE, - password_hash TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('active', 'disabled')), - role TEXT NOT NULL CHECK (role IN ('admin', 'user')), - created_at TEXT NOT NULL, - last_login_at TEXT - ); - "#, - )?; - Ok(()) -} -``` - -Note: `users` is duplicated here with `CREATE TABLE IF NOT EXISTS` so the auth DB is self-contained without needing `initialize_schema`. - -- [ ] **Step 4: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core auth_schema 2>&1 | tail -5 -``` - -Expected: 2 tests pass. - -- [ ] **Step 5: Add `open_auth_db` function** - -After `open_or_initialize`, add: - -```rust -pub fn open_auth_db(auth_db_path: &Path) -> Result { - if let Some(parent) = auth_db_path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!("failed to create auth DB directory {}", parent.display()) - })?; - } - let conn = Connection::open(auth_db_path).with_context(|| { - format!("failed to open auth database at {}", auth_db_path.display()) - })?; - initialize_auth_schema(&conn)?; - Ok(conn) -} -``` - -- [ ] **Step 6: Verify compilation and commit** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core 2>&1 | tail -3 -``` - -Expected: all existing tests pass. - -```bash -git add crates/archivr-core/src/database.rs -git commit -m "feat(auth): add initialize_auth_schema and open_auth_db" -``` - ---- - -## Task 3: User and role DB helpers - -**Files:** -- Modify: `crates/archivr-core/src/database.rs` - -- [ ] **Step 1: Add record types** - -After the existing struct definitions (before `pub fn database_path`), add: - -```rust -#[derive(Debug, Clone)] -pub struct AuthUserRecord { - pub id: i64, - pub user_uid: String, - pub username: String, - pub password_hash: String, - pub status: String, -} - -#[derive(Debug, Clone)] -pub struct SessionRecord { - pub user_id: i64, - pub role_bits: u32, - pub last_seen_at: String, - pub session_uid: String, -} - -#[derive(Debug, Clone)] -pub struct ApiTokenRecord { - pub token_uid: String, - pub name: String, - pub created_at: String, - pub last_used_at: Option, -} -``` - -- [ ] **Step 2: Write failing tests for user helpers** - -In the `#[cfg(test)]` block, add: - -```rust -fn make_auth_conn() -> Connection { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - conn -} - -#[test] -fn ensure_owner_exists_returns_false_when_no_owner() { - let conn = make_auth_conn(); - assert!(!ensure_owner_exists(&conn).unwrap()); -} - -#[test] -fn create_owner_then_ensure_returns_true() { - let conn = make_auth_conn(); - create_owner(&conn, "alice", "hashed_pw").unwrap(); - assert!(ensure_owner_exists(&conn).unwrap()); -} - -#[test] -fn create_owner_assigns_cumulative_roles() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "hashed_pw").unwrap(); - let bits = compute_role_bits(&conn, user_id).unwrap(); - // guest=1, user=2, admin=4, owner=8 → 15 - assert_eq!(bits, 15u32); -} - -#[test] -fn get_user_by_username_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_user_by_username(&conn, "nobody").unwrap().is_none()); -} -``` - -- [ ] **Step 3: Run to confirm they fail** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core ensure_owner 2>&1 | tail -5 -``` - -Expected: FAILED. - -- [ ] **Step 4: Implement user/role helpers** - -After `open_auth_db`, add: - -```rust -/// Returns true if an owner account exists. -pub fn ensure_owner_exists(conn: &Connection) -> Result { - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM user_roles ur - JOIN roles r ON r.id = ur.role_id - WHERE r.slug = 'owner'", - [], - |row| row.get(0), - )?; - Ok(count > 0) -} - -/// Creates a user and assigns all roles from `user` up to `owner` (cumulative). -/// `password_hash` must already be hashed by the caller. -pub fn create_owner(conn: &Connection, username: &str, password_hash: &str) -> Result { - let user_uid = public_id("usr"); - conn.execute( - "INSERT INTO users (user_uid, username, email, password_hash, status, role, created_at) - VALUES (?1, ?2, NULL, ?3, 'active', 'admin', ?4)", - params![user_uid, username, password_hash, now_timestamp()], - )?; - let user_id = conn.last_insert_rowid(); - // Assign user, admin, owner (cumulative) - for slug in &["user", "admin", "owner"] { - let role_id: i64 = conn.query_row( - "SELECT id FROM roles WHERE slug = ?1", - [slug], - |row| row.get(0), - )?; - conn.execute( - "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at) - VALUES (?1, ?2, ?3)", - params![user_id, role_id, now_timestamp()], - )?; - } - Ok(user_id) -} - -pub fn get_user_by_username(conn: &Connection, username: &str) -> Result> { - conn.query_row( - "SELECT id, user_uid, username, password_hash, status FROM users WHERE username = ?1", - [username], - |row| { - Ok(AuthUserRecord { - id: row.get(0)?, - user_uid: row.get(1)?, - username: row.get(2)?, - password_hash: row.get(3)?, - status: row.get(4)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -/// Computes role_bits = ROLE_GUEST | OR(assigned role bit values). -/// ROLE_GUEST (bit 0, value 1) is always included as the implicit floor. -pub fn compute_role_bits(conn: &Connection, user_id: i64) -> Result { - let mut stmt = conn.prepare( - "SELECT (1 << r.bit_position) FROM user_roles ur - JOIN roles r ON r.id = ur.role_id - WHERE ur.user_id = ?1", - )?; - let bits: u32 = stmt - .query_map([user_id], |row| row.get::<_, i64>(0))? - .try_fold(1u32, |acc, val| val.map(|v| acc | v as u32))?; - Ok(bits) -} -``` - -- [ ] **Step 5: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core ensure_owner create_owner get_user compute_role 2>&1 | tail -5 -``` - -Expected: all 4 new tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-core/src/database.rs -git commit -m "feat(auth): user and role DB helpers (create_owner, compute_role_bits)" -``` - ---- - -## Task 4: Session and token DB helpers - -**Files:** -- Modify: `crates/archivr-core/src/database.rs` - -- [ ] **Step 1: Write failing session tests** - -```rust -#[test] -fn create_and_get_session() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - let uid = create_session(&conn, user_id, 15, None).unwrap(); - let sess = get_session(&conn, &uid).unwrap().unwrap(); - assert_eq!(sess.user_id, user_id); - assert_eq!(sess.role_bits, 15); -} - -#[test] -fn get_session_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_session(&conn, "nonexistent").unwrap().is_none()); -} - -#[test] -fn delete_session_removes_it() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - let uid = create_session(&conn, user_id, 15, None).unwrap(); - delete_session(&conn, &uid).unwrap(); - assert!(get_session(&conn, &uid).unwrap().is_none()); -} - -#[test] -fn token_hash_round_trips() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - create_api_token(&conn, user_id, "hash_abc", "My Token").unwrap(); - let found_id = get_user_for_token(&conn, "hash_abc").unwrap(); - assert_eq!(found_id, Some(user_id)); -} - -#[test] -fn get_user_for_token_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_user_for_token(&conn, "unknown").unwrap().is_none()); -} -``` - -- [ ] **Step 2: Run to confirm failure** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core create_and_get_session token_hash 2>&1 | tail -5 -``` - -- [ ] **Step 3: Implement session helpers** - -After `compute_role_bits`, add: - -```rust -/// Returns a new session_uid (UUID). -pub fn create_session( - conn: &Connection, - user_id: i64, - role_bits: u32, - user_agent: Option<&str>, -) -> Result { - let session_uid = public_id("sess"); - let now = now_timestamp(); - // expires_at = 30 days from now (approximate via string arithmetic is fragile; - // compute with chrono instead) - let expires_at = chrono::Utc::now() - .checked_add_signed(chrono::Duration::days(30)) - .unwrap() - .format("%Y-%m-%dT%H-%M-%S%.3f") - .to_string(); - conn.execute( - "INSERT INTO sessions (session_uid, user_id, role_bits, created_at, last_seen_at, expires_at, user_agent) - VALUES (?1, ?2, ?3, ?4, ?4, ?5, ?6)", - params![session_uid, user_id, role_bits as i64, now, expires_at, user_agent], - )?; - Ok(session_uid) -} - -/// Returns session if it exists, the user is active, and it has not expired. -pub fn get_session(conn: &Connection, session_uid: &str) -> Result> { - let now = now_timestamp(); - conn.query_row( - "SELECT s.user_id, s.role_bits, s.last_seen_at, s.session_uid - FROM sessions s - JOIN users u ON u.id = s.user_id - WHERE s.session_uid = ?1 - AND u.status = 'active' - AND s.expires_at > ?2", - params![session_uid, now], - |row| { - Ok(SessionRecord { - user_id: row.get(0)?, - role_bits: row.get::<_, i64>(1)? as u32, - last_seen_at: row.get(2)?, - session_uid: row.get(3)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -pub fn delete_session(conn: &Connection, session_uid: &str) -> Result<()> { - conn.execute("DELETE FROM sessions WHERE session_uid = ?1", [session_uid])?; - Ok(()) -} - -/// Updates last_seen_at and extends expires_at by 30 days. -pub fn touch_session(conn: &Connection, session_uid: &str) -> Result<()> { - let now = now_timestamp(); - let new_expires = chrono::Utc::now() - .checked_add_signed(chrono::Duration::days(30)) - .unwrap() - .format("%Y-%m-%dT%H-%M-%S%.3f") - .to_string(); - conn.execute( - "UPDATE sessions SET last_seen_at = ?1, expires_at = ?2 WHERE session_uid = ?3", - params![now, new_expires, session_uid], - )?; - Ok(()) -} - -pub fn delete_expired_sessions(conn: &Connection) -> Result { - let now = now_timestamp(); - let n = conn.execute("DELETE FROM sessions WHERE expires_at <= ?1", [now])?; - Ok(n) -} -``` - -- [ ] **Step 4: Implement token helpers** - -```rust -/// Creates an API token. `token_hash` is SHA3-256 hex of the raw token. -/// Returns the token_uid. -pub fn create_api_token( - conn: &Connection, - user_id: i64, - token_hash: &str, - name: &str, -) -> Result { - let token_uid = public_id("tok"); - conn.execute( - "INSERT INTO api_tokens (token_uid, user_id, token_hash, name, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![token_uid, user_id, token_hash, name, now_timestamp()], - )?; - Ok(token_uid) -} - -/// Returns the user_id for a given token hash, if the token is valid and user is active. -pub fn get_user_for_token(conn: &Connection, token_hash: &str) -> Result> { - let now = now_timestamp(); - conn.query_row( - "SELECT t.user_id FROM api_tokens t - JOIN users u ON u.id = t.user_id - WHERE t.token_hash = ?1 - AND u.status = 'active' - AND (t.expires_at IS NULL OR t.expires_at > ?2)", - params![token_hash, now], - |row| row.get(0), - ) - .optional() - .map_err(Into::into) -} - -pub fn touch_token(conn: &Connection, token_uid: &str) -> Result<()> { - conn.execute( - "UPDATE api_tokens SET last_used_at = ?1 WHERE token_uid = ?2", - params![now_timestamp(), token_uid], - )?; - Ok(()) -} - -/// Returns true if the token was found and deleted (user_id must match). -pub fn delete_api_token(conn: &Connection, token_uid: &str, user_id: i64) -> Result { - let n = conn.execute( - "DELETE FROM api_tokens WHERE token_uid = ?1 AND user_id = ?2", - params![token_uid, user_id], - )?; - Ok(n > 0) -} - -pub fn list_user_tokens(conn: &Connection, user_id: i64) -> Result> { - let mut stmt = conn.prepare( - "SELECT token_uid, name, created_at, last_used_at - FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC", - )?; - let records = stmt - .query_map([user_id], |row| { - Ok(ApiTokenRecord { - token_uid: row.get(0)?, - name: row.get(1)?, - created_at: row.get(2)?, - last_used_at: row.get(3)?, - }) - })? - .collect::, _>>()?; - Ok(records) -} -``` - -- [ ] **Step 5: Run all new tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core 2>&1 | tail -5 -``` - -Expected: all tests pass (including all previously passing ones). - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-core/src/database.rs -git commit -m "feat(auth): session and token DB helpers" -``` - ---- - -## Task 5: Auth DB path in AppState, registry, and main.rs - -**Files:** -- Modify: `crates/archivr-server/src/registry.rs` -- Modify: `crates/archivr-server/src/routes.rs` -- Modify: `crates/archivr-server/src/main.rs` - -- [ ] **Step 1: Add `auth_db_path` to `ServerRegistry`** - -In `registry.rs`, update the `ServerRegistry` struct: - -```rust -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct ServerRegistry { - #[serde(default)] - pub archives: Vec, - /// Optional bind address. Defaults to `127.0.0.1:8080`. - #[serde(default)] - pub bind: Option, - /// Path to the server-level auth database. - /// Defaults to `archivr-auth.sqlite` in the same directory as the config file. - #[serde(default)] - pub auth_db_path: Option, -} -``` - -- [ ] **Step 2: Update `AppState` in `routes.rs`** - -Change `AppState`: - -```rust -#[derive(Clone)] -pub struct AppState { - registry: Arc, - pub auth_db_path: Arc, -} -``` - -Update `app()` signature and body: - -```rust -pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router { - let state = AppState { - registry: Arc::new(registry), - auth_db_path: Arc::new(auth_db_path), - }; - // ... rest unchanged -``` - -- [ ] **Step 3: Update all tests in `routes.rs` that call `app(registry)`** - -Every `app(registry)` in the test module must become `app(registry, std::path::PathBuf::from("/tmp/test-auth.sqlite"))`. - -Search for all occurrences: - -```bash -grep -n "app(registry" ~/personal/archivr/crates/archivr-server/src/routes.rs | head -20 -``` - -Update each one to `app(registry, tempfile::tempdir().unwrap().path().join("auth.sqlite"))`. Add `use tempfile;` if not present. (The `tempfile` crate is already a workspace dependency.) - -- [ ] **Step 4: Update `main.rs`** - -```rust -mod registry; -mod routes; - -use anyhow::{Context, Result}; -use std::{net::SocketAddr, path::PathBuf}; - -const DEFAULT_BIND: &str = "127.0.0.1:8080"; - -#[tokio::main] -async fn main() -> Result<()> { - let config_path = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("archivr-server.toml")); - - let registry = registry::load_registry(&config_path)?; - - // Auth DB lives next to the config file unless overridden. - let auth_db_path = registry.auth_db_path.clone().unwrap_or_else(|| { - config_path - .parent() - .unwrap_or_else(|| std::path::Path::new(".")) - .join("archivr-auth.sqlite") - }); - - let app = routes::app(registry.clone(), auth_db_path.clone()); - - let bind_str = std::env::var("ARCHIVR_BIND") - .ok() - .or_else(|| registry.bind.clone()) - .unwrap_or_else(|| DEFAULT_BIND.to_string()); - - let addr: SocketAddr = bind_str - .parse() - .with_context(|| format!("invalid bind address: {bind_str}"))?; - - let listener = tokio::net::TcpListener::bind(addr).await?; - println!("archivr-server listening on http://{addr}"); - axum::serve(listener, app).await?; - Ok(()) -} -``` - -- [ ] **Step 5: Run full test suite** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-server/src/routes.rs \ - crates/archivr-server/src/registry.rs \ - crates/archivr-server/src/main.rs -git commit -m "feat(auth): add auth_db_path to AppState, registry, and main.rs" -``` - ---- - -## Task 6: Create `auth.rs` — AuthUser extractor - -**Files:** -- Create: `crates/archivr-server/src/auth.rs` -- Modify: `crates/archivr-server/src/routes.rs` (add `mod auth; use auth::AuthUser;`) - -- [ ] **Step 1: Create `auth.rs`** - -Create `crates/archivr-server/src/auth.rs` with the following content: - -```rust -use anyhow::Result; -use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; -use argon2::password_hash::{SaltString, rand_core::OsRng}; -use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; -use axum_extra::extract::CookieJar; -use rand::RngCore; - -use crate::routes::{ApiError, AppState}; -use archivr_core::database; - -// ── Role bit constants ──────────────────────────────────────────────────────── -pub const ROLE_GUEST: u32 = 1; // bit 0 -pub const ROLE_USER: u32 = 2; // bit 1 -pub const ROLE_ADMIN: u32 = 4; // bit 2 -pub const ROLE_OWNER: u32 = 8; // bit 3 - -// ── AuthUser ───────────────────────────────────────────────────────────────── -#[derive(Clone, Debug)] -pub enum AuthUser { - Guest, - Authenticated { user_id: i64, role_bits: u32 }, -} - -impl AuthUser { - /// Returns (user_id, role_bits) or 401 if Guest. - pub fn require_auth(&self) -> Result<(i64, u32), ApiError> { - match self { - AuthUser::Authenticated { user_id, role_bits } => Ok((*user_id, *role_bits)), - AuthUser::Guest => Err(ApiError::unauthorized("login required")), - } - } - - /// Returns Ok(()) if the user has the given role bit set, else 401/403. - pub fn require_role(&self, bit: u32) -> Result<(), ApiError> { - match self { - AuthUser::Authenticated { role_bits, .. } if role_bits & bit != 0 => Ok(()), - AuthUser::Authenticated { .. } => Err(ApiError::forbidden("insufficient permissions")), - AuthUser::Guest => Err(ApiError::unauthorized("login required")), - } - } - - pub fn has_role(&self, bit: u32) -> bool { - matches!(self, AuthUser::Authenticated { role_bits, .. } if role_bits & bit != 0) - } -} - -#[async_trait] -impl FromRequestParts for AuthUser { - type Rejection = std::convert::Infallible; - - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - let auth_db_path = state.auth_db_path.as_ref(); - - // 1. Try session cookie - let jar = CookieJar::from_headers(&parts.headers); - if let Some(cookie) = jar.get("session") { - let session_uid = cookie.value().to_string(); - if let Ok(conn) = database::open_auth_db(auth_db_path) { - if let Ok(Some(session)) = database::get_session(&conn, &session_uid) { - // Conditional touch: only update last_seen_at if more than 60s have elapsed. - // The session row is already in memory, so no extra query needed. - let should_touch = chrono::NaiveDateTime::parse_from_str( - &session.last_seen_at, "%Y-%m-%dT%H-%M-%S%.3f", - ) - .map(|last| { - let last_utc = chrono::DateTime::::from_naive_utc_and_offset( - last, chrono::Utc, - ); - chrono::Utc::now() - last_utc > chrono::Duration::seconds(60) - }) - .unwrap_or(true); // if parse fails, touch anyway - if should_touch { - let _ = database::touch_session(&conn, &session_uid); - } - return Ok(AuthUser::Authenticated { - user_id: session.user_id, - role_bits: session.role_bits, - }); - } - } - } - - // 2. Try Bearer token - if let Some(auth_header) = parts.headers.get("Authorization") { - if let Ok(header_str) = auth_header.to_str() { - if let Some(raw_token) = header_str.strip_prefix("Bearer ") { - let token_hash = hash_token(raw_token); - if let Ok(conn) = database::open_auth_db(auth_db_path) { - if let Ok(Some(user_id)) = database::get_user_for_token(&conn, &token_hash) { - // Get token_uid for touch (find by hash) - // Compute role_bits live for tokens - if let Ok(role_bits) = database::compute_role_bits(&conn, user_id) { - return Ok(AuthUser::Authenticated { user_id, role_bits }); - } - } - } - } - } - } - - Ok(AuthUser::Guest) - } -} - -// ── Password helpers ────────────────────────────────────────────────────────── - -pub fn hash_password(password: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); - let hash = Argon2::default() - .hash_password(password.as_bytes(), &salt) - .map_err(|e| anyhow::anyhow!("password hashing failed: {e}"))? - .to_string(); - Ok(hash) -} - -pub fn verify_password(password: &str, hash: &str) -> Result { - let parsed = PasswordHash::new(hash) - .map_err(|e| anyhow::anyhow!("invalid password hash: {e}"))?; - Ok(Argon2::default() - .verify_password(password.as_bytes(), &parsed) - .is_ok()) -} - -// ── Token helpers ───────────────────────────────────────────────────────────── - -/// Generates a cryptographically random 32-byte token, base64url-encoded. -pub fn generate_token() -> String { - let mut bytes = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut bytes); - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bytes) -} - -/// SHA3-256 hex hash of a raw token string. Used for storage and lookup. -pub fn hash_token(raw: &str) -> String { - archivr_core::hash::hash_bytes(raw.as_bytes()) -} -``` - -- [ ] **Step 2: Wire into `routes.rs`** - -At the top of `routes.rs`, add after `mod` declarations: - -```rust -mod auth; -pub use auth::{AuthUser, ROLE_ADMIN, ROLE_OWNER, ROLE_USER}; -``` - -Also add `unauthorized` and `forbidden` constructors to `ApiError`, and update `IntoResponse` to return JSON: - -```rust -impl ApiError { - // ... existing constructors ... - - pub fn unauthorized(message: &str) -> Self { - Self { status: StatusCode::UNAUTHORIZED, message: message.to_string() } - } - - pub fn forbidden(message: &str) -> Self { - Self { status: StatusCode::FORBIDDEN, message: message.to_string() } - } -} - -impl IntoResponse for ApiError { - fn into_response(self) -> Response { - let body = serde_json::json!({ "error": self.message }); - (self.status, axum::Json(body)).into_response() - } -} -``` - -- [ ] **Step 3: Compile check** - -```bash -cd ~/personal/archivr && cargo check -p archivr-server 2>&1 | tail -10 -``` - -Fix any import errors. Common fix: add `use std::path::Path;` or check `axum_extra` cookie import. - -- [ ] **Step 4: Write extractor tests in `routes.rs`** - -In the `#[cfg(test)]` block in `routes.rs`, add helpers and tests: - -```rust -fn make_test_app() -> (Router, tempfile::TempDir) { - let dir = tempfile::tempdir().unwrap(); - let auth_db_path = dir.path().join("auth.sqlite"); - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - (app(registry, auth_db_path), dir) -} - -#[tokio::test] -async fn health_check_returns_ok() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); -} -``` - -Update the existing `archives_endpoint_lists_mounted_archives` test to use `make_test_app()`: - -```rust -#[tokio::test] -async fn archives_endpoint_lists_mounted_archives() { - let dir = tempfile::tempdir().unwrap(); - let auth_db_path = dir.path().join("auth.sqlite"); - let registry = ServerRegistry { - archives: vec![MountedArchive { - id: "personal".to_string(), - label: "Personal".to_string(), - archive_path: std::path::PathBuf::from("/tmp/personal/.archivr"), - }], - bind: None, - auth_db_path: None, - }; - let response = app(registry, auth_db_path) - .oneshot(Request::builder().uri("/api/archives").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); -} -``` - -Apply the same pattern to **every other test** in the module that calls `app(registry)`. - -- [ ] **Step 5: Run all tests** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-server/src/auth.rs crates/archivr-server/src/routes.rs -git commit -m "feat(auth): AuthUser extractor, password helpers, token generation" -``` - ---- - -## Task 7: Auth endpoints — login, logout, /me, setup - -**Files:** -- Modify: `crates/archivr-server/src/routes.rs` - -Add the following to `routes.rs`. - -- [ ] **Step 1: Write failing tests for auth endpoints** - -```rust -#[tokio::test] -async fn setup_required_before_owner_created() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot( - Request::builder() - .uri("/api/auth/setup") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); - let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(json["setup_required"], true); -} - -#[tokio::test] -async fn login_with_wrong_password_returns_401() { - let dir = tempfile::tempdir().unwrap(); - let auth_db_path = dir.path().join("auth.sqlite"); - // Seed owner - { - let conn = archivr_core::database::open_auth_db(&auth_db_path).unwrap(); - let hash = crate::auth::hash_password("correct").unwrap(); - archivr_core::database::create_owner(&conn, "owner", &hash).unwrap(); - } - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - let response = app(registry, auth_db_path) - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/login") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner","password":"wrong"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); -} - -#[tokio::test] -async fn setup_post_creates_owner_and_returns_409_on_repeat() { - let (app_once, dir) = make_test_app(); - let auth_db_path = dir.path().join("auth.sqlite"); - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - - // First POST creates the owner - let response = app_once - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/setup") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner","password":"hunter2"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::CREATED); - - // Second POST must return 409 - let app2 = app(registry, auth_db_path); - let response2 = app2 - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/setup") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner2","password":"hunter2"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response2.status(), StatusCode::CONFLICT); -} -``` - -- [ ] **Step 2: Run to confirm failure** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server setup_required login_with_wrong 2>&1 | tail -10 -``` - -Expected: FAILED. - -- [ ] **Step 3: Add auth routes to `app()` in `routes.rs`** - -In the `app()` function, add these routes before `.with_state(state)`: - -```rust -.route("/api/auth/setup", get(auth_setup_status).post(auth_setup)) -.route("/api/auth/login", post(auth_login)) -.route("/api/auth/logout", post(auth_logout)) -.route("/api/auth/me", get(auth_me)) -// Setup guard must come AFTER routes are defined and BEFORE .with_state -.layer(axum::middleware::from_fn_with_state(state.clone(), setup_guard)) -``` - -- [ ] **Step 3b: Implement `setup_guard` middleware** - -Add this function before the `app()` definition: - -```rust -/// Tower middleware: returns 503 on all non-exempt routes if setup hasn't been completed. -async fn setup_guard( - State(state): State, - req: axum::extract::Request, - next: axum::middleware::Next, -) -> Response { - let path = req.uri().path(); - let exempt = path == "/api/auth/setup" - || path == "/api/auth/login" - || path.starts_with("/assets") - || path == "/" - || path == "/health"; - if !exempt { - if let Ok(conn) = database::open_auth_db(&state.auth_db_path) { - if matches!(database::ensure_owner_exists(&conn), Ok(false)) { - return ( - StatusCode::SERVICE_UNAVAILABLE, - axum::Json(serde_json::json!({ "error": "setup_required" })), - ) - .into_response(); - } - } - } - next.run(req).await -} -``` - -- [ ] **Step 4: Add request/response types** - -After the existing `#[derive(serde::Deserialize)]` structs, add: - -```rust -#[derive(Debug, serde::Deserialize)] -struct LoginBody { - username: String, - password: String, -} - -#[derive(Debug, serde::Deserialize)] -struct SetupBody { - username: String, - password: String, -} -``` - -- [ ] **Step 5: Implement handler functions** - -Add after the existing handlers: - -```rust -async fn auth_setup_status( - State(state): State, -) -> Result, ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - let required = !database::ensure_owner_exists(&conn)?; - Ok(Json(serde_json::json!({ "setup_required": required }))) -} - -async fn auth_setup( - State(state): State, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - if database::ensure_owner_exists(&conn)? { - return Err(ApiError { - status: StatusCode::CONFLICT, - message: "already_configured".to_string(), - }); - } - if body.username.trim().is_empty() || body.password.len() < 8 { - return Err(ApiError::bad_request("username required and password must be at least 8 characters")); - } - let hash = auth::hash_password(&body.password).map_err(ApiError::from)?; - let user_id = database::create_owner(&conn, &body.username, &hash)?; - let user = database::get_user_by_username(&conn, &body.username)? - .ok_or_else(|| ApiError::internal("user not found after creation"))?; - Ok((StatusCode::CREATED, Json(serde_json::json!({ - "user_uid": user.user_uid, - "username": user.username, - })))) -} - -async fn auth_login( - State(state): State, - headers: axum::http::HeaderMap, - Json(body): Json, -) -> Result<(StatusCode, axum::http::HeaderMap, Json), ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - let user = database::get_user_by_username(&conn, &body.username)? - .filter(|u| u.status == "active") - .ok_or_else(|| ApiError::unauthorized("invalid_credentials"))?; - if !auth::verify_password(&body.password, &user.password_hash) - .map_err(ApiError::from)? - { - return Err(ApiError::unauthorized("invalid_credentials")); - } - let role_bits = database::compute_role_bits(&conn, user.id)?; - let user_agent = headers - .get("user-agent") - .and_then(|v| v.to_str().ok()); - let session_uid = database::create_session(&conn, user.id, role_bits, user_agent)?; - - // Build Set-Cookie header - let secure = headers - .get("x-forwarded-proto") - .and_then(|v| v.to_str().ok()) - .map(|v| v == "https") - .unwrap_or(false); - let cookie_value = format!( - "session={}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000{}", - session_uid, - if secure { "; Secure" } else { "" } - ); - let mut resp_headers = axum::http::HeaderMap::new(); - resp_headers.insert( - axum::http::header::SET_COOKIE, - cookie_value.parse().map_err(|_| ApiError::internal("cookie serialization failed"))?, - ); - - Ok((StatusCode::OK, resp_headers, Json(serde_json::json!({ - "user_uid": user.user_uid, - "username": user.username, - "role_bits": role_bits, - })))) -} - -async fn auth_logout( - State(state): State, - jar: CookieJar, -) -> Result<(StatusCode, axum::http::HeaderMap), ApiError> { - if let Some(cookie) = jar.get("session") { - let conn = database::open_auth_db(&state.auth_db_path)?; - database::delete_session(&conn, cookie.value())?; - } - let mut resp_headers = axum::http::HeaderMap::new(); - resp_headers.insert( - axum::http::header::SET_COOKIE, - "session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0" - .parse() - .unwrap(), - ); - Ok((StatusCode::NO_CONTENT, resp_headers)) -} - -async fn auth_me( - State(state): State, - auth_user: AuthUser, -) -> Result, ApiError> { - let (user_id, role_bits) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - // Look up username by user_id for response - let username: String = conn - .query_row("SELECT username FROM users WHERE id = ?1", [user_id], |r| r.get(0)) - .map_err(ApiError::from)?; - Ok(Json(serde_json::json!({ - "role_bits": role_bits, - "username": username, - }))) -} -``` - -Add `use axum_extra::extract::CookieJar;` at the top of `routes.rs` imports. - -- [ ] **Step 6: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server 2>&1 | tail -5 -``` - -Expected: all tests pass. - -- [ ] **Step 7: Commit** - -```bash -git add crates/archivr-server/src/routes.rs -git commit -m "feat(auth): login, logout, /me, setup endpoints" -``` - ---- - -## Task 8: API token endpoints - -**Files:** -- Modify: `crates/archivr-server/src/routes.rs` - -- [ ] **Step 1: Write failing test** - -```rust -#[tokio::test] -async fn create_token_requires_auth() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/tokens") - .header("content-type", "application/json") - .body(Body::from(r#"{"name":"my token"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); -} -``` - -- [ ] **Step 2: Add token routes to `app()`** - -```rust -.route("/api/auth/tokens", get(list_tokens).post(create_token)) -.route("/api/auth/tokens/:token_uid", delete(delete_token)) -``` - -- [ ] **Step 3: Add request type** - -```rust -#[derive(Debug, serde::Deserialize)] -struct CreateTokenBody { - name: String, -} -``` - -- [ ] **Step 4: Implement token handlers** - -```rust -async fn create_token( - State(state): State, - auth_user: AuthUser, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - let (user_id, _) = auth_user.require_auth()?; - if body.name.trim().is_empty() { - return Err(ApiError::bad_request("token name is required")); - } - let raw_token = auth::generate_token(); - let token_hash = auth::hash_token(&raw_token); - let conn = database::open_auth_db(&state.auth_db_path)?; - let token_uid = database::create_api_token(&conn, user_id, &token_hash, &body.name)?; - Ok((StatusCode::CREATED, Json(serde_json::json!({ - "token_uid": token_uid, - "raw_token": raw_token, - "name": body.name, - })))) -} - -async fn list_tokens( - State(state): State, - auth_user: AuthUser, -) -> Result>, ApiError> { - let (user_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - Ok(Json(database::list_user_tokens(&conn, user_id)?)) -} - -async fn delete_token( - State(state): State, - auth_user: AuthUser, - Path(token_uid): Path, -) -> Result { - let (user_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - if database::delete_api_token(&conn, &token_uid, user_id)? { - Ok(StatusCode::NO_CONTENT) - } else { - Err(ApiError::not_found("token not found")) - } -} -``` - -- [ ] **Step 5: Derive `serde::Serialize` on `ApiTokenRecord`** - -In `database.rs`, update the struct: - -```rust -#[derive(Debug, Clone, serde::Serialize)] -pub struct ApiTokenRecord { ... } -``` - -- [ ] **Step 6: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server 2>&1 | tail -5 -``` - -Expected: all pass including `create_token_requires_auth`. - -- [ ] **Step 7: Commit** - -```bash -git add crates/archivr-server/src/routes.rs crates/archivr-core/src/database.rs -git commit -m "feat(auth): API token endpoints (create, list, delete)" -``` - ---- - -## Task 9: Route protection for existing routes - -**Files:** -- Modify: `crates/archivr-server/src/routes.rs` - -- [ ] **Step 1: Write failing tests** - -```rust -#[tokio::test] -async fn capture_returns_401_for_unauthenticated() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/api/archives/test/captures") - .header("content-type", "application/json") - .body(Body::from(r#"{"locator":"https://example.com"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); -} -``` - -- [ ] **Step 2: Run to confirm failure** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server capture_returns_401 2>&1 | tail -5 -``` - -Expected: FAILED (currently returns 200 or 404). - -- [ ] **Step 3: Add `AuthUser` to WRITE handlers** - -Update `capture_handler` to require auth: - -```rust -async fn capture_handler( - State(state): State, - auth_user: AuthUser, - Path(archive_id): Path, - Json(body): Json, -) -> Result, ApiError> { - auth_user.require_role(ROLE_USER)?; - // ... rest unchanged -} -``` - -Update `create_tag_handler`, `assign_entry_tag_handler`, `remove_entry_tag_handler` with the same `auth_user: AuthUser` parameter and `auth_user.require_role(ROLE_USER)?` guard. - -- [ ] **Step 4: Update security-boundary comment in `routes.rs`** - -Find the comment block at the top of `routes.rs` (lines 1–23). Replace the route classification list to reflect the new protection: - -```rust -// ── Security Boundary ────────────────────────────────────────────────────── -// -// Route protection tiers: -// STATIC — no auth: GET /, GET /assets/* -// PUBLIC_READ — no auth (visibility filtering deferred to Track 6): -// GET /api/archives, GET /api/archives/:id/entries, etc. -// AUTH — requires login (ROLE_USER bit): -// POST /api/archives/:id/captures -// POST/PUT/DELETE /api/archives/:id/tags -// POST/DELETE /api/archives/:id/entries/:uid/tags -// ADMIN — requires ROLE_ADMIN: -// GET /api/admin/archives (future) -// OWNER — requires ROLE_OWNER: -// instance settings (future) -// AUTH_SELF — no role guard (own resources): -// GET/POST/DELETE /api/auth/tokens -// POST /api/auth/logout -// GET /api/auth/me -// ──────────────────────────────────────────────────────────────────────────── -``` - -- [ ] **Step 5: Run all tests** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-server/src/routes.rs -git commit -m "feat(auth): apply ROLE_USER guard to WRITE routes" -``` - ---- - -## Task 10: Session cleanup background task - -**Files:** -- Modify: `crates/archivr-server/src/main.rs` - -- [ ] **Step 1: Add cleanup task to `main.rs`** - -After the `app` binding and before `let listener`, add: - -```rust -// Spawn session cleanup background task: delete expired sessions at startup -// and every 24 hours. -let cleanup_auth_path = auth_db_path.clone(); -tokio::spawn(async move { - loop { - if let Ok(conn) = archivr_core::database::open_auth_db(&cleanup_auth_path) { - match archivr_core::database::delete_expired_sessions(&conn) { - Ok(n) if n > 0 => eprintln!("info: cleaned up {n} expired sessions"), - Err(e) => eprintln!("warn: session cleanup failed: {e:#}"), - _ => {} - } - } - tokio::time::sleep(tokio::time::Duration::from_secs(24 * 60 * 60)).await; - } -}); -``` - -Add `use archivr_core;` at the top if not already present (it's brought in transitively via routes). - -- [ ] **Step 2: Verify compilation** - -```bash -cd ~/personal/archivr && cargo check -p archivr-server 2>&1 | tail -5 -``` - -Expected: compiles cleanly. - -- [ ] **Step 3: Commit** - -```bash -git add crates/archivr-server/src/main.rs -git commit -m "feat(auth): session cleanup background task (24h interval)" -``` - ---- - -## Task 11: Frontend — auth state, api.js, LoginPage, SetupPage - -**Files:** -- Modify: `frontend/src/App.jsx` -- Modify: `frontend/src/api.js` -- Create: `frontend/src/components/LoginPage.jsx` -- Create: `frontend/src/components/SetupPage.jsx` - -- [ ] **Step 1: Update `api.js`** - -Open `frontend/src/api.js`. Add auth helper functions and the 401 interceptor at the bottom of the file (replace or append to the existing `export` structure): - -```js -// ── Auth helpers ───────────────────────────────────────────────────────────── - -export async function checkSetup() { - const r = await fetch('/api/auth/setup'); - const data = await r.json(); - return data.setup_required === true; -} - -export async function doSetup(username, password) { - const r = await fetch('/api/auth/setup', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - if (!r.ok) throw new Error((await r.json()).error || 'Setup failed'); - return r.json(); -} - -export async function login(username, password) { - const r = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - if (!r.ok) throw new Error((await r.json()).error || 'Login failed'); - return r.json(); // { user_uid, username, role_bits } -} - -export async function logout() { - await fetch('/api/auth/logout', { method: 'POST' }); -} - -export async function fetchMe() { - const r = await fetch('/api/auth/me'); - if (r.status === 401) return null; - return r.json(); -} - -// ── 401 interceptor ─────────────────────────────────────────────────────────── -// Wrap fetch so any 401 dispatches a custom event for App.jsx to handle. -const _origFetch = window.fetch; -window.fetch = async (...args) => { - const r = await _origFetch(...args); - if (r.status === 401) { - const url = typeof args[0] === 'string' ? args[0] : args[0]?.url ?? ''; - // Don't intercept auth endpoints themselves - if (!url.includes('/api/auth/')) { - window.dispatchEvent(new CustomEvent('auth:expired')); - } - } - return r; -}; -``` - -- [ ] **Step 2: Create `LoginPage.jsx`** - -```jsx -import { useState } from 'react'; -import { login } from '../api.js'; - -export default function LoginPage({ onLogin }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function handleSubmit(e) { - e.preventDefault(); - setError(null); - setLoading(true); - try { - const user = await login(username, password); - onLogin(user); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - } - - return ( -
-

Archivr

-
- - - {error &&

{error}

} - -
-
- ); -} -``` - -- [ ] **Step 3: Create `SetupPage.jsx`** - -```jsx -import { useState } from 'react'; -import { doSetup } from '../api.js'; - -export default function SetupPage({ onComplete }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [confirm, setConfirm] = useState(''); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function handleSubmit(e) { - e.preventDefault(); - if (password !== confirm) { - setError('Passwords do not match'); - return; - } - if (password.length < 8) { - setError('Password must be at least 8 characters'); - return; - } - setError(null); - setLoading(true); - try { - await doSetup(username, password); - onComplete(); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - } - - return ( -
-

Welcome to Archivr

-

Create your owner account to get started.

-
- - - - {error &&

{error}

} - -
-
- ); -} -``` - -- [ ] **Step 4: Update `App.jsx`** - -Open `frontend/src/App.jsx`. Add the following at the top (after existing imports): - -```jsx -import { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { checkSetup, fetchMe, logout as apiLogout } from './api.js'; -import LoginPage from './components/LoginPage.jsx'; -import SetupPage from './components/SetupPage.jsx'; - -export const AuthContext = createContext(null); -``` - -Inside the `App` function, add state and effects at the top: - -```jsx -const [authState, setAuthState] = useState('loading'); // 'loading' | 'setup' | 'login' | 'authenticated' -const [currentUser, setCurrentUser] = useState(null); - -useEffect(() => { - (async () => { - const needsSetup = await checkSetup(); - if (needsSetup) { setAuthState('setup'); return; } - const user = await fetchMe(); - if (!user) { setAuthState('login'); return; } - setCurrentUser(user); - setAuthState('authenticated'); - })(); -}, []); - -// Listen for 401s from the fetch interceptor in api.js -useEffect(() => { - const handler = () => { setCurrentUser(null); setAuthState('login'); }; - window.addEventListener('auth:expired', handler); - return () => window.removeEventListener('auth:expired', handler); -}, []); -``` - -In the `App` return, add guards at the very top of the JSX: - -```jsx -if (authState === 'loading') return
Loading…
; -if (authState === 'setup') return setAuthState('login')} />; -if (authState === 'login') return { setCurrentUser(user); setAuthState('authenticated'); }} />; -``` - -Wrap the existing return content in: - -```jsx -return ( - - {/* existing app JSX */} - -); -``` - -- [ ] **Step 5: Build to check for errors** - -```bash -cd ~/personal/archivr/frontend && bun run build 2>&1 | tail -10 -``` - -Expected: builds successfully, no type errors. - -- [ ] **Step 6: Commit** - -```bash -cd ~/personal/archivr -git add frontend/src/App.jsx frontend/src/api.js \ - frontend/src/components/LoginPage.jsx \ - frontend/src/components/SetupPage.jsx -git commit -m "feat(auth): frontend login/setup pages + auth state in App.jsx" -``` - ---- - -## Task 12: Topbar — user menu and logout button - -**Files:** -- Modify: `frontend/src/components/Topbar.jsx` - -- [ ] **Step 1: Update `Topbar.jsx`** - -Open `frontend/src/components/Topbar.jsx`. Import `AuthContext` and `logout`: - -```jsx -import { useContext, useState } from 'react'; -import { AuthContext } from '../App.jsx'; -import { logout as apiLogout } from '../api.js'; -``` - -Inside the component, add: - -```jsx -const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}; -const [loggingOut, setLoggingOut] = useState(false); - -async function handleLogout() { - setLoggingOut(true); - await apiLogout(); - setCurrentUser(null); - window.location.reload(); // simplest way to reset all state -} -``` - -In the JSX, add a user menu at the end of the topbar: - -```jsx -{currentUser && ( -
- {currentUser.username} - -
-)} -``` - -- [ ] **Step 2: Build to check for errors** - -```bash -cd ~/personal/archivr/frontend && bun run build 2>&1 | tail -5 -``` - -Expected: clean build. - -- [ ] **Step 3: Commit** - -```bash -cd ~/personal/archivr -git add frontend/src/components/Topbar.jsx -git commit -m "feat(auth): user menu and logout button in Topbar" -``` - ---- - -## Task 13: Update NEXT.md - -**Files:** -- Modify: `NEXT.md` - -- [ ] **Step 1: Add Track 4 as done, renumber remaining tracks** - -Open `NEXT.md`. Add a new Track 4 section (done) after Track 3, following the same ~~strikethrough~~ ✅ pattern as Tracks 1 and 2. Rename the old Track 4 (Cloud backup) to Track 9 and Track 5 (Cloud storage) to Track 10. Add stub sections for new Tracks 5, 6, 7, 8 with brief descriptions and "pending" status. - -Use this numbering: - -| # | Track | Status | -|---|---|---| -| 1 | Generic URL capture | Done | -| 2 | Web page archiving | Done | -| 3 | Async capture jobs | Pending | -| **4** | **Auth foundation** | **Done (this track)** | -| 5 | User management | Pending | -| 6 | Permissions & visibility | Pending | -| 7 | Settings | Pending | -| 8 | Collections UI | Pending | -| 9 | Cloud backup | Pending (was 4) | -| 10 | Cloud storage | Pending (was 5) | - -- [ ] **Step 2: Commit** - -```bash -git add NEXT.md -git commit -m "docs: mark Track 4 auth foundation done, renumber roadmap tracks" -``` - ---- - -## Final verification - -- [ ] **Run the full test suite** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all tests pass (count will be higher than the pre-implementation baseline due to new tests). - -- [ ] **Build the frontend** - -```bash -cd ~/personal/archivr/frontend && bun run build 2>&1 | tail -5 -``` - -Expected: builds cleanly. diff --git a/docs/superpowers/specs/2026-06-25-auth-foundation-design.md b/docs/superpowers/specs/2026-06-25-auth-foundation-design.md deleted file mode 100644 index 23e74c8..0000000 --- a/docs/superpowers/specs/2026-06-25-auth-foundation-design.md +++ /dev/null @@ -1,403 +0,0 @@ -# Auth Foundation Design - -**Track:** 4 of the roadmap (inserted after Track 3: Async capture jobs) -**Date:** 2026-06-25 -**Status:** Approved for implementation - ---- - -## Context & Roadmap Position - -Archivr is evolving from a local-only tool (single hard-coded user, 127.0.0.1 binding) into a -self-hosted multi-user platform — think ArchiveBox but with real accounts, roles, and -public/private visibility. This track lays the foundation. All subsequent tracks depend on it. - -**Full decomposition:** - -| Track | Scope | Depends on | -|---|---|---| -| 4 (this) | Auth foundation | — | -| 5 | User management — registration, custom roles, admin panel | Track 4 | -| 6 | Permissions & visibility — collection model, per-membership visibility | Track 5 | -| 7 | Settings — account profile, instance-wide toggles | Track 5 | -| 8 | Collections UI | Tracks 5–6 | - ---- - -## Goals - -- Password-protected login with cookie sessions and API tokens -- Role table with bitmask-based visibility (extensible to custom roles in Track 5) -- Auth middleware that protects write/admin routes -- First-run owner setup wizard -- Frontend login page and session-aware API calls - -## Non-Goals (explicitly deferred) - -- Custom role creation UI → Track 5 -- User registration flow → Track 5 -- Visibility enforcement on queries → Track 6 -- Collection model (replacing `archived_entries.visibility`) → Track 6 -- Account settings page → Track 7 -- API token management UI → Track 7 - ---- - -## Schema - -### New table: `roles` - -```sql -CREATE TABLE IF NOT EXISTS roles ( - id INTEGER PRIMARY KEY, - role_uid TEXT NOT NULL UNIQUE, - slug TEXT NOT NULL UNIQUE, -- 'guest', 'user', 'admin', 'owner', or custom - name TEXT NOT NULL, - level INTEGER NOT NULL, -- ordering: guest=0, user=1, admin=3, owner=4 - bit_position INTEGER NOT NULL UNIQUE, -- position in visibility bitmask - is_builtin INTEGER NOT NULL DEFAULT 0 CHECK (is_builtin IN (0, 1)) -); -``` - -**Built-in rows seeded at schema init:** - -| slug | level | bit_position | bit value | is_builtin | -|---|---|---|---|---| -| guest | 0 | 0 | 1 | 1 | -| user | 1 | 1 | 2 | 1 | -| admin | 3 | 2 | 4 | 1 | -| owner | 4 | 3 | 8 | 1 | - -Bit position 2 (value 4) is reserved for `admin`. Bit positions 4+ (values 16, 32, …) are assigned -to custom roles in Track 5. Level 2 is reserved for custom roles sitting between `user` and `admin`. - -**role_bits computation — implicit guest floor:** - -`role_bits` for any **authenticated** user is computed as: -``` -role_bits = ROLE_GUEST | (OR of bit values for all rows in user_roles) -``` -The `ROLE_GUEST` bit (1) is always included for authenticated users so they can access -public (guest-visible) content. Example: an owner assigned only the `owner` role gets -`role_bits = 1 | 8 = 9`, which passes `role_bits & ROLE_USER (2) = 0` — still broken. - -**Therefore, role assignment is cumulative by level.** When a role is assigned, all -built-in roles at lower levels are also assigned: -- Assigning `owner` (level 4) → also assign `admin`, `user` in `user_roles` -- Assigning `admin` (level 3) → also assign `user` in `user_roles` -- Assigning `user` (level 1) → no additional rows -- `guest` is never assigned; it is the implicit unauthenticated floor - -Setup creates owner with three `user_roles` rows: `user`, `admin`, `owner`. -Resulting `role_bits = ROLE_GUEST | ROLE_USER | ROLE_ADMIN | ROLE_OWNER = 1|2|4|8 = 15`. - -**Visibility check:** `viewer.role_bits & content.visibility != 0` passes if the viewer -has any bit the content requires. Owner (15) can see everything. User (1|2=3) can see -guest-visible (1) and user-visible (2) content but not admin-only (4). ✓ - -`is_builtin = 1` rows cannot be deleted. - -### New table: `sessions` - -```sql -CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY, - session_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_bits INTEGER NOT NULL, -- snapshot of bitmask at login time; role changes take effect on next login - created_at TEXT NOT NULL, - last_seen_at TEXT NOT NULL, - expires_at TEXT NOT NULL, -- 30 days from last_seen_at - user_agent TEXT -); -CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); -``` - -### New table: `api_tokens` - -```sql -CREATE TABLE IF NOT EXISTS api_tokens ( - id INTEGER PRIMARY KEY, - token_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of raw token; raw token never stored - name TEXT NOT NULL, - created_at TEXT NOT NULL, - last_used_at TEXT, - expires_at TEXT -- NULL = never expires -); -CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id ON api_tokens(user_id); -``` - -### `users` table — existing, minimally changed - -The existing `role TEXT NOT NULL CHECK (role IN ('admin','user'))` column is **kept but inert** — -auth middleware reads from `user_roles`, not this column. It will be removed in Track 5 cleanup. -`ensure_owner_exists` must supply a value for this column; use `'admin'` as the placeholder. - -`ensure_default_user` is replaced by `ensure_owner_exists` which returns `false` if no owner -row exists in `user_roles` (triggers setup mode). The old local-admin stub is never created on -fresh instances. Session lookup JOINs `users` and checks `users.status = 'active'`; a session -belonging to a disabled user resolves to `AuthUser::Guest`. - -### `instance_settings` — one new column - -The column is added **inside** the existing `CREATE TABLE IF NOT EXISTS instance_settings` DDL, -not via `ALTER TABLE` (which is not idempotent in `initialize_schema`): - -```sql -CREATE TABLE IF NOT EXISTS instance_settings ( - id INTEGER PRIMARY KEY CHECK (id = 1), - public_index_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_index_enabled IN (0, 1)), - public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)), - public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)), - default_entry_visibility INTEGER NOT NULL DEFAULT 2 -- 2 = user-visible by default -); -``` - -The existing `INSERT OR IGNORE INTO instance_settings … VALUES (1, 0, 0, 0)` seed row must be -updated to include the new column: `VALUES (1, 0, 0, 0, 2)`. - -### `archived_entries.visibility` — deprecated, not removed - -Flagged with a `-- DEPRECATED: replaced by collection_entries.visibility in Track 6` comment in -`initialize_schema`. No data migration needed yet; Track 6 handles it. - ---- - -## Auth Flow - -### Login - -``` -POST /api/auth/login -Body: { username: string, password: string } -``` - -1. Look up user by username. -2. Verify password with Argon2id (`argon2` crate). -3. Compute `role_bits = ROLE_GUEST | (OR of bit values for all user_roles rows)` (cumulative; see Schema § role_bits computation). -4. Insert `sessions` row (`session_uid` = UUID, `expires_at` = now + 30 days). -5. Set-Cookie: `session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000`. - Add `Secure` flag when the request arrived over HTTPS (detected via `X-Forwarded-Proto: https` - header or TLS connection info). Omit `Secure` for plain HTTP to support local dev without TLS. -6. Return `200 { user_uid, username, role_bits }`. - -On failure: `401 { error: "invalid_credentials" }` — same message for unknown user and wrong -password (no user enumeration). - -### Logout - -``` -POST /api/auth/logout -``` - -Deletes the `sessions` row for the current session cookie. Responds with -`Set-Cookie: session=; Max-Age=0` to clear the browser cookie. Returns `204`. - -### Current user - -``` -GET /api/auth/me -``` - -Returns `200 { user_uid, username, role_bits }` for an authenticated request, or `401` for a -guest. The frontend calls this once on mount to restore session state. - -### First-run setup - -``` -GET /api/auth/setup → 200 { setup_required: bool } -POST /api/auth/setup → 201 { user_uid, username } -Body: { username: string, password: string } -``` - -`setup_required` is `true` when no user has the `owner` role in `user_roles`. On `POST`: -- If setup is **already complete** (an owner exists): return `409 { error: "already_configured" }`. -- Otherwise: create the user (with `users.role = 'admin'` as placeholder), assign `user_roles` - rows for `user`, `admin`, `owner` (cumulative), seed `instance_settings` row if absent. - Return `201 { user_uid, username }`. Normal login flow applies immediately after. - -All non-setup API routes return `503 { error: "setup_required" }` until setup is complete. -The following routes are **exempt** from the 503 check: `GET /api/auth/setup`, -`POST /api/auth/setup`, `GET /` (static), `GET /assets/*` (static). - -### API tokens - -``` -POST /api/auth/tokens → 201 { token_uid, raw_token, name, created_at } -GET /api/auth/tokens → 200 [{ token_uid, name, created_at, last_used_at }] -DELETE /api/auth/tokens/:token_uid → 204 -``` - -`raw_token` is a cryptographically random 32-byte value, base64url-encoded, returned once. The -server stores only its SHA-256 hash. The management UI for these endpoints is in Track 7; the -endpoints are implemented here. - -### Password hashing - -Argon2id with default parameters from the `argon2` crate (memory=19 MiB, iterations=2, -parallelism=1). The current `"disabled-local-password"` sentinel in `ensure_default_user` becomes -irrelevant once setup is required on fresh instances. - -### Session expiry & cleanup - -`last_seen_at` is updated on every authenticated request using a **conditional update**: the -session row is already read during extraction; if `now() - last_seen_at > 60s`, issue an UPDATE. -This adds no extra query — only an extra UPDATE when the threshold is crossed. -`expires_at` = `last_seen_at + 30 days`, recalculated on each UPDATE. A background task in -`archivr-server/src/main.rs` runs `DELETE FROM sessions WHERE expires_at < now()` at startup -and every 24 hours via `tokio::time::interval`. - ---- - -## Auth Extractor - -New file: `crates/archivr-server/src/auth.rs` - -```rust -pub enum AuthUser { - Guest, - Authenticated { user_id: i64, role_bits: u32 }, -} - -impl AuthUser { - pub fn require_auth(&self) -> Result<(i64, u32), ApiError> // 401 if Guest - pub fn require_role(&self, bit: u32) -> Result<(), ApiError> // 403 if bit not set - pub fn has_role(&self, bit: u32) -> bool -} - -// Role bit constants -pub const ROLE_GUEST: u32 = 1; -pub const ROLE_USER: u32 = 2; -pub const ROLE_ADMIN: u32 = 4; -pub const ROLE_OWNER: u32 = 8; -``` - -Implemented as an Axum `FromRequestParts` extractor. Tries `session` cookie first, then -`Authorization: Bearer` header. -- **Cookie path**: look up `sessions` row JOIN `users` WHERE `session_uid = ?` - AND `users.status = 'active'` AND `expires_at > now()`. Use cached `role_bits` from the - session row. -- **Bearer path**: SHA-256 the token, look up `api_tokens` row JOIN `users` WHERE - `token_hash = ?` AND `users.status = 'active'` AND (`expires_at IS NULL OR expires_at > now()`). - Compute `role_bits` live: `ROLE_GUEST | (OR of user_roles bit values for that user)`. - Update `api_tokens.last_used_at`. -- Missing or invalid credential → `AuthUser::Guest` (never a hard error at extraction time). - ---- - -## Route Protection Tiers - -The existing security-boundary comment block in `routes.rs` is updated: - -| Tier | Requirement | Examples | -|---|---|---| -| `STATIC` | none | `GET /`, `GET /assets/*` | -| `PUBLIC_READ` | none (visibility filtering deferred to Track 6) | `GET /api/archives/:id/entries` | -| `AUTH_READ` | `ROLE_USER` bit | authenticated entry access | -| `WRITE` | `ROLE_USER` bit | `POST /api/archives/:id/captures`, tag mutations | -| `ADMIN` | `ROLE_ADMIN` bit | `GET /api/admin/archives`, user management | -| `OWNER` | `ROLE_OWNER` bit | instance settings, ownership transfer | - -**Error responses:** -- No/invalid session → `401` (frontend redirects to login) -- Valid session, insufficient role → `403` -- Private resource accessed without sufficient role → `404` (do not reveal existence) - -Track 4 applies `ROLE_USER` enforcement to all existing `WRITE` routes and `ROLE_ADMIN` to -`/api/admin/*`. `PUBLIC_READ` routes return all data for now; Track 6 adds visibility filters. - ---- - -## Frontend Changes - -### New components - -| Component | Purpose | -|---|---| -| `SetupPage.jsx` | First-run owner account creation wizard | -| `LoginPage.jsx` | Username/password login form | - -### App.jsx changes - -- On mount: call `GET /api/auth/setup`; if `setup_required`, render `` and nothing else. -- Otherwise: call `GET /api/auth/me`; store result as `currentUser` state (null = guest). -- Pass `currentUser` down via React context (`AuthContext`). -- Any `401` response from any API call sets `currentUser` to null → triggers ``. - -### api.js changes - -- Thin response interceptor: if status is `401`, dispatch a global `auth:expired` event that - `App.jsx` listens to and handles by clearing `currentUser`. -- No token storage in JS — cookies are handled entirely by the browser. - -### Topbar.jsx changes - -- When `currentUser` is set: show `username` and a **Log out** button. -- Log out calls `POST /api/auth/logout`, then clears `currentUser`. - -### What is NOT in Track 4 frontend - -- Settings page (Track 7) -- User management UI (Track 5) -- Role or visibility controls (Track 6) -- API token management UI (Track 7) - ---- - -## New Dependencies - -| Crate | Purpose | -|---|---| -| `argon2` | Password hashing (Argon2id) | -| `rand` | Cryptographically random token generation | -| `tower-cookies` | Cookie extraction in Axum (or use `axum-extra`) | - -Add to `archivr-server/Cargo.toml` and workspace `Cargo.toml` as needed. - ---- - -## Files Changed - -| File | Change | -|---|---| -| `crates/archivr-core/src/database.rs` | Add `roles`, `user_roles`, `sessions`, `api_tokens` tables; seed built-in roles; add `instance_settings.default_entry_visibility`; replace `ensure_default_user` with `ensure_owner_exists`; add session/token CRUD helpers | -| `crates/archivr-server/src/auth.rs` | New: `AuthUser` extractor, role bit constants, session/token lookup | -| `crates/archivr-server/src/routes.rs` | Add auth endpoints (`/api/auth/*`); apply `AuthUser` extractor to WRITE/ADMIN routes; update security-boundary comment | -| `crates/archivr-server/src/main.rs` | Session cleanup background task | -| `frontend/src/App.jsx` | Setup check, auth state, `AuthContext` | -| `frontend/src/api.js` | 401 interceptor | -| `frontend/src/components/LoginPage.jsx` | New | -| `frontend/src/components/SetupPage.jsx` | New | -| `frontend/src/components/Topbar.jsx` | User menu + logout | -| `Cargo.toml` | Add `argon2`, `rand`, `tower-cookies` (or `axum-extra`) | - ---- - -## Test Coverage - -- `database.rs`: role seeding, `ensure_owner_exists`, session CRUD, token hash round-trip -- `auth.rs`: extractor resolves cookie → session → user; extractor resolves Bearer → token → user; - missing credential → Guest; expired session → Guest -- `routes.rs`: login happy path; login wrong password returns 401; logout clears session; - setup endpoint returns 503 after setup complete; WRITE route returns 401 for Guest; - WRITE route returns 403 for insufficient role; setup flow end-to-end - ---- - -## Track Numbering Update for NEXT.md - -Original tracks 4 and 5 shift to 8 and 9. Collections is a named future track (no number until -scoped): - -| # | Track | -|---|---| -| 3 | Async capture jobs | -| **4** | **Auth foundation (this spec)** | -| **5** | **User management** | -| **6** | **Permissions & visibility (collection model)** | -| **7** | **Settings** | -| **8** | **Collections UI** | -| 9 | Cloud backup (was 4) | -| 10 | Cloud storage (was 5) | diff --git a/flake.nix b/flake.nix index c87369b..3ab5b52 100644 --- a/flake.nix +++ b/flake.nix @@ -73,7 +73,6 @@ cargoLock nativeBuildInputs ; - buildInputs = [ pkgs.openssl ]; cargoBuildFlags = [ "-p" "archivr-cli" @@ -91,7 +90,6 @@ cargoLock nativeBuildInputs ; - buildInputs = [ pkgs.openssl ]; cargoBuildFlags = [ "-p" "archivr-server" diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0061113..f663efe 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,41 +1,14 @@ -import { useState, useEffect, useCallback, useRef, createContext } from 'react' -import { fetchArchives, fetchEntries, searchEntries, fetchRuns, fetchTags, checkSetup, fetchMe } from './api' -import LoginPage from './components/LoginPage.jsx' -import SetupPage from './components/SetupPage.jsx' - +import { useState, useEffect, useCallback, useRef } from 'react' +import { fetchArchives, fetchEntries, searchEntries, fetchRuns, fetchTags } from './api' import Topbar from './components/Topbar' import CaptureDialog from './components/CaptureDialog' import EntriesView from './components/EntriesView' import RunsView from './components/RunsView' import AdminView from './components/AdminView' import TagsView from './components/TagsView' -import CollectionsView from './components/CollectionsView' -import SettingsView from './components/SettingsView' import ContextRail from './components/ContextRail' -export const AuthContext = createContext(null); - export default function App() { - const [authState, setAuthState] = useState('loading'); - const [currentUser, setCurrentUser] = useState(null); - - useEffect(() => { - (async () => { - const needsSetup = await checkSetup(); - if (needsSetup) { setAuthState('setup'); return; } - const user = await fetchMe(); - if (!user) { setAuthState('login'); return; } - setCurrentUser(user); - setAuthState('authenticated'); - })(); - }, []); - - useEffect(() => { - const handler = () => { setCurrentUser(null); setAuthState('login'); }; - window.addEventListener('auth:expired', handler); - return () => window.removeEventListener('auth:expired', handler); - }, []); - const [archives, setArchives] = useState([]) const [archiveId, setArchiveId] = useState(null) const [entries, setEntries] = useState([]) @@ -154,89 +127,77 @@ export default function App() { ]) }, [archiveId, searchQuery, tagFilter, loadEntries]) - if (authState === 'loading') return
Loading\u2026
; - if (authState === 'setup') return setAuthState('login')} />; - if (authState === 'login') return { setCurrentUser(user); setAuthState('authenticated'); }} />; - return ( - - <> - -
-
- {view === 'archive' && ( -
- setSearchQuery(e.target.value)} - /> -
- {resultCount} - {tagFilter && ( - - )} -
+ <> + +
+
+ {view === 'archive' && ( +
+ setSearchQuery(e.target.value)} + /> +
+ {resultCount} + {tagFilter && ( + + )}
- )} - {view === 'archive' && ( - - )} - {view === 'runs' && } - {view === 'admin' && } - {view === 'tags' && ( - - )} - {view === 'collections' && ( - - )} - {view === 'settings' && ( - - )} -
- -
- + )} + {view === 'archive' && ( + + )} + {view === 'runs' && } + {view === 'admin' && } + {view === 'tags' && ( + + )} +
+ - - +
+ + ) } diff --git a/frontend/src/api.js b/frontend/src/api.js index 6b17079..0ce034b 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -64,250 +64,7 @@ export async function submitCapture(archiveId, locator) { body: JSON.stringify({ locator }), }); if (!res.ok) { - const body = await res.json().catch(() => ({})); - throw new Error(body.error || `HTTP ${res.status}`); - } - return res.json(); // { job_uid, status: "pending" } -} - -export async function pollCaptureJob(archiveId, jobUid) { - return getJson(`/api/archives/${archiveId}/capture_jobs/${jobUid}`); -} - -// ── Auth helpers ───────────────────────────────────────────────────────────── - -export async function checkSetup() { - const r = await fetch('/api/auth/setup'); - const data = await r.json(); - return data.setup_required === true; -} - -export async function doSetup(username, password) { - const r = await fetch('/api/auth/setup', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - if (!r.ok) throw new Error((await r.json()).error || 'Setup failed'); - return r.json(); -} - -export async function login(username, password) { - const r = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - if (!r.ok) throw new Error((await r.json()).error || 'Login failed'); - return r.json(); -} - -export async function logout() { - await fetch('/api/auth/logout', { method: 'POST' }); -} - -export async function fetchMe() { - const r = await fetch('/api/auth/me'); - if (r.status === 401) return null; - return r.json(); -} - -// ── Profile & settings helpers ─────────────────────────────────────────────── - -export async function updateProfile(displayName) { - const res = await fetch('/api/auth/me', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ display_name: displayName }), - }); - if (!res.ok) throw new Error(await res.text()); -} - -export async function changePassword(currentPassword, newPassword) { - const res = await fetch('/api/auth/me', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }), - }); - if (!res.ok) { - let msg = await res.text(); - try { msg = JSON.parse(msg).error ?? msg; } catch {} - throw new Error(msg); + const msg = await res.text(); + throw new Error(msg || `HTTP ${res.status}`); } } - -export async function listTokens() { - return getJson('/api/auth/tokens'); -} - -export async function createToken(name) { - const res = await fetch('/api/auth/tokens', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), - }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -export async function deleteToken(tokenUid) { - const res = await fetch(`/api/auth/tokens/${tokenUid}`, { method: 'DELETE' }); - if (!res.ok) throw new Error(await res.text()); -} - -export async function getInstanceSettings() { - return getJson('/api/admin/instance-settings'); -} - -export async function updateInstanceSettings(patch) { - const res = await fetch('/api/admin/instance-settings', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(patch), - }); - if (!res.ok) throw new Error(await res.text()); -} - -// ── Admin helpers ───────────────────────────────────────────────────────────── - -export async function listAdminUsers() { - return getJson('/api/admin/users'); -} - -export async function createAdminUser(username, password, email) { - const res = await fetch('/api/admin/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password, email: email || undefined }), - }); - if (!res.ok) { const b = await res.json().catch(() => ({})); throw new Error(b.error || `HTTP ${res.status}`); } - return res.json(); -} - -export async function setUserStatus(userUid, status) { - const res = await fetch(`/api/admin/users/${userUid}/status`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ status }), - }); - if (!res.ok) { const b = await res.json().catch(() => ({})); throw new Error(b.error || `HTTP ${res.status}`); } - return res.json(); -} - -export async function assignRole(userUid, roleSlug) { - const res = await fetch(`/api/admin/users/${userUid}/roles`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ role_slug: roleSlug }), - }); - if (!res.ok) { const b = await res.json().catch(() => ({})); throw new Error(b.error || `HTTP ${res.status}`); } -} - -export async function removeRole(userUid, roleSlug) { - const res = await fetch(`/api/admin/users/${userUid}/roles/${encodeURIComponent(roleSlug)}`, { - method: 'DELETE', - }); - if (!res.ok && res.status !== 204) { const b = await res.json().catch(() => ({})); throw new Error(b.error || `HTTP ${res.status}`); } -} - -export async function listRoles() { - return getJson('/api/admin/roles'); -} - -export async function createRole(slug, name) { - const res = await fetch('/api/admin/roles', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ slug, name }), - }); - if (!res.ok) { const b = await res.json().catch(() => ({})); throw new Error(b.error || `HTTP ${res.status}`); } - return res.json(); -} - -// ── Collection helpers ───────────────────────────────────────────────────── - -export async function listCollections(archiveId) { - return getJson(`/api/archives/${archiveId}/collections`); -} - -export async function createCollection(archiveId, name, slug, defaultVisibilityBits = 2) { - const res = await fetch(`/api/archives/${archiveId}/collections`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ name, slug, default_visibility_bits: defaultVisibilityBits }), - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); - } - return res.json(); -} - -export async function getCollection(archiveId, collUid) { - return getJson(`/api/archives/${archiveId}/collections/${collUid}`); -} - -export async function addEntryToCollection(archiveId, collUid, entryUid, visibilityBits = 2) { - const res = await fetch(`/api/archives/${archiveId}/collections/${collUid}/entries`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ entry_uid: entryUid, visibility_bits: visibilityBits }), - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); - } -} - -export async function removeEntryFromCollection(archiveId, collUid, entryUid) { - const res = await fetch(`/api/archives/${archiveId}/collections/${collUid}/entries/${entryUid}`, { - method: 'DELETE', - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); - } -} - -export async function updateEntryVisibility(archiveId, collUid, entryUid, visibilityBits) { - const res = await fetch(`/api/archives/${archiveId}/collections/${collUid}/entries/${entryUid}`, { - method: 'PATCH', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ visibility_bits: visibilityBits }), - }); - if (!res.ok) { - const err = await res.json().catch(() => ({ error: res.statusText })); - throw new Error(err.error || res.statusText); - } -} - -export async function listEntryCollections(archiveId, entryUid) { - return getJson(`/api/archives/${archiveId}/entries/${entryUid}/collections`); -} - -export async function updateCollection(archiveId, collUid, patch) { - const res = await fetch(`/api/archives/${archiveId}/collections/${collUid}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(patch), - }) - if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error ?? res.statusText) } -} - -export async function deleteCollection(archiveId, collUid) { - const res = await fetch(`/api/archives/${archiveId}/collections/${collUid}`, { method: 'DELETE' }) - if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error ?? res.statusText) } -} - -// ── 401 interceptor ─────────────────────────────────────────────────────────── -const _origFetch = window.fetch; -window.fetch = async (...args) => { - const r = await _origFetch(...args); - if (r.status === 401) { - const url = typeof args[0] === 'string' ? args[0] : args[0]?.url ?? ''; - if (!url.includes('/api/auth/')) { - window.dispatchEvent(new CustomEvent('auth:expired')); - } - } - return r; -}; diff --git a/frontend/src/components/AdminView.jsx b/frontend/src/components/AdminView.jsx index 7abd8b2..f9520c2 100644 --- a/frontend/src/components/AdminView.jsx +++ b/frontend/src/components/AdminView.jsx @@ -1,218 +1,15 @@ -import { useState, useEffect, useContext, useCallback } from 'react' -import { AuthContext } from '../App.jsx' -import { - listAdminUsers, createAdminUser, setUserStatus, assignRole, removeRole, - listRoles, createRole -} from '../api.js' - -const ROLE_ADMIN = 4 - export default function AdminView({ archives }) { - const { currentUser } = useContext(AuthContext) ?? {} - const isAdmin = currentUser && (currentUser.role_bits & ROLE_ADMIN) !== 0 - - const [tab, setTab] = useState('users') - const [users, setUsers] = useState([]) - const [roles, setRoles] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - // Create user form state - const [newUsername, setNewUsername] = useState('') - const [newPassword, setNewPassword] = useState('') - const [newEmail, setNewEmail] = useState('') - const [createError, setCreateError] = useState(null) - const [creating, setCreating] = useState(false) - - // Create role form state - const [newRoleSlug, setNewRoleSlug] = useState('') - const [newRoleName, setNewRoleName] = useState('') - const [roleCreateError, setRoleCreateError] = useState(null) - const [creatingRole, setCreatingRole] = useState(false) - - const refresh = useCallback(async () => { - if (!isAdmin) return - setLoading(true) - setError(null) - try { - const [u, r] = await Promise.all([listAdminUsers(), listRoles()]) - setUsers(u) - setRoles(r) - } catch (e) { - setError(e.message) - } finally { - setLoading(false) - } - }, [isAdmin]) - - useEffect(() => { refresh() }, [refresh]) - - async function handleToggleStatus(user) { - const next = user.status === 'active' ? 'disabled' : 'active' - try { - await setUserStatus(user.user_uid, next) - setUsers(us => us.map(u => u.user_uid === user.user_uid ? { ...u, status: next } : u)) - } catch (e) { - setError(e.message) - } - } - - async function handleCreateUser(e) { - e.preventDefault() - if (!newUsername.trim() || !newPassword) { setCreateError('Username and password required'); return } - setCreating(true) - setCreateError(null) - try { - await createAdminUser(newUsername.trim(), newPassword, newEmail.trim() || undefined) - setNewUsername('') - setNewPassword('') - setNewEmail('') - await refresh() - } catch (e) { - setCreateError(e.message) - } finally { - setCreating(false) - } - } - - async function handleCreateRole(e) { - e.preventDefault() - if (!newRoleSlug.trim() || !newRoleName.trim()) { setRoleCreateError('Slug and name required'); return } - setCreatingRole(true) - setRoleCreateError(null) - try { - await createRole(newRoleSlug.trim(), newRoleName.trim()) - setNewRoleSlug('') - setNewRoleName('') - await refresh() - } catch (e) { - setRoleCreateError(e.message) - } finally { - setCreatingRole(false) - } - } - - if (!isAdmin) { - return ( -
-

Admin

-

You need admin privileges to access this panel.

-

Mounted Archives

-
- {archives.map(a => ( -
- {a.label} -
{a.archive_path}
-
- ))} -
-
- ) - } - return (
-

Admin

- -
- - - -
- - {error &&
{error}
} - - {tab === 'users' && ( -
-

Users

- {loading ?

Loading…

: ( - - - - - - {users.map(u => ( - - - - - - - - ))} - -
UsernameEmailRolesStatusActions
{u.username}{u.email || '—'}{u.role_slugs.join(', ') || '—'}{u.status} - -
- )} - -

Create User

-
- setNewUsername(e.target.value)} required /> - setNewPassword(e.target.value)} required /> - setNewEmail(e.target.value)} /> - {createError &&
{createError}
} - -
-
- )} - - {tab === 'roles' && ( -
-

Roles

- {loading ?

Loading…

: ( - - - - - - {roles.map(r => ( - - - - - - - - ))} - -
SlugNameLevelBitBuilt-in
{r.slug}{r.name}{r.level}{r.bit_position}{r.is_builtin ? '✓' : ''}
- )} - -

Create Custom Role

-
- setNewRoleSlug(e.target.value)} required /> - setNewRoleName(e.target.value)} required /> - {roleCreateError &&
{roleCreateError}
} - -
-
- )} - - {tab === 'archives' && ( -
-

Mounted Archives

-
- {archives.map(a => ( -
- {a.label} -
{a.archive_path}
-
- ))} +

Mounted Archives

+
+ {archives.map(archive => ( +
+ {archive.label} +
{archive.archive_path}
-
- )} + ))} +
) } diff --git a/frontend/src/components/CaptureDialog.jsx b/frontend/src/components/CaptureDialog.jsx index 9942a71..9a6c73c 100644 --- a/frontend/src/components/CaptureDialog.jsx +++ b/frontend/src/components/CaptureDialog.jsx @@ -1,21 +1,16 @@ import { useRef, useEffect, useState } from 'react' -import { submitCapture, pollCaptureJob } from '../api' +import { submitCapture } from '../api' export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) { const dialogRef = useRef(null) const [locator, setLocator] = useState('') const [error, setError] = useState(null) const [busy, setBusy] = useState(false) - const [jobStatus, setJobStatus] = useState(null) // null | 'running' | 'completed' | 'failed' - const pollRef = useRef(null) useEffect(() => { const dialog = dialogRef.current if (!dialog) return - const handleClose = () => { - clearInterval(pollRef.current) - onClose() - } + const handleClose = () => onClose() dialog.addEventListener('close', handleClose) return () => dialog.removeEventListener('close', handleClose) }, [onClose]) @@ -26,9 +21,6 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) if (open) { setLocator('') setError(null) - setJobStatus(null) - setBusy(false) - clearInterval(pollRef.current) if (!dialog.open) dialog.showModal() } else { if (dialog.open) dialog.close() @@ -39,47 +31,17 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) if (!locator.trim()) { setError('Enter a locator.'); return } setBusy(true) setError(null) - setJobStatus(null) try { - const job = await submitCapture(archiveId, locator.trim()) - setJobStatus('running') - pollRef.current = setInterval(async () => { - try { - const updated = await pollCaptureJob(archiveId, job.job_uid) - if (updated.status === 'completed') { - clearInterval(pollRef.current) - pollRef.current = null - setBusy(false) - setJobStatus('completed') - dialogRef.current?.close() - onCaptured() - } else if (updated.status === 'failed') { - clearInterval(pollRef.current) - pollRef.current = null - setBusy(false) - setJobStatus('failed') - setError(updated.error_text || 'Capture failed.') - } - // pending / running: keep polling - } catch (pollErr) { - clearInterval(pollRef.current) - pollRef.current = null - setBusy(false) - setError(pollErr.message) - } - }, 500) + await submitCapture(archiveId, locator.trim()) + dialogRef.current?.close() + onCaptured() } catch (e) { setError(e.message) + } finally { setBusy(false) } } - function buttonLabel() { - if (!busy) return 'Capture' - if (jobStatus === 'running') return 'Running\u2026' - return 'Capturing\u2026' - } - return (
@@ -94,7 +56,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })
diff --git a/frontend/src/components/CollectionsView.jsx b/frontend/src/components/CollectionsView.jsx deleted file mode 100644 index 48e4271..0000000 --- a/frontend/src/components/CollectionsView.jsx +++ /dev/null @@ -1,366 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react' -import { - listCollections, createCollection, getCollection, - addEntryToCollection, removeEntryFromCollection, - updateEntryVisibility, updateCollection, deleteCollection, -} from '../api.js' - -const VIS_OPTIONS = [ - { value: 0, label: 'Private' }, - { value: 2, label: 'Users only' }, - { value: 3, label: 'Public' }, -] -const VIS_LABEL = v => VIS_OPTIONS.find(o => o.value === v)?.label ?? String(v) - -export default function CollectionsView({ archiveId }) { - const [collections, setCollections] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [selectedUid, setSelectedUid] = useState(null) - const [collDetail, setCollDetail] = useState(null) - const [detailLoading, setDetailLoading] = useState(false) - const [detailError, setDetailError] = useState(null) - - // Create form - const [newName, setNewName] = useState('') - const [newSlug, setNewSlug] = useState('') - const [newVis, setNewVis] = useState(2) - const [creating, setCreating] = useState(false) - const [createError, setCreateError] = useState(null) - - // Add-entry form - const [addUid, setAddUid] = useState('') - const [addVis, setAddVis] = useState(2) - const [adding, setAdding] = useState(false) - const [addError, setAddError] = useState(null) - - // Inline rename state - const [renaming, setRenaming] = useState(false) - const [renameVal, setRenameVal] = useState('') - const renameRef = useRef(null) - - const selected = collections.find(c => c.collection_uid === selectedUid) ?? null - const isDefault = selected?.slug === '_default_' - - const refreshList = useCallback(async () => { - if (!archiveId) return - setLoading(true) - setError(null) - try { - const cols = await listCollections(archiveId) - setCollections(cols) - } catch (e) { - setError(e.message) - } finally { - setLoading(false) - } - }, [archiveId]) - - const refreshDetail = useCallback(async (uid) => { - if (!uid) { setCollDetail(null); return } - setDetailLoading(true) - setDetailError(null) - try { - const d = await getCollection(archiveId, uid) - setCollDetail(d) - } catch (e) { - setDetailError(e.message) - } finally { - setDetailLoading(false) - } - }, [archiveId]) - - useEffect(() => { refreshList() }, [refreshList]) - useEffect(() => { refreshDetail(selectedUid) }, [selectedUid, refreshDetail]) - - // Auto-focus rename input - useEffect(() => { if (renaming && renameRef.current) renameRef.current.focus() }, [renaming]) - - async function handleCreate(e) { - e.preventDefault() - const name = newName.trim() - const slug = newSlug.trim() - if (!name || !slug) return - setCreating(true) - setCreateError(null) - try { - const coll = await createCollection(archiveId, name, slug, newVis) - setNewName('') - setNewSlug('') - setNewVis(2) - await refreshList() - setSelectedUid(coll.collection_uid) - } catch (err) { - setCreateError(err.message) - } finally { - setCreating(false) - } - } - - async function handleRenameCommit() { - const name = renameVal.trim() - if (!name || !selected) { setRenaming(false); return } - try { - await updateCollection(archiveId, selected.collection_uid, { name }) - await refreshList() - setCollDetail(d => d ? { ...d, name } : d) - } catch (e) { - setError(e.message) - } finally { - setRenaming(false) - } - } - - async function handleVisChange(newVisVal) { - if (!selected) return - try { - await updateCollection(archiveId, selected.collection_uid, { default_visibility_bits: newVisVal }) - await refreshList() - setCollDetail(d => d ? { ...d, default_visibility_bits: newVisVal } : d) - } catch (e) { - setError(e.message) - } - } - - async function handleDelete() { - if (!selected) return - if (!window.confirm(`Delete collection "${selected.name}"? Entries will not be deleted.`)) return - try { - await deleteCollection(archiveId, selected.collection_uid) - setSelectedUid(null) - setCollDetail(null) - await refreshList() - } catch (e) { - setError(e.message) - } - } - - async function handleAddEntry(e) { - e.preventDefault() - const uid = addUid.trim() - if (!uid || !selected) return - setAdding(true) - setAddError(null) - try { - await addEntryToCollection(archiveId, selected.collection_uid, uid, addVis) - setAddUid('') - await refreshDetail(selected.collection_uid) - } catch (err) { - setAddError(err.message) - } finally { - setAdding(false) - } - } - - async function handleRemoveEntry(entryUid) { - if (!selected) return - try { - await removeEntryFromCollection(archiveId, selected.collection_uid, entryUid) - await refreshDetail(selected.collection_uid) - } catch (e) { - setDetailError(e.message) - } - } - - async function handleEntryVisChange(entryUid, vis) { - if (!selected) return - try { - await updateEntryVisibility(archiveId, selected.collection_uid, entryUid, vis) - setCollDetail(d => d ? { - ...d, - entries: d.entries.map(en => - en.entry_uid === entryUid ? { ...en, collection_visibility_bits: vis } : en - ), - } : d) - } catch (e) { - setDetailError(e.message) - } - } - - if (!archiveId) return
Select an archive.
- - return ( -
-

Collections

- - {loading &&
Loading…
} - {error &&
{error}
} - -
- {/* Sidebar */} -
- {collections.map(c => ( - - ))} - {collections.length === 0 && !loading && ( -
No collections yet.
- )} -
- - {/* Detail pane */} - {selected ? ( -
- {/* Header */} -
- {renaming ? ( - setRenameVal(e.target.value)} - onBlur={handleRenameCommit} - onKeyDown={e => { - if (e.key === 'Enter') handleRenameCommit() - if (e.key === 'Escape') setRenaming(false) - }} - /> - ) : ( -

{ if (!isDefault) { setRenameVal(selected.name); setRenaming(true) } }} - > - {collDetail?.name ?? selected.name} - {!isDefault && } -

- )} - {!isDefault && ( - - )} -
- - {/* Visibility */} -
- Default visibility - -
- - {/* Entries */} -
-
Entries
- {detailLoading &&
Loading…
} - {detailError &&
{detailError}
} - {!detailLoading && collDetail && ( - collDetail.entries.length === 0 ? ( -
No entries in this collection.
- ) : ( -
    - {collDetail.entries.map(entry => ( -
  • -
    - {entry.title || entry.entry_uid} - {entry.source_kind} -
    -
    - - {!isDefault && ( - - )} -
    -
  • - ))} -
- ) - )} -
- - {/* Add entry (non-default only) */} - {!isDefault && ( -
-
Add entry
-
- setAddUid(e.target.value)} - placeholder="entry_uid" - required - /> - - -
- {addError &&
{addError}
} -
- )} -
- ) : ( -
-
Select a collection to view details.
-
- )} -
- - {/* Create form */} -
- Create collection -
- - - - {createError &&
{createError}
} - -
-
-
- ) -} diff --git a/frontend/src/components/ContextRail.jsx b/frontend/src/components/ContextRail.jsx index b94c6a5..ff497fc 100644 --- a/frontend/src/components/ContextRail.jsx +++ b/frontend/src/components/ContextRail.jsx @@ -1,12 +1,11 @@ import { useState, useEffect, useRef } from 'react' -import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag, listEntryCollections } from '../api' +import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag } from '../api' import { formatTimestamp, formatBytes, valueText } from '../utils' export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, tagNodes, onTagsRefresh }) { const [detail, setDetail] = useState(null) const [tags, setTags] = useState([]) const [assignInput, setAssignInput] = useState('') - const [entryCollections, setEntryCollections] = useState([]) const [assignError, setAssignError] = useState('') const selectSeqRef = useRef(0) @@ -14,7 +13,6 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, if (!selectedEntry || !archiveId) { setDetail(null) setTags([]) - setEntryCollections([]) return } const seq = ++selectSeqRef.current @@ -23,12 +21,10 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, Promise.all([ fetchEntryDetail(archiveId, selectedEntry.entry_uid), fetchEntryTags(archiveId, selectedEntry.entry_uid), - listEntryCollections(archiveId, selectedEntry.entry_uid), - ]).then(([det, tgs, ecs]) => { + ]).then(([det, tgs]) => { if (seq !== selectSeqRef.current) return setDetail(det) setTags(tgs) - setEntryCollections(ecs) }).catch(() => {}) }, [selectedEntry, archiveId]) @@ -160,19 +156,6 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, )} - {selectedEntry && entryCollections.length > 0 && ( -
-
Collections
- {entryCollections.map(c => ( -
- {c.collection_uid}:{' '} - - {{ 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' }[c.visibility_bits] ?? `bits:${c.visibility_bits}`} - -
- ))} -
- )} )} diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx deleted file mode 100644 index 105ed5a..0000000 --- a/frontend/src/components/LoginPage.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useState } from 'react'; -import { login } from '../api.js'; - -export default function LoginPage({ onLogin }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function handleSubmit(e) { - e.preventDefault(); - setError(null); - setLoading(true); - try { - const user = await login(username, password); - onLogin(user); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - } - - return ( -
-

Archivr

-
- - - {error &&

{error}

} - -
-
- ); -} diff --git a/frontend/src/components/SettingsView.jsx b/frontend/src/components/SettingsView.jsx deleted file mode 100644 index 37e3a9f..0000000 --- a/frontend/src/components/SettingsView.jsx +++ /dev/null @@ -1,271 +0,0 @@ -import { useState, useEffect, useContext, useCallback } from 'react' -import { AuthContext } from '../App.jsx' -import { - updateProfile, changePassword, - listTokens, createToken, deleteToken, - getInstanceSettings, updateInstanceSettings, -} from '../api.js' - -const ROLE_ADMIN = 4 -const ROLE_OWNER = 8 - -export default function SettingsView() { - const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {} - const isAdmin = currentUser && ((currentUser.role_bits & ROLE_ADMIN) !== 0) - const [tab, setTab] = useState('profile') - - return ( -
-

Settings

-
- {['profile', 'tokens', ...(isAdmin ? ['instance'] : [])].map(t => ( - - ))} -
- - {tab === 'profile' && } - {tab === 'tokens' && } - {tab === 'instance' && isAdmin && } -
- ) -} - -function ProfileTab({ currentUser, setCurrentUser }) { - const [displayName, setDisplayName] = useState(currentUser?.display_name ?? '') - const [saving, setSaving] = useState(false) - const [saveMsg, setSaveMsg] = useState(null) - - const [curPw, setCurPw] = useState('') - const [newPw, setNewPw] = useState('') - const [confirmPw, setConfirmPw] = useState('') - const [pwSaving, setPwSaving] = useState(false) - const [pwMsg, setPwMsg] = useState(null) - - async function handleSaveProfile(e) { - e.preventDefault() - setSaving(true) - setSaveMsg(null) - try { - await updateProfile(displayName) - setCurrentUser(u => ({ ...u, display_name: displayName || null })) - setSaveMsg({ ok: true, text: 'Saved.' }) - } catch (err) { - setSaveMsg({ ok: false, text: err.message }) - } finally { - setSaving(false) - } - } - - async function handleChangePassword(e) { - e.preventDefault() - if (newPw !== confirmPw) { setPwMsg({ ok: false, text: 'Passwords do not match.' }); return } - setPwSaving(true) - setPwMsg(null) - try { - await changePassword(curPw, newPw) - setCurPw(''); setNewPw(''); setConfirmPw('') - setPwMsg({ ok: true, text: 'Password changed.' }) - } catch (err) { - setPwMsg({ ok: false, text: err.message }) - } finally { - setPwSaving(false) - } - } - - return ( -
-
-

Display Name

-
- setDisplayName(e.target.value)} /> - {saveMsg && ( -
{saveMsg.text}
- )} - -
-
- -
-

Change Password

-
- setCurPw(e.target.value)} required /> - setNewPw(e.target.value)} required /> - setConfirmPw(e.target.value)} required /> - {pwMsg && ( -
{pwMsg.text}
- )} - -
-
-
- ) -} - -function TokensTab() { - const [tokens, setTokens] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [newName, setNewName] = useState('') - const [creating, setCreating] = useState(false) - const [newToken, setNewToken] = useState(null) // { raw_token, name } - - const refresh = useCallback(async () => { - setLoading(true); setError(null) - try { setTokens(await listTokens()) } - catch (e) { setError(e.message) } - finally { setLoading(false) } - }, []) - - useEffect(() => { refresh() }, [refresh]) - - async function handleCreate(e) { - e.preventDefault() - if (!newName.trim()) return - setCreating(true) - try { - const tok = await createToken(newName.trim()) - setNewToken(tok) - setNewName('') - refresh() - } catch (err) { - setError(err.message) - } finally { - setCreating(false) - } - } - - async function handleDelete(tokenUid) { - try { - await deleteToken(tokenUid) - setTokens(ts => ts.filter(t => t.token_uid !== tokenUid)) - } catch (err) { - setError(err.message) - } - } - - return ( -
-

API Tokens

- {newToken && ( -
- Token created. Copy it now \u2014 it will not be shown again.
- - {newToken.raw_token} - - -
- )} -
- setNewName(e.target.value)} style={{ flex: 1 }} required /> - -
- {error &&
{error}
} - {loading ?
Loading\u2026
: ( -
- {tokens.length === 0 &&
No tokens yet.
} - {tokens.map(tok => ( -
-
- {tok.name} -
- Created {tok.created_at.slice(0, 10)} - {tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`} -
-
- -
- ))} -
- )} -
- ) -} - -function InstanceTab() { - const [settings, setSettings] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [saving, setSaving] = useState(false) - const [saveMsg, setSaveMsg] = useState(null) - - useEffect(() => { - (async () => { - try { setSettings(await getInstanceSettings()) } - catch (e) { setError(e.message) } - finally { setLoading(false) } - })() - }, []) - - async function handleSave(e) { - e.preventDefault() - setSaving(true); setSaveMsg(null) - try { - await updateInstanceSettings(settings) - setSaveMsg({ ok: true, text: 'Saved.' }) - } catch (err) { - setSaveMsg({ ok: false, text: err.message }) - } finally { - setSaving(false) - } - } - - if (loading) return
Loading\u2026
- if (error) return
{error}
- if (!settings) return null - - return ( -
-

Instance Settings

-
-
- {[ - ['public_index_enabled', 'Public index (unauthenticated browsing)'], - ['public_entry_content_enabled', 'Public entry content'], - ['open_registration_enabled', 'Open registration'], - ].map(([key, label]) => ( - - ))} -
- - -
-
- {saveMsg &&
{saveMsg.text}
} - -
-
- ) -} diff --git a/frontend/src/components/SetupPage.jsx b/frontend/src/components/SetupPage.jsx deleted file mode 100644 index aca4cb3..0000000 --- a/frontend/src/components/SetupPage.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useState } from 'react'; -import { doSetup } from '../api.js'; - -export default function SetupPage({ onComplete }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [confirm, setConfirm] = useState(''); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function handleSubmit(e) { - e.preventDefault(); - if (password !== confirm) { - setError('Passwords do not match'); - return; - } - if (password.length < 8) { - setError('Password must be at least 8 characters'); - return; - } - setError(null); - setLoading(true); - try { - await doSetup(username, password); - onComplete(); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - } - - return ( -
-

Welcome to Archivr

-

Create your owner account to get started.

-
- - - - {error &&

{error}

} - -
-
- ); -} diff --git a/frontend/src/components/Topbar.jsx b/frontend/src/components/Topbar.jsx index e188cec..b81d72a 100644 --- a/frontend/src/components/Topbar.jsx +++ b/frontend/src/components/Topbar.jsx @@ -1,18 +1,4 @@ -import { useContext, useState } from 'react'; -import { AuthContext } from '../App.jsx'; -import { logout as apiLogout } from '../api.js'; - export default function Topbar({ archives, archiveId, onArchiveChange, view, onViewChange, onCaptureClick }) { - const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}; - const [loggingOut, setLoggingOut] = useState(false); - - async function handleLogout() { - setLoggingOut(true); - await apiLogout(); - setCurrentUser(null); - window.location.reload(); - } - return (
Archivr
@@ -21,7 +7,7 @@ export default function Topbar({ archives, archiveId, onArchiveChange, view, onV {archives.map(a => )} - {currentUser && ( -
- {currentUser.display_name || currentUser.username} - - -
- )}
- ); + ) } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d5a2470..8916da2 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -640,291 +640,3 @@ select { opacity: 0.45; cursor: default; } - -/* ── Collections view ─────────────────────────────────────────────────────── */ - -.collections-view { - padding: 24px; -} - -.collections-heading { - margin: 0 0 16px; - font-size: 22px; - font-family: Georgia, "Times New Roman", serif; -} - -.collections-error { - color: var(--accent); - font-size: 13px; - margin-bottom: 8px; -} - -.coll-dismiss { - background: none; - border: none; - cursor: pointer; - color: var(--accent); - font-size: 16px; - line-height: 1; - padding: 0 4px; -} - -.collections-layout { - display: grid; - grid-template-columns: 220px 1fr; - gap: 0; - border: 1px solid var(--line); - min-height: 340px; -} - -.collections-sidebar { - border-right: 1px solid var(--line); - overflow-y: auto; -} - -.coll-row { - display: flex; - flex-direction: column; - width: 100%; - padding: 10px 14px; - border: none; - border-bottom: 1px solid var(--line-soft); - background: none; - cursor: pointer; - text-align: left; - gap: 2px; -} - -.coll-row:hover { - background: var(--paper-2); -} - -.coll-row.is-active { - background: var(--paper-2); - border-left: 3px solid var(--accent); -} - -.coll-row-name { - font-size: 14px; - font-weight: 600; - color: var(--ink); -} - -.coll-row-meta { - font-size: 11px; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.coll-detail { - padding: 20px 24px; - display: flex; - flex-direction: column; - gap: 16px; -} - -.coll-detail--empty { - justify-content: center; - align-items: center; -} - -.coll-detail-header { - display: flex; - align-items: center; - gap: 12px; -} - -.coll-detail-name { - margin: 0; - font-size: 18px; - font-family: Georgia, "Times New Roman", serif; - flex: 1; -} - -.coll-detail-name--editable { - cursor: pointer; -} - -.coll-detail-name--editable:hover { - color: var(--link); -} - -.coll-edit-hint { - font-size: 14px; - opacity: 0.5; - margin-left: 4px; -} - -.coll-rename-input { - flex: 1; - font-size: 16px; - font-family: Georgia, "Times New Roman", serif; - border: 2px solid var(--ink); - padding: 4px 8px; - background: var(--paper-3); - color: var(--ink); -} - -.coll-delete-btn { - border: 1px solid var(--accent); - background: none; - color: var(--accent); - padding: 5px 12px; - font-size: 13px; - cursor: pointer; -} - -.coll-delete-btn:hover { - background: var(--accent); - color: var(--paper); -} - -.coll-detail-vis { - display: flex; - align-items: center; - gap: 10px; -} - -.coll-vis-label { - font-size: 13px; - font-weight: 600; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.04em; - white-space: nowrap; -} - -.coll-vis-select { - border: 1px solid var(--line); - background: var(--paper-3); - color: var(--ink); - padding: 4px 8px; - font-size: 13px; -} - -.coll-section-heading { - font-size: 11px; - font-weight: 700; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.06em; - margin-bottom: 8px; -} - -.coll-entries-section { - flex: 1; -} - -.coll-entries-list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 2px; -} - -.coll-entry-row { - display: flex; - align-items: center; - gap: 10px; - padding: 7px 0; - border-bottom: 1px solid var(--line-soft); -} - -.coll-entry-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 2px; -} - -.coll-entry-title { - font-size: 14px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.coll-entry-kind { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.coll-entry-actions { - display: flex; - align-items: center; - gap: 6px; - flex-shrink: 0; -} - -.coll-entry-vis-select { - border: 1px solid var(--line); - background: var(--paper-3); - color: var(--ink); - padding: 3px 6px; - font-size: 12px; -} - -.coll-entry-remove { - border: none; - background: none; - cursor: pointer; - color: var(--muted); - font-size: 18px; - line-height: 1; - padding: 0 4px; -} - -.coll-entry-remove:hover { - color: var(--accent); -} - -.coll-add-entry-form { - border-top: 1px solid var(--line-soft); - padding-top: 12px; -} - -.coll-add-entry-row { - display: flex; - gap: 8px; - align-items: center; -} - -.coll-add-entry-input { - flex: 1; - border: 1px solid var(--line); - background: var(--paper-3); - color: var(--ink); - padding: 6px 10px; - font-size: 13px; - min-width: 0; -} - -.coll-add-btn { - border: none; - background: var(--ink); - color: var(--paper); - padding: 6px 14px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - white-space: nowrap; -} - -.coll-add-btn:hover { - opacity: 0.85; -} - -.coll-add-btn:disabled { - opacity: 0.45; - cursor: default; -} - -.coll-create-details summary { - font-weight: 600; - cursor: pointer; -}