diff --git a/.gitignore b/.gitignore index 2240c85..cc251a7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ !docs/ !docs/LICENSE !docs/README* - !crates !crates/** @@ -18,6 +17,7 @@ !Cargo.toml !Cargo.lock !ARCHIVR-MENTAL-MODEL.md +!NEXT.md !frontend/ !frontend/** diff --git a/Cargo.lock b/Cargo.lock index 9ce1bd3..a0729aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,7 +125,13 @@ version = "0.1.0" dependencies = [ "anyhow", "archivr-core", + "argon2", "axum", + "axum-extra", + "base64", + "chrono", + "rand", + "rusqlite", "serde", "serde_json", "tempfile", @@ -135,6 +141,18 @@ 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" @@ -213,18 +231,57 @@ 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" @@ -321,6 +378,17 @@ 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" @@ -366,6 +434,12 @@ 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" @@ -374,6 +448,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -979,6 +1054,23 @@ 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" @@ -996,6 +1088,12 @@ 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" @@ -1060,6 +1158,17 @@ 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" @@ -1087,6 +1196,21 @@ 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" @@ -1111,6 +1235,36 @@ 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" @@ -1413,6 +1567,12 @@ 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" @@ -1496,6 +1656,36 @@ 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 f88610d..64e0fa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,6 @@ 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 new file mode 100644 index 0000000..be3460e --- /dev/null +++ b/NEXT.md @@ -0,0 +1,225 @@ +# 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 4cf56a4..22606be 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)?; + let result = archivr_core::capture::perform_capture(&archive_paths, path, None)?; println!("Archived: run {}", result.run_uid); Ok(()) } diff --git a/crates/archivr-core/src/archive.rs b/crates/archivr-core/src/archive.rs index ae26171..0961830 100644 --- a/crates/archivr-core/src/archive.rs +++ b/crates/archivr-core/src/archive.rs @@ -60,6 +60,17 @@ 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, @@ -74,6 +85,15 @@ 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 { @@ -173,7 +193,7 @@ pub fn initialize_store_directories(store_path: &Path) -> Result<()> { Ok(()) } -pub fn list_root_entries(conn: &rusqlite::Connection) -> Result> { +pub fn list_root_entries(conn: &rusqlite::Connection, caller_bits: u32) -> Result> { let mut stmt = conn.prepare( "SELECT e.entry_uid, @@ -192,12 +212,20 @@ pub fn list_root_entries(conn: &rusqlite::Connection) -> Result 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`). @@ -324,7 +456,7 @@ pub fn resolve_artifact_path( Ok(canonical_artifact) } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct SearchEntriesQuery { /// Free-text term: LIKE-matched against title, canonical_url, entry_uid, source_kind, entity_kind, visibility pub q: Option, @@ -342,6 +474,25 @@ 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`]. @@ -472,6 +623,15 @@ 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)?; @@ -823,7 +983,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).unwrap(); + let entries = list_root_entries(&conn, u32::MAX).unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].title.as_deref(), Some("Saved Article")); assert_eq!(entries[0].artifact_count, 1); @@ -994,7 +1154,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).unwrap(); + let all = list_root_entries(&conn, u32::MAX).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 d85ceaf..c2345d7 100644 --- a/crates/archivr-core/src/capture.rs +++ b/crates/archivr-core/src/capture.rs @@ -332,9 +332,6 @@ 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()) } @@ -675,7 +672,11 @@ fn fail_run( anyhow::anyhow!("{}", message) } -pub fn perform_capture(archive_paths: &ArchivePaths, locator: &str) -> Result { +pub fn perform_capture( + archive_paths: &ArchivePaths, + locator: &str, + archive_id: Option<&str>, +) -> Result { let timestamp = Local::now().format("%Y-%m-%dT%H-%M-%S%.3f").to_string(); let store_path = &archive_paths.store_path; @@ -790,13 +791,32 @@ pub fn perform_capture(archive_paths: &ArchivePaths, locator: &str) -> Result Result Result Result 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 d207c35..d690070 100644 --- a/crates/archivr-core/src/database.rs +++ b/crates/archivr-core/src/database.rs @@ -73,6 +73,81 @@ 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) } @@ -215,6 +290,17 @@ 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); @@ -225,11 +311,643 @@ 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( @@ -257,6 +975,71 @@ 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, @@ -431,6 +1214,26 @@ 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"); @@ -474,6 +1277,11 @@ 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", @@ -1101,4 +2108,218 @@ 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 new file mode 100644 index 0000000..3ae8ecc --- /dev/null +++ b/crates/archivr-core/src/downloader/font_extractor.rs @@ -0,0 +1,187 @@ +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 d8c3911..6c8d62a 100644 --- a/crates/archivr-core/src/downloader/mod.rs +++ b/crates/archivr-core/src/downloader/mod.rs @@ -5,3 +5,4 @@ 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 5d24712..d50d554 100644 --- a/crates/archivr-core/src/downloader/singlefile.rs +++ b/crates/archivr-core/src/downloader/singlefile.rs @@ -63,6 +63,16 @@ 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) @@ -72,6 +82,11 @@ 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 cbf1194..949a86b 100644 --- a/crates/archivr-core/src/hash.rs +++ b/crates/archivr-core/src/hash.rs @@ -1,17 +1,56 @@ use anyhow::Result; use sha3::{Digest, Sha3_256}; -use std::{fs::File, io::Read, path::Path}; +use std::path::Path; -pub fn hash_file(path: &Path) -> Result { - 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())) -} - -pub fn hash(path: String) -> String { +/// 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(path.as_bytes()); - let result = hasher.finalize(); - hex::encode(result) + 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)) +} + +/// 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); + } } diff --git a/crates/archivr-server/Cargo.toml b/crates/archivr-server/Cargo.toml index 4c3548e..b647f08 100644 --- a/crates/archivr-server/Cargo.toml +++ b/crates/archivr-server/Cargo.toml @@ -12,8 +12,14 @@ 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 new file mode 100644 index 0000000..195e2b0 --- /dev/null +++ b/crates/archivr-server/src/auth.rs @@ -0,0 +1,127 @@ +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 47f9532..ab7366b 100644 --- a/crates/archivr-server/src/main.rs +++ b/crates/archivr-server/src/main.rs @@ -1,3 +1,4 @@ +mod auth; mod registry; mod routes; @@ -12,10 +13,45 @@ 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()); - // Bind address priority: ARCHIVR_BIND env var > TOML bind field > default loopback. + let registry = registry::load_registry(&config_path)?; + + // 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; + } + }); + let bind_str = std::env::var("ARCHIVR_BIND") .ok() .or_else(|| registry.bind.clone()) @@ -25,15 +61,6 @@ 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 a9fef6c..f58fa04 100644 --- a/crates/archivr-server/src/registry.rs +++ b/crates/archivr-server/src/registry.rs @@ -16,10 +16,13 @@ pub struct MountedArchive { pub struct ServerRegistry { #[serde(default)] pub archives: Vec, - /// 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. + /// 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, } pub fn load_registry(path: &Path) -> Result { @@ -43,19 +46,16 @@ pub fn save_registry(path: &Path, registry: &ServerRegistry) -> Result<()> { } pub fn validate_registry(registry: &ServerRegistry) -> Result<()> { - let mut ids = std::collections::HashSet::new(); + let mut seen_ids = std::collections::HashSet::new(); for archive in ®istry.archives { if archive.id.trim().is_empty() { bail!("archive id must not be empty"); } - if !ids.insert(archive.id.as_str()) { + if !seen_ids.insert(archive.id.clone()) { bail!("duplicate archive id: {}", archive.id); } - if !archive.archive_path.ends_with(".archivr") { - bail!( - "mounted archive path must point at a .archivr directory: {}", - archive.archive_path.display() - ); + if archive.label.trim().is_empty() { + bail!("archive label must not be empty for id={}", archive.id); } } Ok(()) @@ -84,6 +84,7 @@ 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(); @@ -109,6 +110,7 @@ mod tests { }, ], bind: None, + auth_db_path: None, }; let err = validate_registry(®istry).unwrap_err().to_string(); @@ -136,6 +138,7 @@ 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 3441f5e..3ae5804 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -1,26 +1,23 @@ -// ── 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. +// ── Security Boundary ────────────────────────────────────────────────────────────────── +// setup_guard middleware returns 503 for all non-auth routes until POST /api/auth/setup +// creates the owner account. // -// 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. -// ───────────────────────────────────────────────────────────────────────────── +// 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 +// ──────────────────────────────────────────────────────────────────────────── use std::{path::PathBuf, sync::Arc}; @@ -29,17 +26,23 @@ use axum::{ Json, Router, extract::{Path, Query, Request, State}, http::StatusCode, + middleware::Next, response::{IntoResponse, Response}, - routing::{delete, get, post}, + routing::{delete, get, patch, 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)] @@ -48,9 +51,35 @@ pub struct EntrySearchParams { pub tag: Option, } -pub fn app(registry: ServerRegistry) -> Router { +/// 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 { let state = AppState { registry: Arc::new(registry), + auth_db_path: Arc::new(auth_db_path), }; let static_dir = static_dir(); @@ -71,8 +100,16 @@ pub fn app(registry: ServerRegistry) -> 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", @@ -82,8 +119,35 @@ pub fn app(registry: ServerRegistry) -> 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) } @@ -99,15 +163,18 @@ 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)?; - Ok(Json(archive::list_root_entries(&conn)?)) + let caller_bits = auth_to_caller_bits(&auth); + Ok(Json(archive::list_root_entries(&conn, caller_bits)?)) } async fn search_entries_handler( State(state): State, + auth: AuthUser, Path(archive_id): Path, Query(params): Query, ) -> Result>, ApiError> { @@ -119,6 +186,7 @@ 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)?)) } @@ -189,6 +257,39 @@ 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, @@ -199,6 +300,34 @@ 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, @@ -210,9 +339,11 @@ 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")); } @@ -236,9 +367,11 @@ 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")); } @@ -252,8 +385,10 @@ 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)? { @@ -268,20 +403,431 @@ 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, ApiError> { +) -> Result<(StatusCode, Json), ApiError> { + auth_user.require_role(ROLE_USER)?; 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)?; - let result = capture::perform_capture(&archive_paths, &body.locator) + + // 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) .map_err(ApiError::from)?; - Ok(Json(result)) + 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, + } } fn mounted_archive<'a>( @@ -316,6 +862,21 @@ 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 @@ -333,7 +894,220 @@ where impl IntoResponse for ApiError { fn into_response(self) -> Response { - (self.status, self.message).into_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")) } } @@ -344,8 +1118,112 @@ 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(), @@ -353,8 +1231,9 @@ mod tests { archive_path: std::path::PathBuf::from("/tmp/personal/.archivr"), }], bind: None, + auth_db_path: None, }; - let response = app(registry) + let response = app(registry, auth_path) .oneshot( Request::builder() .uri("/api/archives") @@ -369,7 +1248,8 @@ mod tests { #[tokio::test] async fn missing_archive_returns_404() { - let response = app(ServerRegistry::default()) + let (test_app, _dir) = make_test_app(); + let response = test_app .oneshot( Request::builder() .uri("/api/archives/missing/entries") @@ -384,7 +1264,8 @@ mod tests { #[tokio::test] async fn artifact_missing_archive_returns_404() { - let response = app(ServerRegistry::default()) + let (test_app, _dir) = make_test_app(); + let response = test_app .oneshot( Request::builder() .uri("/api/archives/nope/entries/entry_abc/artifacts/0") @@ -407,6 +1288,11 @@ 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(), @@ -414,8 +1300,9 @@ mod tests { archive_path, }], bind: None, + auth_db_path: None, }; - let response = app(registry) + let response = app(registry, auth_path) .oneshot( Request::builder() .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0") @@ -438,6 +1325,11 @@ 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(), @@ -445,8 +1337,9 @@ mod tests { archive_path, }], bind: None, + auth_db_path: None, }; - let response = app(registry) + let response = app(registry, auth_path) .oneshot( Request::builder() .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99") @@ -531,6 +1424,11 @@ 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(), @@ -538,12 +1436,13 @@ 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) + let response = app(registry, auth_path) .oneshot(Request::builder().uri(&uri).body(Body::empty()).unwrap()) .await .unwrap(); @@ -552,7 +1451,8 @@ mod tests { #[tokio::test] async fn search_missing_archive_returns_404() { - let response = app(ServerRegistry::default()) + let (test_app, _dir) = make_test_app(); + let response = test_app .oneshot( Request::builder() .uri("/api/archives/nope/entries/search?q=anything") @@ -567,23 +1467,8 @@ mod tests { #[tokio::test] async fn search_empty_q_returns_ok() { let dir = tempfile::tempdir().unwrap(); - archivr_core::archive::initialize_archive( - dir.path(), - &dir.path().join("store"), - "test", - false, - ) - .unwrap(); - let archive_path = dir.path().join(".archivr"); - let registry = ServerRegistry { - archives: vec![MountedArchive { - id: "test".to_string(), - label: "Test".to_string(), - archive_path, - }], - bind: None, - }; - let response = app(registry) + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) .oneshot( Request::builder() .uri("/api/archives/test/entries/search") @@ -598,23 +1483,8 @@ mod tests { #[tokio::test] async fn search_unknown_prefix_returns_400() { let dir = tempfile::tempdir().unwrap(); - archivr_core::archive::initialize_archive( - dir.path(), - &dir.path().join("store"), - "test", - false, - ) - .unwrap(); - let archive_path = dir.path().join(".archivr"); - let registry = ServerRegistry { - archives: vec![MountedArchive { - id: "test".to_string(), - label: "Test".to_string(), - archive_path, - }], - bind: None, - }; - let response = app(registry) + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) .oneshot( Request::builder() .uri("/api/archives/test/entries/search?q=unknownprefix%3Aval") @@ -626,74 +1496,12 @@ 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 response = app(ServerRegistry::default()) + let (test_app, _dir) = make_test_app(); + let response = test_app .oneshot( Request::builder() .uri("/api/archives/ghost/tags") @@ -707,7 +1515,8 @@ mod tests { #[tokio::test] async fn test_create_tag_unknown_archive() { - let response = app(ServerRegistry::default()) + let (test_app, _dir) = make_test_app(); + let response = test_app .oneshot( Request::builder() .method("POST") @@ -718,14 +1527,14 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before archive lookup } #[tokio::test] async fn test_create_tag_empty_path() { let dir = tempfile::tempdir().unwrap(); - let (registry, _) = make_test_registry(&dir); - let response = app(registry) + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) .oneshot( Request::builder() .method("POST") @@ -736,28 +1545,29 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before validation } #[tokio::test] async fn test_tag_round_trip() { let dir = tempfile::tempdir().unwrap(); - let (registry, _) = make_test_registry(&dir); + let (registry, _, auth_path) = make_test_registry(&dir); + let session_cookie = make_test_session(&auth_path); - let create_response = app(registry.clone()) + let create_response = app(registry.clone(), auth_path.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()) + let list_response = app(registry.clone(), auth_path.clone()) .oneshot( Request::builder() .uri("/api/archives/test/tags") @@ -780,18 +1590,20 @@ mod tests { #[tokio::test] async fn test_entry_tag_assign_and_remove() { let dir = tempfile::tempdir().unwrap(); - let (registry, archive_path) = make_test_registry(&dir); + let (registry, archive_path, auth_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()) + let assign_response = app(registry.clone(), auth_path.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(), ) @@ -802,7 +1614,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()) + let list_response = app(registry.clone(), auth_path.clone()) .oneshot( Request::builder() .uri(&entry_tags_uri) @@ -817,11 +1629,12 @@ mod tests { // Remove tag let delete_uri = format!("{entry_tags_uri}/{tag_uid}"); - let delete_response = app(registry.clone()) + let delete_response = app(registry.clone(), auth_path.clone()) .oneshot( Request::builder() .method("DELETE") .uri(&delete_uri) + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -830,7 +1643,7 @@ mod tests { assert_eq!(delete_response.status(), StatusCode::NO_CONTENT); // List entry tags again — should be empty - let list2_response = app(registry.clone()) + let list2_response = app(registry.clone(), auth_path.clone()) .oneshot( Request::builder() .uri(&entry_tags_uri) @@ -847,17 +1660,19 @@ mod tests { #[tokio::test] async fn test_search_with_tag_param() { let dir = tempfile::tempdir().unwrap(); - let (registry, archive_path) = make_test_registry(&dir); + let (registry, archive_path, auth_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()) + let assign_resp = app(registry.clone(), auth_path.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(), ) @@ -865,11 +1680,12 @@ mod tests { .unwrap(); assert_eq!(assign_resp.status(), StatusCode::CREATED, "assign tag should return 201"); - // Search with ?tag=/science — entry should appear - let response = app(registry.clone()) + // Search with ?tag=/science — entry should appear (requires auth since entry is private) + let response = app(registry.clone(), auth_path.clone()) .oneshot( Request::builder() .uri("/api/archives/test/entries/search?tag=%2Fscience") + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -885,10 +1701,11 @@ mod tests { ); // Search with ?tag=/art — should return empty - let response2 = app(registry.clone()) + let response2 = app(registry.clone(), auth_path.clone()) .oneshot( Request::builder() .uri("/api/archives/test/entries/search?tag=%2Fart") + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -905,8 +1722,8 @@ mod tests { #[tokio::test] async fn test_list_entry_tags_unknown_entry() { let dir = tempfile::tempdir().unwrap(); - let (registry, _) = make_test_registry(&dir); - let response = app(registry) + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) .oneshot( Request::builder() .uri("/api/archives/test/entries/ghost_uid/tags") @@ -921,8 +1738,8 @@ mod tests { #[tokio::test] async fn test_assign_entry_tag_unknown_entry() { let dir = tempfile::tempdir().unwrap(); - let (registry, _) = make_test_registry(&dir); - let response = app(registry) + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) .oneshot( Request::builder() .method("POST") @@ -933,16 +1750,16 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before entry lookup } #[tokio::test] async fn test_assign_entry_tag_empty_tag_path() { let dir = tempfile::tempdir().unwrap(); - let (registry, archive_path) = make_test_registry(&dir); + let (registry, archive_path, auth_path) = make_test_registry(&dir); let entry = make_test_entry(&archive_path); let entry_uid = entry.entry_uid.clone(); - let response = app(registry) + let response = app(registry, auth_path) .oneshot( Request::builder() .method("POST") @@ -953,14 +1770,14 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before validation } #[tokio::test] async fn test_remove_entry_tag_unknown_entry() { let dir = tempfile::tempdir().unwrap(); - let (registry, _) = make_test_registry(&dir); - let response = app(registry) + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) .oneshot( Request::builder() .method("DELETE") @@ -970,12 +1787,13 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before entry lookup } #[tokio::test] async fn capture_rejects_empty_locator() { - let response = app(ServerRegistry::default()) + let (test_app, _dir) = make_test_app(); + let response = test_app .oneshot( Request::builder() .method("POST") @@ -986,12 +1804,13 @@ mod tests { ) .await .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // auth fires before validation } #[tokio::test] async fn capture_rejects_unknown_archive() { - let response = app(ServerRegistry::default()) + let (test_app, _dir) = make_test_app(); + let response = test_app .oneshot( Request::builder() .method("POST") @@ -1002,6 +1821,276 @@ 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 new file mode 100644 index 0000000..6632729 --- /dev/null +++ b/crates/archivr-server/static/assets/index-BfwsUa2j.js @@ -0,0 +1,40 @@ +(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 new file mode 100644 index 0000000..5000950 --- /dev/null +++ b/crates/archivr-server/static/assets/index-C6h0jcH3.js @@ -0,0 +1,40 @@ +(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 new file mode 100644 index 0000000..04f6484 --- /dev/null +++ b/crates/archivr-server/static/assets/index-D40QFUPh.css @@ -0,0 +1 @@ +: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 new file mode 100644 index 0000000..a5d6cd2 --- /dev/null +++ b/crates/archivr-server/static/assets/index-DRRKzKIq.js @@ -0,0 +1,40 @@ +(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 new file mode 100644 index 0000000..cf5673d --- /dev/null +++ b/crates/archivr-server/static/assets/index-Davzo15o.js @@ -0,0 +1,40 @@ +(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 new file mode 100644 index 0000000..944e05f --- /dev/null +++ b/crates/archivr-server/static/assets/index-Dfnwsgsa.js @@ -0,0 +1,40 @@ +(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 c3da2f9..5d3d3d6 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 new file mode 100644 index 0000000..3f9da52 --- /dev/null +++ b/docs/superpowers/plans/2026-06-25-auth-foundation.md @@ -0,0 +1,1977 @@ +# 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 new file mode 100644 index 0000000..23e74c8 --- /dev/null +++ b/docs/superpowers/specs/2026-06-25-auth-foundation-design.md @@ -0,0 +1,403 @@ +# 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 3ab5b52..c87369b 100644 --- a/flake.nix +++ b/flake.nix @@ -73,6 +73,7 @@ cargoLock nativeBuildInputs ; + buildInputs = [ pkgs.openssl ]; cargoBuildFlags = [ "-p" "archivr-cli" @@ -90,6 +91,7 @@ cargoLock nativeBuildInputs ; + buildInputs = [ pkgs.openssl ]; cargoBuildFlags = [ "-p" "archivr-server" diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f663efe..0061113 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,14 +1,41 @@ -import { useState, useEffect, useCallback, useRef } from 'react' -import { fetchArchives, fetchEntries, searchEntries, fetchRuns, fetchTags } from './api' +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 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([]) @@ -127,77 +154,89 @@ 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' && ( - - )} - {view === 'runs' && } - {view === 'admin' && } - {view === 'tags' && ( - - )} -
- + <> + -
- - +
+
+ {view === 'archive' && ( +
+ setSearchQuery(e.target.value)} + /> +
+ {resultCount} + {tagFilter && ( + + )} +
+
+ )} + {view === 'archive' && ( + + )} + {view === 'runs' && } + {view === 'admin' && } + {view === 'tags' && ( + + )} + {view === 'collections' && ( + + )} + {view === 'settings' && ( + + )} +
+ +
+ + + ) } diff --git a/frontend/src/api.js b/frontend/src/api.js index 0ce034b..6b17079 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -64,7 +64,250 @@ export async function submitCapture(archiveId, locator) { body: JSON.stringify({ locator }), }); if (!res.ok) { - const msg = await res.text(); - throw new Error(msg || `HTTP ${res.status}`); + 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); } } + +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 f9520c2..7abd8b2 100644 --- a/frontend/src/components/AdminView.jsx +++ b/frontend/src/components/AdminView.jsx @@ -1,15 +1,218 @@ +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 (
-

Mounted Archives

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

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}
+
+ ))} +
+
+ )}
) } diff --git a/frontend/src/components/CaptureDialog.jsx b/frontend/src/components/CaptureDialog.jsx index 9a6c73c..9942a71 100644 --- a/frontend/src/components/CaptureDialog.jsx +++ b/frontend/src/components/CaptureDialog.jsx @@ -1,16 +1,21 @@ import { useRef, useEffect, useState } from 'react' -import { submitCapture } from '../api' +import { submitCapture, pollCaptureJob } 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 = () => onClose() + const handleClose = () => { + clearInterval(pollRef.current) + onClose() + } dialog.addEventListener('close', handleClose) return () => dialog.removeEventListener('close', handleClose) }, [onClose]) @@ -21,6 +26,9 @@ 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() @@ -31,17 +39,47 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) if (!locator.trim()) { setError('Enter a locator.'); return } setBusy(true) setError(null) + setJobStatus(null) try { - await submitCapture(archiveId, locator.trim()) - dialogRef.current?.close() - onCaptured() + 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) } catch (e) { setError(e.message) - } finally { setBusy(false) } } + function buttonLabel() { + if (!busy) return 'Capture' + if (jobStatus === 'running') return 'Running\u2026' + return 'Capturing\u2026' + } + return (
@@ -56,7 +94,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })
diff --git a/frontend/src/components/CollectionsView.jsx b/frontend/src/components/CollectionsView.jsx new file mode 100644 index 0000000..48e4271 --- /dev/null +++ b/frontend/src/components/CollectionsView.jsx @@ -0,0 +1,366 @@ +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 ff497fc..b94c6a5 100644 --- a/frontend/src/components/ContextRail.jsx +++ b/frontend/src/components/ContextRail.jsx @@ -1,11 +1,12 @@ import { useState, useEffect, useRef } from 'react' -import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag } from '../api' +import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag, listEntryCollections } 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) @@ -13,6 +14,7 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, if (!selectedEntry || !archiveId) { setDetail(null) setTags([]) + setEntryCollections([]) return } const seq = ++selectSeqRef.current @@ -21,10 +23,12 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, Promise.all([ fetchEntryDetail(archiveId, selectedEntry.entry_uid), fetchEntryTags(archiveId, selectedEntry.entry_uid), - ]).then(([det, tgs]) => { + listEntryCollections(archiveId, selectedEntry.entry_uid), + ]).then(([det, tgs, ecs]) => { if (seq !== selectSeqRef.current) return setDetail(det) setTags(tgs) + setEntryCollections(ecs) }).catch(() => {}) }, [selectedEntry, archiveId]) @@ -156,6 +160,19 @@ 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 new file mode 100644 index 0000000..105ed5a --- /dev/null +++ b/frontend/src/components/LoginPage.jsx @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000..37e3a9f --- /dev/null +++ b/frontend/src/components/SettingsView.jsx @@ -0,0 +1,271 @@ +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 new file mode 100644 index 0000000..aca4cb3 --- /dev/null +++ b/frontend/src/components/SetupPage.jsx @@ -0,0 +1,73 @@ +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 b81d72a..e188cec 100644 --- a/frontend/src/components/Topbar.jsx +++ b/frontend/src/components/Topbar.jsx @@ -1,4 +1,18 @@ +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
@@ -7,7 +21,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 8916da2..d5a2470 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -640,3 +640,291 @@ 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; +}