From f9de282b0e3862f7d889fcd15f89f130fd612947 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:51:20 +0200 Subject: [PATCH 1/9] feat(nix): add NixOS module for archivr-server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit modules/nixos/archivr-server.nix — services.archivr-server NixOS module: - enable/bind/archives/user/group/openFirewall options - generates archivr-server.toml from options via pkgs.writeText - pins auth DB to /var/lib/archivr-server/ (StateDirectory) - dedicated archivr system user + group - hardened systemd unit: ProtectSystem=strict, NoNewPrivileges, PrivateTmp, RestrictNamespaces, etc.; archive paths whitelisted - openFirewall parses the port from the bind string automatically flake.nix: - add self to outputs args - add aarch64-linux to systems (Raspberry Pi / ARM servers) - expose nixosModules.archivr-server + nixosModules.default .gitignore: whitelist modules/ directory docs/README.md: add 'Hosting on NixOS' section with full example --- .gitignore | 3 + docs/README.md | 52 +++++++++++ flake.nix | 8 +- modules/nixos/archivr-server.nix | 156 +++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 modules/nixos/archivr-server.nix diff --git a/.gitignore b/.gitignore index c950ddc..754d1d4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ !ARCHIVR-MENTAL-MODEL.md !NEXT.md +!modules/ +!modules/** + !frontend/ !frontend/** frontend/node_modules/ diff --git a/docs/README.md b/docs/README.md index 98e7dbe..1df2eed 100644 --- a/docs/README.md +++ b/docs/README.md @@ -137,6 +137,58 @@ Auth and session handling will be designed when remote or public hosting becomes - X/Twitter Tweet content scrape: [Tweet and Thread shorthands](#supported-shorthand-inputs). (These are saved as JSON files in `raw_tweets/`) - Instagram, Facebook, TikTok, Reddit, Snapchat: direct URLs or platform-prefixed shorthand passed through to `yt-dlp` +### Hosting on NixOS + +The flake exposes a `nixosModules.default` output. Add it to your system flake and +enable the service: + +```nix +# flake.nix (your system flake) +{ + inputs.archivr.url = "github:thegeneralist/archivr"; + + outputs = { nixpkgs, archivr, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + archivr.nixosModules.default + { + services.archivr-server = { + enable = true; + bind = "127.0.0.1:8080"; # loopback only; put nginx/caddy in front for TLS + archives = [ + { id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; } + { id = "work"; label = "Work"; path = "/srv/archivr/work/.archivr"; } + ]; + }; + } + ]; + }; + }; +} +``` + +The module: +- Creates an `archivr` system user and group. +- Generates the TOML config from your options and stores the auth database under + `/var/lib/archivr-server/` (persists across upgrades). +- Runs under a hardened systemd unit (`ProtectSystem = strict`, `NoNewPrivileges`, + `PrivateTmp`, etc.). Archive directories are whitelisted for read-write access. +- Restarts automatically on failure. + +**`openFirewall`** — set to `true` to open the TCP port derived from `bind`. +Only needed when binding to a non-loopback address: + +```nix +services.archivr-server = { + bind = "0.0.0.0:8080"; + openFirewall = true; +}; +``` + +**Archive directories** must be readable and writable by the `archivr` user. +Initialise them with `archivr init` first, then `chown -R archivr:archivr /srv/archivr`. + + ### Supported Shorthand Inputs - YouTube video/short media: diff --git a/flake.nix b/flake.nix index c87369b..48ad04e 100644 --- a/flake.nix +++ b/flake.nix @@ -16,11 +16,12 @@ inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; outputs = - { nixpkgs, ... }: + { nixpkgs, self, ... }: let lib = nixpkgs.lib; systems = [ "x86_64-linux" + "aarch64-linux" "aarch64-darwin" ]; in @@ -171,6 +172,11 @@ } ); + nixosModules = { + archivr-server = import ./modules/nixos/archivr-server.nix { inherit self; }; + default = self.nixosModules.archivr-server; + }; + devShells = lib.genAttrs systems ( system: let diff --git a/modules/nixos/archivr-server.nix b/modules/nixos/archivr-server.nix new file mode 100644 index 0000000..05aebfb --- /dev/null +++ b/modules/nixos/archivr-server.nix @@ -0,0 +1,156 @@ +# NixOS module for archivr-server. +# Consumed by flake.nix as: +# nixosModules.archivr-server = import ./modules/nixos/archivr-server.nix { inherit self; }; +{ self }: +{ config, lib, pkgs, ... }: +let + cfg = config.services.archivr-server; + + # Generate the TOML registry file from NixOS options. + # The auth DB is pinned to the StateDirectory so it survives upgrades. + configFile = pkgs.writeText "archivr-server.toml" '' + bind = "${cfg.bind}" + auth_db_path = "/var/lib/archivr-server/archivr-auth.sqlite" + + ${lib.concatMapStrings (a: '' + [[archives]] + id = "${a.id}" + label = "${a.label}" + archive_path = "${a.path}" + + '') cfg.archives} + ''; +in +{ + options.services.archivr-server = { + enable = lib.mkEnableOption "archivr web server"; + + package = lib.mkOption { + type = lib.types.package; + default = self.packages.${pkgs.system}.archivr-server; + defaultText = lib.literalExpression "archivr-server from the archivr flake"; + description = "The archivr-server package to use."; + }; + + bind = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1:8080"; + example = "0.0.0.0:8080"; + description = '' + Address and port to listen on. Defaults to loopback only. + Binding to a non-loopback address exposes the server on the network; + put a reverse proxy with TLS and authentication in front before doing so. + ''; + }; + + archives = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + id = lib.mkOption { + type = lib.types.str; + example = "personal"; + description = "Unique archive identifier used in the config and API URLs."; + }; + label = lib.mkOption { + type = lib.types.str; + example = "Personal"; + description = "Display name shown in the UI archive switcher."; + }; + path = lib.mkOption { + type = lib.types.str; + example = "/srv/archivr/personal/.archivr"; + description = "Absolute path to the .archivr directory created by {command}`archivr init`."; + }; + }; + }); + default = []; + description = '' + Archives to mount. Each entry maps to one {code}`[[archives]]` block in the + generated config file. The list must contain at least one entry. + ''; + example = lib.literalExpression '' + [ + { id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; } + { id = "work"; label = "Work"; path = "/srv/archivr/work/.archivr"; } + ] + ''; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "archivr"; + description = "User account under which archivr-server runs."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "archivr"; + description = "Group under which archivr-server runs."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Open the firewall TCP port derived from + {option}`services.archivr-server.bind`. Only meaningful when binding + to a non-loopback address. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.archives != []; + message = "services.archivr-server.archives must contain at least one archive."; + } + ]; + + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + description = "archivr-server service user"; + home = "/var/lib/archivr-server"; + createHome = false; # StateDirectory creates it + }; + users.groups.${cfg.group} = { }; + + systemd.services.archivr-server = { + description = "archivr web server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/archivr-server ${configFile}"; + User = cfg.user; + Group = cfg.group; + + # State directory — auth SQLite lives here across upgrades/restarts. + StateDirectory = "archivr-server"; + StateDirectoryMode = "0750"; + + Restart = "on-failure"; + RestartSec = "5s"; + + # Hardening — make the entire FS read-only except for the state + # directory and the mounted archive directories. + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ReadWritePaths = [ "/var/lib/archivr-server" ] ++ (map (a: a.path) cfg.archives); + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + LockPersonality = true; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ + (lib.toInt (lib.last (lib.splitString ":" cfg.bind))) + ]; + }; + }; +} From 3b3b0d720748b84f02542aecb47df9bbed5a70ac Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:03:44 +0200 Subject: [PATCH 2/9] feat(server): add HTTP security response headers middleware Injects X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Content-Security-Policy, and Permissions-Policy on every response. Registered as outermost layer so it covers setup_guard 503s too. No new dependencies. --- crates/archivr-server/src/routes.rs | 92 +++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index 3ae5804..ab09974 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -76,6 +76,42 @@ async fn setup_guard( next.run(req).await } +/// Tower middleware: injects HTTP security response headers on every response. +/// HSTS is intentionally omitted — that belongs at the reverse-proxy layer. +async fn security_headers(req: Request, next: Next) -> Response { + let mut response = next.run(req).await; + let headers = response.headers_mut(); + headers.insert( + axum::http::header::HeaderName::from_static("x-content-type-options"), + axum::http::HeaderValue::from_static("nosniff"), + ); + headers.insert( + axum::http::header::HeaderName::from_static("x-frame-options"), + axum::http::HeaderValue::from_static("DENY"), + ); + headers.insert( + axum::http::header::HeaderName::from_static("referrer-policy"), + axum::http::HeaderValue::from_static("strict-origin-when-cross-origin"), + ); + headers.insert( + axum::http::header::HeaderName::from_static("content-security-policy"), + axum::http::HeaderValue::from_static( + "default-src 'self'; \ + script-src 'self'; \ + style-src 'self' 'unsafe-inline'; \ + img-src 'self' data: blob:; \ + font-src 'self'; \ + connect-src 'self'; \ + frame-ancestors 'none'", + ), + ); + headers.insert( + axum::http::header::HeaderName::from_static("permissions-policy"), + axum::http::HeaderValue::from_static("camera=(), microphone=(), geolocation=()"), + ); + response +} + pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router { let state = AppState { registry: Arc::new(registry), @@ -148,6 +184,7 @@ pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router .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)) + .layer(axum::middleware::from_fn(security_headers)) .with_state(state) } @@ -2093,4 +2130,59 @@ mod tests { ).await.unwrap(); assert_eq!(response.status(), StatusCode::NO_CONTENT); } + #[tokio::test] + async fn security_headers_present_on_success_response() { + let (test_app, _dir) = make_test_app(); + let response = test_app + .oneshot( + Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("x-content-type-options").unwrap(), + "nosniff" + ); + assert_eq!( + response.headers().get("x-frame-options").unwrap(), + "DENY" + ); + assert_eq!( + response.headers().get("referrer-policy").unwrap(), + "strict-origin-when-cross-origin" + ); + assert!( + response.headers().get("content-security-policy").is_some(), + "content-security-policy header must be present" + ); + assert!( + response.headers().get("permissions-policy").is_some(), + "permissions-policy header must be present" + ); + } + + #[tokio::test] + async fn security_headers_present_on_error_response() { + let (test_app, _dir) = make_test_app(); + let response = test_app + .oneshot( + Request::builder() + .uri("/api/archives/nosucharchive/entries") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_ne!(response.status(), StatusCode::OK); + assert!(response.headers().get("x-content-type-options").is_some()); + assert!(response.headers().get("x-frame-options").is_some()); + assert!(response.headers().get("referrer-policy").is_some()); + assert!(response.headers().get("content-security-policy").is_some()); + assert!(response.headers().get("permissions-policy").is_some()); + } + } From a8e52bdf71b0e0424dc8121766d24f3f71f57019 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:06:41 +0200 Subject: [PATCH 3/9] security: add per-IP sliding-window rate limit on POST /api/auth/login 5 attempts / 15 min per IP. Excess attempts get 429 + Retry-After. Client IP read from X-Forwarded-For with loopback fallback. In-memory; prune inline on each check. Refactored app() into app_with_state() + thin wrapper. Uses parking_lot::Mutex (no unwrap, no poisoning). --- Cargo.toml | 1 + crates/archivr-server/Cargo.toml | 1 + crates/archivr-server/src/routes.rs | 205 +++++++++++++++++++++++++--- 3 files changed, 189 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 64e0fa4..e4992e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,4 @@ base64 = "0.22" argon2 = { version = "0.5", features = ["std"] } rand = { version = "0.8", features = ["std"] } axum-extra = { version = "0.9", features = ["cookie"] } +parking_lot = "0.12" diff --git a/crates/archivr-server/Cargo.toml b/crates/archivr-server/Cargo.toml index b647f08..46a6c94 100644 --- a/crates/archivr-server/Cargo.toml +++ b/crates/archivr-server/Cargo.toml @@ -19,6 +19,7 @@ chrono.workspace = true base64.workspace = true serde_json.workspace = true rusqlite.workspace = true +parking_lot.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index ab09974..9ba5f6c 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -19,7 +19,14 @@ // GET/PATCH /api/admin/instance-settings // ──────────────────────────────────────────────────────────────────────────── -use std::{path::PathBuf, sync::Arc}; +use std::{ + collections::{HashMap, VecDeque}, + net::IpAddr, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; +use parking_lot::Mutex; use archivr_core::{archive, capture, database}; use axum::{ @@ -39,10 +46,14 @@ pub use crate::auth::{AuthUser, ROLE_ADMIN, ROLE_GUEST, ROLE_OWNER, ROLE_USER}; use axum_extra::extract::CookieJar; use rusqlite::OptionalExtension; +const LOGIN_WINDOW: Duration = Duration::from_secs(15 * 60); +const LOGIN_MAX_ATTEMPTS: usize = 5; + #[derive(Clone)] pub struct AppState { registry: Arc, pub auth_db_path: Arc, + pub login_attempts: Arc>>>, } #[derive(Debug, serde::Deserialize, Default)] @@ -112,11 +123,59 @@ async fn security_headers(req: Request, next: Next) -> Response { response } -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), +async fn login_rate_limit( + State(state): State, + req: Request, + next: Next, +) -> Response { + if req.method() != axum::http::Method::POST || req.uri().path() != "/api/auth/login" { + return next.run(req).await; + } + let ip = extract_client_ip(&req); + let retry_after = { + let mut map = state.login_attempts.lock(); + let attempts = map.entry(ip).or_default(); + let now = Instant::now(); + attempts.retain(|t| now.duration_since(*t) < LOGIN_WINDOW); + if attempts.len() >= LOGIN_MAX_ATTEMPTS { + let oldest = *attempts.front().unwrap(); + let elapsed = now.duration_since(oldest).as_secs() as i64; + let secs = (LOGIN_WINDOW.as_secs() as i64 - elapsed).max(1); + Some(secs) + } else { + attempts.push_back(now); + None + } }; + if let Some(secs) = retry_after { + return ( + StatusCode::TOO_MANY_REQUESTS, + [( + axum::http::header::RETRY_AFTER, + axum::http::HeaderValue::from_str(&secs.to_string()).unwrap(), + )], + axum::Json(serde_json::json!({ + "error": "rate_limited", + "retry_after_secs": secs, + })), + ) + .into_response(); + } + next.run(req).await +} + +fn extract_client_ip(req: &Request) -> IpAddr { + req.headers() + .get("x-forwarded-for") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.split(',').next()) + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(IpAddr::from([127, 0, 0, 1])) +} + +/// Build the Axum router from a pre-constructed `AppState`. +/// Use this in tests that need to share state across multiple `oneshot` calls. +pub fn app_with_state(state: AppState) -> Router { let static_dir = static_dir(); Router::new() @@ -166,28 +225,51 @@ pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router .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", + .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", + .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)) + .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)) + .layer(axum::middleware::from_fn_with_state(state.clone(), login_rate_limit)) .layer(axum::middleware::from_fn(security_headers)) .with_state(state) } +/// Build the Axum router, constructing `AppState` from the given registry and auth DB path. +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), + login_attempts: Arc::new(Mutex::new(HashMap::new())), + }; + app_with_state(state) +} + fn static_dir() -> PathBuf { std::env::var_os("ARCHIVR_STATIC_DIR") .map(PathBuf::from) @@ -2185,4 +2267,91 @@ mod tests { assert!(response.headers().get("permissions-policy").is_some()); } + #[tokio::test] + async fn login_rate_limit_blocks_after_max_attempts() { + 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 state = AppState { + registry: Arc::new(ServerRegistry { + archives: vec![], + bind: None, + auth_db_path: None, + }), + auth_db_path: Arc::new(auth_path), + login_attempts: Arc::new(Mutex::new(HashMap::new())), + }; + let bad_creds = serde_json::json!({ "username": "nobody", "password": "wrong" }); + for _ in 0..LOGIN_MAX_ATTEMPTS { + let resp = app_with_state(state.clone()) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/auth/login") + .header("content-type", "application/json") + .header("x-forwarded-for", "10.0.0.1") + .body(json_body(&bad_creds)) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED, "attempt within limit should reach handler"); + } + let resp = app_with_state(state) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/auth/login") + .header("content-type", "application/json") + .header("x-forwarded-for", "10.0.0.1") + .body(json_body(&bad_creds)) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS, "attempt over limit must be 429"); + assert!(resp.headers().contains_key("retry-after"), "429 must carry Retry-After header"); + let body = body_json(resp).await; + assert_eq!(body["error"], "rate_limited"); + assert!(body["retry_after_secs"].as_i64().unwrap() > 0); + } + + #[tokio::test] + async fn login_rate_limit_does_not_affect_other_routes() { + 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 state = AppState { + registry: Arc::new(ServerRegistry { archives: vec![], bind: None, auth_db_path: None }), + auth_db_path: Arc::new(auth_path), + login_attempts: Arc::new(Mutex::new(HashMap::new())), + }; + let bad_creds = serde_json::json!({ "username": "x", "password": "y" }); + for _ in 0..LOGIN_MAX_ATTEMPTS { + app_with_state(state.clone()) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/auth/login") + .header("content-type", "application/json") + .header("x-forwarded-for", "10.0.0.2") + .body(json_body(&bad_creds)) + .unwrap(), + ) + .await + .unwrap(); + } + let resp = app_with_state(state) + .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::OK, "/health must be unaffected"); + } + } From 007c5fc5fb88ee9e51f90ff71efc8be69b20d0ac Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:12:00 +0200 Subject: [PATCH 4/9] feat(server): enforce auth on all public read endpoints Add auth_user: AuthUser + require_auth() to entry_detail, list_runs, serve_artifact, serve_entry_favicon, serve_blob, list_tags, list_entry_tags, list_collections_handler, list_entry_collections_handler. Add require_auth() to get_collection_handler (already had extractor). Update existing tests to pass session cookies. --- crates/archivr-server/src/routes.rs | 391 +++++++++++++++++++++++++--- 1 file changed, 355 insertions(+), 36 deletions(-) diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index 9ba5f6c..69b058c 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -311,8 +311,10 @@ async fn search_entries_handler( async fn entry_detail( State(state): State, + auth_user: AuthUser, Path((archive_id, entry_uid)): Path<(String, String)>, ) -> Result, ApiError> { + auth_user.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; let detail = archive::get_entry_detail(&conn, &entry_uid)? @@ -322,8 +324,10 @@ async fn entry_detail( async fn list_runs( State(state): State, + auth_user: AuthUser, Path(archive_id): Path, ) -> Result>, ApiError> { + auth_user.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; Ok(Json(archive::list_runs(&conn)?)) @@ -331,9 +335,11 @@ async fn list_runs( async fn serve_artifact( State(state): State, + auth_user: AuthUser, Path((archive_id, entry_uid, artifact_index)): Path<(String, String, usize)>, req: Request, ) -> Result { + auth_user.require_auth()?; 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)?; @@ -344,8 +350,6 @@ async fn serve_artifact( .get(artifact_index) .ok_or(ApiError::not_found("artifact index out of range"))?; let file_path = archive::resolve_artifact_path(&paths.store_path, artifact)?; - // ServeFile streams the file, handles Range requests (video seeking), - // sets Content-Type/ETag/Last-Modified. Error type is Infallible. Ok(ServeFile::new(&file_path) .oneshot(req) .await @@ -355,9 +359,11 @@ async fn serve_artifact( async fn serve_entry_favicon( State(state): State, + auth_user: AuthUser, Path((archive_id, entry_uid)): Path<(String, String)>, req: Request, ) -> Result { + auth_user.require_auth()?; 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)?; @@ -378,19 +384,17 @@ async fn serve_entry_favicon( async fn serve_blob( State(state): State, + auth_user: AuthUser, Path((archive_id, sha256)): Path<(String, String)>, req: Request, ) -> Result { + auth_user.require_auth()?; 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"))?; @@ -401,7 +405,6 @@ async fn serve_blob( if !canonical_file.starts_with(&canonical_store) { return Err(ApiError::not_found("blob not found")); } - Ok(ServeFile::new(&canonical_file) .oneshot(req) .await @@ -449,8 +452,10 @@ struct PatchCollectionBody { async fn list_tags( State(state): State, + auth_user: AuthUser, Path(archive_id): Path, ) -> Result>, ApiError> { + auth_user.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; Ok(Json(archive::list_tag_tree(&conn)?)) @@ -474,8 +479,10 @@ async fn create_tag_handler( async fn list_entry_tags( State(state): State, + auth_user: AuthUser, Path((archive_id, entry_uid)): Path<(String, String)>, ) -> Result>, ApiError> { + auth_user.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; match archive::get_entry_tags(&conn, &entry_uid)? { @@ -1022,8 +1029,10 @@ impl IntoResponse for ApiError { async fn list_collections_handler( State(state): State, + auth_user: AuthUser, Path(archive_id): Path, ) -> Result>, ApiError> { + auth_user.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; Ok(Json(archive::list_collections(&conn)?)) @@ -1060,6 +1069,7 @@ async fn get_collection_handler( auth: AuthUser, Path((archive_id, coll_uid)): Path<(String, String)>, ) -> Result, ApiError> { + auth.require_auth()?; 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)? @@ -1186,8 +1196,10 @@ async fn update_entry_visibility_handler( async fn list_entry_collections_handler( State(state): State, + auth_user: AuthUser, Path((archive_id, entry_uid)): Path<(String, String)>, ) -> Result>, ApiError> { + auth_user.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; match archive::get_entry_collections(&conn, &entry_uid)? { @@ -1383,16 +1395,14 @@ mod tests { #[tokio::test] async fn artifact_missing_archive_returns_404() { - let (test_app, _dir) = make_test_app(); - let response = test_app - .oneshot( - Request::builder() - .uri("/api/archives/nope/entries/entry_abc/artifacts/0") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); + 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 session_cookie = make_test_session(&auth_path); + let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/nope/entries/entry_abc/artifacts/0").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -1412,6 +1422,7 @@ mod tests { let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); } + let session_cookie = make_test_session(&auth_path); let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), @@ -1425,6 +1436,7 @@ mod tests { .oneshot( Request::builder() .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/0") + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1449,6 +1461,7 @@ mod tests { let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); } + let session_cookie = make_test_session(&auth_path); let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), @@ -1462,6 +1475,7 @@ mod tests { .oneshot( Request::builder() .uri("/api/archives/test/entries/entry_doesnotexist/artifacts/99") + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1548,6 +1562,7 @@ mod tests { let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); } + let session_cookie = make_test_session(&auth_path); let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), @@ -1562,7 +1577,7 @@ mod tests { entry.entry_uid ); let response = app(registry, auth_path) - .oneshot(Request::builder().uri(&uri).body(Body::empty()).unwrap()) + .oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); @@ -1619,16 +1634,14 @@ mod tests { #[tokio::test] async fn test_list_tags_unknown_archive() { - let (test_app, _dir) = make_test_app(); - let response = test_app - .oneshot( - Request::builder() - .uri("/api/archives/ghost/tags") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); + 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 session_cookie = make_test_session(&auth_path); + let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/ghost/tags").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -1690,6 +1703,7 @@ mod tests { .oneshot( Request::builder() .uri("/api/archives/test/tags") + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1737,6 +1751,7 @@ mod tests { .oneshot( Request::builder() .uri(&entry_tags_uri) + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1766,6 +1781,7 @@ mod tests { .oneshot( Request::builder() .uri(&entry_tags_uri) + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -1842,15 +1858,10 @@ mod tests { async fn test_list_entry_tags_unknown_entry() { 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/archives/test/entries/ghost_uid/tags") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); + .oneshot(Request::builder().uri("/api/archives/test/entries/ghost_uid/tags").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -1959,6 +1970,7 @@ mod tests { let conn = archivr_core::database::open_auth_db(&auth_path).unwrap(); archivr_core::database::create_owner(&conn, "testowner", "dummy").unwrap(); } + let session_cookie = make_test_session(&auth_path); let registry = ServerRegistry { archives: vec![MountedArchive { id: "test".to_string(), @@ -1972,6 +1984,7 @@ mod tests { .oneshot( Request::builder() .uri("/api/archives/test/blobs/0000000000000000000000000000000000000000000000000000000000000000") + .header("cookie", &session_cookie) .body(Body::empty()) .unwrap(), ) @@ -2354,4 +2367,310 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK, "/health must be unaffected"); } + // ── Task 1: read-endpoint auth enforcement ──────────────────────────────── + + #[tokio::test] + async fn entry_detail_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn entry_detail_with_auth_returns_ok() { + let dir = tempfile::tempdir().unwrap(); + let (registry, archive_path, auth_path) = make_test_registry(&dir); + let entry = make_test_entry(&archive_path); + let session_cookie = make_test_session(&auth_path); + let uri = format!("/api/archives/test/entries/{}", entry.entry_uid); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn list_runs_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/runs").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn list_runs_with_auth_returns_ok() { + 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/archives/test/runs").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn serve_artifact_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/artifacts/0").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn serve_artifact_with_auth_returns_ok() { + let dir = tempfile::tempdir().unwrap(); + let store_path = dir.path().join("store"); + let paths = archivr_core::archive::initialize_archive(dir.path(), &store_path, "test", false).unwrap(); + let artifact_relpath = "raw/a/u/page.html"; + let artifact_dir = store_path.join("raw").join("a").join("u"); + std::fs::create_dir_all(&artifact_dir).unwrap(); + std::fs::write(artifact_dir.join("page.html"), b"auth test").unwrap(); + let conn = database::open_or_initialize(&paths.archive_path).unwrap(); + let user_id = database::ensure_default_user(&conn).unwrap(); + let sid = database::upsert_source_identity(&conn, "web", "page", Some("auth-page"), Some("https://example.com/auth"), "https://example.com/auth").unwrap(); + let run = database::create_archive_run(&conn, user_id, 1).unwrap(); + let entry = database::create_archived_entry(&conn, &database::NewEntry { + source_identity_id: sid, 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("Auth Test Page".to_string()), visibility: "private".to_string(), + representation_kind: "html".to_string(), source_metadata_json: "{}".to_string(), display_metadata_json: None, + }).unwrap(); + let blob_id = database::upsert_blob(&conn, &database::BlobRecord { + sha256: "aaaa1111bbbb2222cccc3333dddd4444aaaa1111bbbb2222cccc3333dddd4444".to_string(), + byte_size: 21, mime_type: Some("text/html".to_string()), extension: Some("html".to_string()), + raw_relpath: artifact_relpath.to_string(), + }).unwrap(); + database::add_entry_artifact(&conn, &database::NewArtifact { + entry_id: entry.id, artifact_role: "primary_media".to_string(), + storage_area: "raw".to_string(), relpath: artifact_relpath.to_string(), + blob_id: Some(blob_id), logical_path: None, metadata_json: None, + }).unwrap(); + drop(conn); + 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 session_cookie = make_test_session(&auth_path); + 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 }; + let uri = format!("/api/archives/test/entries/{}/artifacts/0", entry.entry_uid); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn serve_entry_favicon_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/favicon").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn serve_entry_favicon_with_auth_returns_ok() { + let dir = tempfile::tempdir().unwrap(); + let store_path = dir.path().join("store"); + let paths = archivr_core::archive::initialize_archive(dir.path(), &store_path, "test", false).unwrap(); + let favicon_relpath = "raw/f/a/favicon.png"; + let favicon_dir = store_path.join("raw").join("f").join("a"); + std::fs::create_dir_all(&favicon_dir).unwrap(); + std::fs::write(favicon_dir.join("favicon.png"), &[0x89u8, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]).unwrap(); + let conn = database::open_or_initialize(&paths.archive_path).unwrap(); + let user_id = database::ensure_default_user(&conn).unwrap(); + let sid = database::upsert_source_identity(&conn, "web", "page", Some("fav-page"), Some("https://example.com/fav"), "https://example.com/fav").unwrap(); + let run = database::create_archive_run(&conn, user_id, 1).unwrap(); + let entry = database::create_archived_entry(&conn, &database::NewEntry { + source_identity_id: sid, 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("Favicon Test".to_string()), visibility: "private".to_string(), + representation_kind: "html".to_string(), source_metadata_json: "{}".to_string(), display_metadata_json: None, + }).unwrap(); + let blob_id = database::upsert_blob(&conn, &database::BlobRecord { + sha256: "ffffffffffff1111ffffffffffff1111ffffffffffff1111ffffffffffff1111".to_string(), + byte_size: 8, mime_type: Some("image/png".to_string()), extension: Some("png".to_string()), + raw_relpath: favicon_relpath.to_string(), + }).unwrap(); + database::add_entry_artifact(&conn, &database::NewArtifact { + entry_id: entry.id, artifact_role: "favicon".to_string(), + storage_area: "raw".to_string(), relpath: favicon_relpath.to_string(), + blob_id: Some(blob_id), logical_path: None, metadata_json: None, + }).unwrap(); + drop(conn); + 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 session_cookie = make_test_session(&auth_path); + 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 }; + let uri = format!("/api/archives/test/entries/{}/favicon", entry.entry_uid); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn serve_blob_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let sha256 = "0000000000000000000000000000000000000000000000000000000000000000"; + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&format!("/api/archives/test/blobs/{sha256}")).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn serve_blob_with_auth_returns_ok() { + let dir = tempfile::tempdir().unwrap(); + let store_path = dir.path().join("store"); + let paths = archivr_core::archive::initialize_archive(dir.path(), &store_path, "test", false).unwrap(); + let blob_relpath = "raw/b/l/data.bin"; + let blob_dir = store_path.join("raw").join("b").join("l"); + std::fs::create_dir_all(&blob_dir).unwrap(); + std::fs::write(blob_dir.join("data.bin"), b"blob content here").unwrap(); + let sha256 = "bbbb2222cccc4444bbbb2222cccc4444bbbb2222cccc4444bbbb2222cccc4444"; + let conn = database::open_or_initialize(&paths.archive_path).unwrap(); + database::upsert_blob(&conn, &database::BlobRecord { + sha256: sha256.to_string(), byte_size: 17, + mime_type: Some("application/octet-stream".to_string()), extension: Some("bin".to_string()), + raw_relpath: blob_relpath.to_string(), + }).unwrap(); + drop(conn); + 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 session_cookie = make_test_session(&auth_path); + 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 }; + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&format!("/api/archives/test/blobs/{sha256}")).header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn list_tags_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/tags").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn list_tags_with_auth_returns_ok() { + 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/archives/test/tags").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn list_entry_tags_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/tags").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn list_entry_tags_with_auth_returns_ok() { + let dir = tempfile::tempdir().unwrap(); + let (registry, archive_path, auth_path) = make_test_registry(&dir); + let entry = make_test_entry(&archive_path); + let session_cookie = make_test_session(&auth_path); + let uri = format!("/api/archives/test/entries/{}/tags", entry.entry_uid); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn list_collections_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/collections").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn list_collections_with_auth_returns_ok() { + 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/archives/test/collections").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn list_entry_collections_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/entries/fake_uid/collections").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn list_entry_collections_with_auth_returns_ok() { + let dir = tempfile::tempdir().unwrap(); + let (registry, archive_path, auth_path) = make_test_registry(&dir); + let entry = make_test_entry(&archive_path); + let session_cookie = make_test_session(&auth_path); + let uri = format!("/api/archives/test/entries/{}/collections", entry.entry_uid); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn get_collection_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/collections/coll_notexist").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn get_collection_with_auth_returns_ok() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let session_cookie = make_test_session(&auth_path); + let create_resp = app(registry.clone(), auth_path.clone()) + .oneshot(Request::builder().method("POST").uri("/api/archives/test/collections") + .header("content-type", "application/json").header("cookie", &session_cookie) + .body(json_body(&serde_json::json!({"name": "Auth Test Collection", "slug": "auth-test-coll", "default_visibility_bits": 2}))) + .unwrap()).await.unwrap(); + assert_eq!(create_resp.status(), StatusCode::CREATED); + let coll = body_json(create_resp).await; + let coll_uid = coll["collection_uid"].as_str().unwrap().to_string(); + let uri = format!("/api/archives/test/collections/{coll_uid}"); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri(&uri).header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + } From 5ce68b58f39269387b9e90de51e6c4088d901b23 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:13:31 +0200 Subject: [PATCH 5/9] feat(server): require auth on list_entries and search_entries_handler Both handlers accepted AuthUser but served guests. auth.require_auth()? added as first line closes the gap. Existing unauthenticated tests updated with session cookies. --- crates/archivr-server/src/routes.rs | 105 ++++++++++++++++++---------- 1 file changed, 68 insertions(+), 37 deletions(-) diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index 69b058c..18f12ff 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -285,6 +285,7 @@ async fn list_entries( auth: AuthUser, Path(archive_id): Path, ) -> Result>, ApiError> { + auth.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; let caller_bits = auth_to_caller_bits(&auth); @@ -297,6 +298,7 @@ async fn search_entries_handler( Path(archive_id): Path, Query(params): Query, ) -> Result>, ApiError> { + auth.require_auth()?; let mounted = mounted_archive(&state, &archive_id)?; let conn = database::open_or_initialize(&mounted.archive_path)?; let raw = params.q.as_deref().unwrap_or(""); @@ -1379,17 +1381,14 @@ mod tests { #[tokio::test] async fn missing_archive_returns_404() { - let (test_app, _dir) = make_test_app(); - let response = test_app - .oneshot( - Request::builder() - .uri("/api/archives/missing/entries") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - + 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 session_cookie = make_test_session(&auth_path); + let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/missing/entries").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -1585,16 +1584,14 @@ mod tests { #[tokio::test] async fn search_missing_archive_returns_404() { - let (test_app, _dir) = make_test_app(); - let response = test_app - .oneshot( - Request::builder() - .uri("/api/archives/nope/entries/search?q=anything") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); + 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 session_cookie = make_test_session(&auth_path); + let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/nope/entries/search?q=anything").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } @@ -1602,15 +1599,10 @@ mod tests { async fn search_empty_q_returns_ok() { 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/archives/test/entries/search") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); + .oneshot(Request::builder().uri("/api/archives/test/entries/search").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } @@ -1618,15 +1610,10 @@ mod tests { async fn search_unknown_prefix_returns_400() { 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/archives/test/entries/search?q=unknownprefix%3Aval") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); + .oneshot(Request::builder().uri("/api/archives/test/entries/search?q=unknownprefix%3Aval").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } @@ -2673,4 +2660,48 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + // ── Task 2: list_entries / search_entries auth enforcement ─────────────── + + #[tokio::test] + async fn list_entries_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/entries").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn list_entries_with_auth_returns_ok() { + 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/archives/test/entries").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn search_entries_requires_auth() { + let dir = tempfile::tempdir().unwrap(); + let (registry, _, auth_path) = make_test_registry(&dir); + let response = app(registry, auth_path) + .oneshot(Request::builder().uri("/api/archives/test/entries/search").body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn search_entries_with_auth_returns_ok() { + 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/archives/test/entries/search").header("cookie", &session_cookie).body(Body::empty()).unwrap()) + .await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + } + } From 7ed7cb882ff9969a87366a1f5b1fe8671de4fdeb Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:13:59 +0200 Subject: [PATCH 6/9] chore: update Cargo.lock after parking_lot dep addition --- Cargo.lock | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a0729aa..7c1d897 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,7 @@ dependencies = [ "axum-extra", "base64", "chrono", + "parking_lot", "rand", "rusqlite", "serde", @@ -1009,6 +1010,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.28" @@ -1158,6 +1168,29 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -1265,6 +1298,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.2" @@ -1431,6 +1473,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.7.0" From 71e176cbd1f51f43f8d7f0bf5145d70831df2a57 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:32:08 +0200 Subject: [PATCH 7/9] security: fix two Codex review findings + add port option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 1 — NixOS: store/ dir missing from ReadWritePaths archive_path points to .archivr/; captures write artifacts to the sibling store/ directory. Whitelisting only .archivr/ caused POST /captures to fail under ProtectSystem=strict. Fix: use builtins.dirOf a.path to whitelist the parent, which contains both .archivr/ and store/. Fix 2 — Rust: X-Forwarded-For was unconditionally trusted An attacker could send a different IP on every login attempt, bypassing LOGIN_MAX_ATTEMPTS entirely. Fix: use ConnectInfo (via into_make_service_with_connect_info) as the primary rate-limit key; XFF is trusted only when the TCP peer is loopback (i.e. a local reverse proxy). Tests without a real socket fall back to XFF unchanged. Port configuration: NixOS module: split 'bind' string into separate 'listenAddress' (default 127.0.0.1) and 'port' (default 8080) options. openFirewall now uses cfg.port directly instead of parsing it from the bind string. README NixOS example updated accordingly. --- crates/archivr-server/src/main.rs | 2 +- crates/archivr-server/src/routes.rs | 43 ++++++++++++++++++++++----- docs/README.md | 6 ++-- modules/nixos/archivr-server.nix | 46 +++++++++++++++++++---------- 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/crates/archivr-server/src/main.rs b/crates/archivr-server/src/main.rs index ab7366b..2e1b31b 100644 --- a/crates/archivr-server/src/main.rs +++ b/crates/archivr-server/src/main.rs @@ -63,6 +63,6 @@ async fn main() -> Result<()> { let listener = tokio::net::TcpListener::bind(addr).await?; println!("archivr-server listening on http://{addr}"); - axum::serve(listener, app).await?; + axum::serve(listener, app.into_make_service_with_connect_info::()).await?; Ok(()) } diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index 18f12ff..1d074f4 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -21,7 +21,7 @@ use std::{ collections::{HashMap, VecDeque}, - net::IpAddr, + net::{IpAddr, SocketAddr}, path::PathBuf, sync::Arc, time::{Duration, Instant}, @@ -31,7 +31,7 @@ use parking_lot::Mutex; use archivr_core::{archive, capture, database}; use axum::{ Json, Router, - extract::{Path, Query, Request, State}, + extract::{ConnectInfo, Path, Query, Request, State}, http::StatusCode, middleware::Next, response::{IntoResponse, Response}, @@ -165,12 +165,39 @@ async fn login_rate_limit( } fn extract_client_ip(req: &Request) -> IpAddr { - req.headers() - .get("x-forwarded-for") - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.split(',').next()) - .and_then(|s| s.trim().parse().ok()) - .unwrap_or(IpAddr::from([127, 0, 0, 1])) + // Attempt to read the real peer address injected by + // `into_make_service_with_connect_info` in main.rs. + let peer_ip: Option = req + .extensions() + .get::>() + .map(|ci| ci.0.ip()); + + match peer_ip { + // Peer is a loopback address → the connection came from a local + // reverse proxy (nginx/caddy on the same host). Trust the first + // address in X-Forwarded-For as the real client IP. + Some(peer) if peer.is_loopback() => req + .headers() + .get("x-forwarded-for") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.split(',').next()) + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(peer), + + // Peer is a real address → use it directly; ignoring X-Forwarded-For + // prevents header-spoofing attacks. + Some(peer) => peer, + + // No ConnectInfo present (unit tests using .oneshot() without a real + // socket). Fall back to XFF for test compatibility. + None => req + .headers() + .get("x-forwarded-for") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.split(',').next()) + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(IpAddr::from([127, 0, 0, 1])), + } } /// Build the Axum router from a pre-constructed `AppState`. diff --git a/docs/README.md b/docs/README.md index 1df2eed..5efdf7c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -154,7 +154,8 @@ enable the service: { services.archivr-server = { enable = true; - bind = "127.0.0.1:8080"; # loopback only; put nginx/caddy in front for TLS + # listenAddress defaults to "127.0.0.1" (loopback only) + # port defaults to 8080 archives = [ { id = "personal"; label = "Personal"; path = "/srv/archivr/personal/.archivr"; } { id = "work"; label = "Work"; path = "/srv/archivr/work/.archivr"; } @@ -180,7 +181,8 @@ Only needed when binding to a non-loopback address: ```nix services.archivr-server = { - bind = "0.0.0.0:8080"; + listenAddress = "0.0.0.0"; + port = 8080; # explicit, though 8080 is the default openFirewall = true; }; ``` diff --git a/modules/nixos/archivr-server.nix b/modules/nixos/archivr-server.nix index 05aebfb..1c26c36 100644 --- a/modules/nixos/archivr-server.nix +++ b/modules/nixos/archivr-server.nix @@ -6,10 +6,13 @@ let cfg = config.services.archivr-server; + # Derived bind string from separate address + port options. + bindStr = "${cfg.listenAddress}:${toString cfg.port}"; + # Generate the TOML registry file from NixOS options. # The auth DB is pinned to the StateDirectory so it survives upgrades. configFile = pkgs.writeText "archivr-server.toml" '' - bind = "${cfg.bind}" + bind = "${bindStr}" auth_db_path = "/var/lib/archivr-server/archivr-auth.sqlite" ${lib.concatMapStrings (a: '' @@ -32,17 +35,24 @@ in description = "The archivr-server package to use."; }; - bind = lib.mkOption { + listenAddress = lib.mkOption { type = lib.types.str; - default = "127.0.0.1:8080"; - example = "0.0.0.0:8080"; + default = "127.0.0.1"; + example = "0.0.0.0"; description = '' - Address and port to listen on. Defaults to loopback only. - Binding to a non-loopback address exposes the server on the network; - put a reverse proxy with TLS and authentication in front before doing so. + IP address to listen on. Defaults to loopback only (127.0.0.1). + Set to {code}`0.0.0.0` (or a specific interface address) to expose + the server on the network. Always put a reverse proxy with TLS + in front before doing this. ''; }; + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "TCP port to listen on."; + }; + archives = lib.mkOption { type = lib.types.listOf (lib.types.submodule { options = { @@ -59,7 +69,9 @@ in path = lib.mkOption { type = lib.types.str; example = "/srv/archivr/personal/.archivr"; - description = "Absolute path to the .archivr directory created by {command}`archivr init`."; + description = ''Absolute path to the .archivr directory created by {command}`archivr init`. + The parent directory (which also contains the {file}`store/` blob directory) + is whitelisted for read-write access under systemd hardening.''; }; }; }); @@ -92,9 +104,10 @@ in type = lib.types.bool; default = false; description = '' - Open the firewall TCP port derived from - {option}`services.archivr-server.bind`. Only meaningful when binding - to a non-loopback address. + Open the firewall TCP port specified by + {option}`services.archivr-server.port`. Only meaningful when + {option}`services.archivr-server.listenAddress` is set to a + non-loopback address. ''; }; }; @@ -134,11 +147,14 @@ in RestartSec = "5s"; # Hardening — make the entire FS read-only except for the state - # directory and the mounted archive directories. + # directory and the parent directory of each mounted archive. + # Each archive_path is an .archivr dir; its sibling store/ dir (where + # capture artifacts are written) lives at the same level. Whitelisting + # the parent covers both without over-permissioning. NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; - ReadWritePaths = [ "/var/lib/archivr-server" ] ++ (map (a: a.path) cfg.archives); + ReadWritePaths = [ "/var/lib/archivr-server" ] ++ (map (a: builtins.dirOf a.path) cfg.archives); RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; LockPersonality = true; RestrictNamespaces = true; @@ -148,9 +164,7 @@ in }; networking.firewall = lib.mkIf cfg.openFirewall { - allowedTCPPorts = [ - (lib.toInt (lib.last (lib.splitString ":" cfg.bind))) - ]; + allowedTCPPorts = [ cfg.port ]; }; }; } From 9544e707ed7f5acbd2a21ccdc99dc8144984b475 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:30:45 +0200 Subject: [PATCH 8/9] fix: address 6 code-review findings NixOS module (4 fixes): - Remove RestrictNamespaces=true: Chromium (launched by single-file for web captures) needs Linux user namespaces; blocking them broke captures - Escape TOML string values: labels/paths with quotes or backslashes would produce invalid archivr-server.toml and prevent startup - Bracket IPv6 listen addresses: ::1:8080 is rejected by Rust's SocketAddr parser; [::1]:8080 is the correct form (RFC 2732) - Add optional storePath per archive: covers archives initialised with a custom store path outside the default sibling store/ directory Rust (1 fix): - extract_client_ip loopback branch: take last XFF value not first. When a proxy appends to X-Forwarded-For an attacker controls the first entry; the last entry is always set by the trusted local proxy Frontend (1 fix): - Gate fetchArchives on authState === 'authenticated'. The empty-dep useEffect fired on mount (before login), hit 401, and never retried after login because authState was not in the dependency array --- crates/archivr-server/src/routes.rs | 8 +++++--- frontend/src/App.jsx | 6 ++++-- modules/nixos/archivr-server.nix | 32 +++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index 1d074f4..90d3a2f 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -174,13 +174,15 @@ fn extract_client_ip(req: &Request) -> IpAddr { match peer_ip { // Peer is a loopback address → the connection came from a local - // reverse proxy (nginx/caddy on the same host). Trust the first - // address in X-Forwarded-For as the real client IP. + // reverse proxy (nginx/caddy on the same host). Trust the last + // address in X-Forwarded-For as the real client IP — the last entry + // is always appended by the trusted proxy, even if the client sent a + // spoofed value earlier in the chain. Some(peer) if peer.is_loopback() => req .headers() .get("x-forwarded-for") .and_then(|v| v.to_str().ok()) - .and_then(|s| s.split(',').next()) + .and_then(|s| s.split(',').last()) .and_then(|s| s.trim().parse().ok()) .unwrap_or(peer), diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b49e053..1a3b758 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -70,8 +70,10 @@ export default function App() { } }, []) - // Mount: load archives, pick first, then parallel load + // Load archives once authenticated (re-runs when authState changes so + // it triggers correctly after first login or a session refresh). useEffect(() => { + if (authState !== 'authenticated') return fetchArchives().then(list => { setArchives(list) if (list.length > 0) { @@ -79,7 +81,7 @@ export default function App() { setArchiveId(first) } }) - }, []) + }, [authState]) // Archive change: parallel load entries + runs + tags useEffect(() => { diff --git a/modules/nixos/archivr-server.nix b/modules/nixos/archivr-server.nix index 1c26c36..935ba46 100644 --- a/modules/nixos/archivr-server.nix +++ b/modules/nixos/archivr-server.nix @@ -6,8 +6,16 @@ let cfg = config.services.archivr-server; + # Escape characters that would break TOML double-quoted strings. + escapeTOML = s: builtins.replaceStrings [ "\\" "\"" ] [ "\\\\" "\\\"" ] s; + # Derived bind string from separate address + port options. - bindStr = "${cfg.listenAddress}:${toString cfg.port}"; + # IPv6 addresses contain ":" and must be wrapped in brackets per RFC 2732. + bindStr = + let hasColon = builtins.match ".*:.*" cfg.listenAddress != null; + in if hasColon + then "[${cfg.listenAddress}]:${toString cfg.port}" + else "${cfg.listenAddress}:${toString cfg.port}"; # Generate the TOML registry file from NixOS options. # The auth DB is pinned to the StateDirectory so it survives upgrades. @@ -17,9 +25,9 @@ let ${lib.concatMapStrings (a: '' [[archives]] - id = "${a.id}" - label = "${a.label}" - archive_path = "${a.path}" + id = "${escapeTOML a.id}" + label = "${escapeTOML a.label}" + archive_path = "${escapeTOML a.path}" '') cfg.archives} ''; @@ -73,6 +81,16 @@ in The parent directory (which also contains the {file}`store/` blob directory) is whitelisted for read-write access under systemd hardening.''; }; + storePath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Absolute path to the store directory for this archive. Only needed + when the archive was initialised with a custom store path outside + the default sibling {file}`store/` location. When null (the default), + the parent of {option}`path` already covers the standard layout. + ''; + }; }; }); default = []; @@ -154,10 +172,12 @@ in NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; - ReadWritePaths = [ "/var/lib/archivr-server" ] ++ (map (a: builtins.dirOf a.path) cfg.archives); + ReadWritePaths = + [ "/var/lib/archivr-server" ] + ++ (map (a: builtins.dirOf a.path) cfg.archives) + ++ (lib.concatMap (a: lib.optional (a.storePath != null) a.storePath) cfg.archives); RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; LockPersonality = true; - RestrictNamespaces = true; RestrictSUIDSGID = true; RemoveIPC = true; }; From 120eccc03256e89250a37cdc98be246e5958024e Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:39:55 +0200 Subject: [PATCH 9/9] chore: rebuild frontend bundle JS bundle rehashed after frontend changes (App.jsx auth gating, ContextRail full tag path, Topbar tab reorder, Capture button copy). --- .../static/assets/{index-CWb0sQjJ.js => index-CbWD4KAy.js} | 2 +- crates/archivr-server/static/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename crates/archivr-server/static/assets/{index-CWb0sQjJ.js => index-CbWD4KAy.js} (98%) diff --git a/crates/archivr-server/static/assets/index-CWb0sQjJ.js b/crates/archivr-server/static/assets/index-CbWD4KAy.js similarity index 98% rename from crates/archivr-server/static/assets/index-CWb0sQjJ.js rename to crates/archivr-server/static/assets/index-CbWD4KAy.js index a804230..04d8937 100644 --- a/crates/archivr-server/static/assets/index-CWb0sQjJ.js +++ b/crates/archivr-server/static/assets/index-CbWD4KAy.js @@ -37,4 +37,4 @@ `+l[s].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=s&&0<=u);break}}}finally{zl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?zn(e):""}function qc(e){switch(e.tag){case 5:return zn(e.type);case 16:return zn("Lazy");case 13:return zn("Suspense");case 19:return zn("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 wu: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 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 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 Su(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function ed(e){var t=Su(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=ed(e))}function ku(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Su(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Br(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 Nu(e,t){t=t.checked,t!=null&&ts(e,"checked",t,!1)}function ai(e,t){Nu(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"||Br(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 Mn={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},td=["Webkit","ms","Moz","O"];Object.keys(Mn).forEach(function(e){td.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Mn[t]=Mn[e]})});function _u(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Mn.hasOwnProperty(e)&&Mn[e]?(""+t).trim():t+"px"}function Pu(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=_u(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var nd=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(nd[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(N(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(N(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(N(61))}if(t.style!=null&&typeof t.style!="object")throw Error(N(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(N(280));var t=e.stateNode;t&&(t=gl(t),vi(e.stateNode,e.type,t))}}function Tu(e){un?an?an.push(e):an=[e]:un=e}function Lu(){if(un){var e=un,t=an;if(an=un=null,bs(e),t)for(e=0;e>>=0,e===0?32:31-(pd(e)/hd|0)|0}var gr=64,yr=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 yd(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 Ju(e,t){switch(e){case"keyup":return Kd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Zu(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Jt=!1;function Xd(e,t){switch(e){case"compositionend":return Zu(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 Gd(e,t){if(Jt)return e==="compositionend"||!ps&&Ju(e,t)?(e=Xu(),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 ta(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ta(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function na(){for(var e=window,t=Br();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Br(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 lf(e){var t=na(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ta(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,Ni=!1;function vo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ni||Zt==null||Zt!==Br(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 B(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 Ne(e){return e=e.childContextTypes,e!=null}function Zr(){W(ke),W(ce)}function No(e,t,n){if(ce.current!==_t)throw Error(N(168));B(ce,t),B(ke,n)}function da(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(N(108,bc(e)||"Unknown",l));return X({},n,r)}function qr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||_t,At=ce.current,B(ce,e),B(ke,ke.current),!0}function jo(e,t,n){var r=e.stateNode;if(!r)throw Error(N(169));n?(e=da(e,t,At),r.__reactInternalMemoizedMergedChildContext=e,W(ke),W(ce),B(ce,e)):W(ke),B(ke,n)}var nt=null,yl=!1,Kl=!1;function fa(e){nt===null?nt=[e]:nt.push(e)}function gf(e){yl=!0,fa(e)}function Lt(){if(!Kl&&nt!==null){Kl=!0;var e=0,t=V;try{var n=nt;for(V=1;e>=s,l-=s,rt=1<<32-Ye(t)+l|n<z?(U=T,T=null):U=T.sibling;var D=h(p,T,f[z],g);if(D===null){T===null&&(T=U);break}e&&T&&D.alternate===null&&t(p,T),c=i(D,c,z),P===null?j=D:P.sibling=D,P=D,T=U}if(z===f.length)return n(p,T),Q&&Rt(p,z),j;if(T===null){for(;zz?(U=T,T=null):U=T.sibling;var J=h(p,T,D.value,g);if(J===null){T===null&&(T=U);break}e&&T&&J.alternate===null&&t(p,T),c=i(J,c,z),P===null?j=J:P.sibling=J,P=J,T=U}if(D.done)return n(p,T),Q&&Rt(p,z),j;if(T===null){for(;!D.done;z++,D=f.next())D=m(p,D.value,g),D!==null&&(c=i(D,c,z),P===null?j=D:P.sibling=D,P=D);return Q&&Rt(p,z),j}for(T=r(p,T);!D.done;z++,D=f.next())D=w(T,p,z,D.value,g),D!==null&&(e&&D.alternate!==null&&T.delete(D.key===null?z:D.key),c=i(D,c,z),P===null?j=D:P.sibling=D,P=D);return e&&T.forEach(function(Ce){return t(p,Ce)}),Q&&Rt(p,z),j}function C(p,c,f,g){if(typeof f=="object"&&f!==null&&f.type===Gt&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case hr:e:{for(var j=f.key,P=c;P!==null;){if(P.key===j){if(j=f.type,j===Gt){if(P.tag===7){n(p,P.sibling),c=l(P,f.props.children),c.return=p,p=c;break e}}else if(P.elementType===j||typeof j=="object"&&j!==null&&j.$$typeof===ft&&_o(j)===P.type){n(p,P.sibling),c=l(P,f.props),c.ref=Pn(p,P,f),c.return=p,p=c;break e}n(p,P);break}else t(p,P);P=P.sibling}f.type===Gt?(c=Ut(f.props.children,p.mode,g,f.key),c.return=p,p=c):(g=Vr(f.type,f.key,f.props,null,p.mode,g),g.ref=Pn(p,c,f),g.return=p,p=g)}return s(p);case Xt:e:{for(P=f.key;c!==null;){if(c.key===P)if(c.tag===4&&c.stateNode.containerInfo===f.containerInfo&&c.stateNode.implementation===f.implementation){n(p,c.sibling),c=l(c,f.children||[]),c.return=p,p=c;break e}else{n(p,c);break}else t(p,c);c=c.sibling}c=ei(f,p.mode,g),c.return=p,p=c}return s(p);case ft:return P=f._init,C(p,c,P(f._payload),g)}if(Rn(f))return x(p,c,f,g);if(Nn(f))return S(p,c,f,g);Cr(p,f)}return typeof f=="string"&&f!==""||typeof f=="number"?(f=""+f,c!==null&&c.tag===6?(n(p,c.sibling),c=l(c,f),c.return=p,p=c):(n(p,c),c=bl(f,p.mode,g),c.return=p,p=c),s(p)):n(p,c)}return C}var vn=va(!0),ga=va(!1),tl=Tt(null),nl=null,rn=null,ys=null;function ws(){ys=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,ys=rn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Se=!0),e.firstContext=null)}function $e(e){var t=e._currentValue;if(ys!==e)if(e={context:e,memoizedValue:t,next:null},rn===null){if(nl===null)throw Error(N(308));rn=e,nl.dependencies={lanes:0,firstContext:e}}else rn=rn.next=e;return t}var Ft=null;function Ss(e){Ft===null?Ft=[e]:Ft.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 wa(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,$&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 Fr(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{V=n,Xl.transition=r}}function Ma(){return Ie().memoizedState}function Sf(e,t,n){var r=jt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},$a(e))Ia(t,n);else if(n=ya(e,t,n,r),n!==null){var l=pe();Xe(n,e,r,l),Ua(n,t,r)}}function kf(e,t,n){var r=jt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if($a(e))Ia(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),Ua(n,t,r))}}function $a(e){var t=e.alternate;return e===Y||t!==null&&t===Y}function Ia(e,t){Vn=il=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ua(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,os(e,n)}}var sl={readContext:$e,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},Nf={readContext:$e,useCallback:function(e,t){return Ze().memoizedState=[e,t===void 0?null:t],e},useContext:$e,useEffect:zo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,$r(4194308,4,za.bind(null,t,e),n)},useLayoutEffect:function(e,t){return $r(4194308,4,e,t)},useInsertionEffect:function(e,t){return $r(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=Sf.bind(null,Y,e),[r.memoizedState,e]},useRef:function(e){var t=Ze();return e={current:e},t.memoizedState=e},useState:Lo,useDebugValue:Ls,useDeferredValue:function(e){return Ze().memoizedState=e},useTransition:function(){var e=Lo(!1),t=e[0];return e=xf.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(N(407));n=n()}else{if(n=t(),ne===null)throw Error(N(349));Bt&30||Na(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,zo(Ca.bind(null,r,i,e),[e]),r.flags|=2048,sr(9,ja.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,Ga(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,B(K,r?n&1|2:n&1),t):(ue(t),null);case 22:case 23:return Ms(),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(N(156,t.tag))}function zf(e,t){switch(vs(t),t.tag){case 1:return Ne(t.type)&&Zr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return gn(),W(ke),W(ce),Cs(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return js(t),null;case 13:if(W(K),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(N(340));mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return W(K),null;case 4:return gn(),null;case 10:return xs(t.type._context),null;case 22:case 23:return Ms(),null;case 24:return null;default:return null}}var _r=!1,ae=!1,Rf=typeof WeakSet=="function"?WeakSet:Set,L=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 Vi(e,t,n){try{n()}catch(r){G(e,t,r)}}var Bo=!1;function Of(e,t){if(ji=Yr,e=na(),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(Ci={focusedElem:e,selectionRange:n},Yr=!1,L=t;L!==null;)if(t=L,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,L=e;else for(;L!==null;){t=L;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,C=x.memoizedState,p=t.stateNode,c=p.getSnapshotBeforeUpdate(t.elementType===t.type?S:We(t.type,S),C);p.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(N(163))}}catch(g){G(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,L=e;break}L=t.return}return x=Bo,Bo=!1,x}function Bn(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&&Vi(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 Bi(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 qa(e){var t=e.alternate;t!==null&&(e.alternate=null,qa(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[mf],delete t[vf])),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 ba(e){return e.tag===5||e.tag===3||e.tag===4}function Ho(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||ba(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;)ec(e,t,n),n=n.sibling}function ec(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)&&Vi(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 Rf),t.forEach(function(r){var l=Bf.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*Ff(r/1960))-r,10e?16:e,gt===null)var r=!1;else{if(e=gt,gt=null,al=0,$&6)throw Error(N(331));var l=$;for($|=4,L=e.current;L!==null;){var i=L,s=i.child;if(L.flags&16){var u=i.deletions;if(u!==null){for(var a=0;aZ()-Ds?It(e,0):Os|=n),je(e,t)}function uc(e,t){t===0&&(e.mode&1?(t=yr,yr<<=1,!(yr&130023424)&&(yr=4194304)):t=1);var n=pe();e=ut(e,t),e!==null&&(ar(e,t,n),je(e,n))}function Vf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),uc(e,n)}function Bf(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(N(314))}r!==null&&r.delete(t),uc(e,n)}var ac;ac=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,Tf(e,t,n);Se=!!(e.flags&131072)}else Se=!1,Q&&t.flags&1048576&&pa(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,Ne(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=$i(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=Wf(r),e=We(r,e),l){case 0:t=Mi(null,t,r,e,n);break e;case 1:t=Uo(null,t,r,e,n);break e;case 11:t=$o(null,t,r,e,n);break e;case 14:t=Io(null,t,r,We(r.type,e),n);break e}throw Error(N(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:We(r,l),Mi(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(Ka(t),e===null)throw Error(N(387));r=t.pendingProps,i=t.memoizedState,l=i.element,wa(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=yn(Error(N(423)),t),t=Ao(e,t,r,n,l);break e}else if(r!==l){l=yn(Error(N(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=ga(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 xa(t),e===null&&zi(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),Qa(e,t),fe(e,t,s,n),t.child;case 6:return e===null&&zi(t),null;case 13:return Ya(e,t,n);case 4:return Ns(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),$o(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,B(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(N(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=$e(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 Ha(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,Ne(r)?(e=!0,qr(t)):e=!1,dn(t,n),Aa(t,r,l),Di(t,r,l,n),$i(null,t,r,!0,e,n);case 19:return Xa(e,t,n);case 22:return Wa(e,t,n)}throw Error(N(156,t.tag))};function cc(e,t){return $u(e,t)}function Hf(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 Fe(e,t,n,r){return new Hf(e,t,n,r)}function Is(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Wf(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 Ct(e,t){var n=e.alternate;return n===null?(n=Fe(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 Vr(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=Fe(12,n,t,l|2),e.elementType=li,e.lanes=i,e;case ii:return e=Fe(13,n,t,l),e.elementType=ii,e.lanes=i,e;case si:return e=Fe(19,n,t,l),e.elementType=si,e.lanes=i,e;case xu:return Nl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case yu:s=10;break e;case wu: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(N(130,e==null?e:typeof e,""))}return t=Fe(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Ut(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function Nl(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=xu,e.lanes=n,e.stateNode={isHidden:!1},e}function bl(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function ei(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Qf(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 Qf(e,t,n,u,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Fe(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 Kf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(hc)}catch(e){console.error(e)}}hc(),hu.exports=Le;var Zf=hu.exports,mc,qo=Zf;mc=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 qf(){return ve("/api/archives")}async function bf(e){return ve(`/api/archives/${e}/entries`)}async function ep(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 tp(e,t){return ve(`/api/archives/${e}/entries/${t}`)}async function ti(e,t){return ve(`/api/archives/${e}/entries/${t}/tags`)}async function np(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 rp(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 lp(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 ip(e,t){return ve(`/api/archives/${e}/capture_jobs/${t}`)}async function sp(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function op(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 up(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 ap(){await fetch("/api/auth/logout",{method:"POST"})}async function cp(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function dp(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 fp(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 pp(){return ve("/api/auth/tokens")}async function hp(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 mp(e){const t=await fetch(`/api/auth/tokens/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(await t.text())}async function vp(){return ve("/api/admin/instance-settings")}async function gp(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 yp(){return ve("/api/admin/users")}async function wp(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 xp(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 Sp(){return ve("/api/admin/roles")}async function kp(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 Np(e){return ve(`/api/archives/${e}/collections`)}async function jp(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 Ep(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 _p(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 Pp(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 Tp(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 Lp(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 zp=window.fetch;window.fetch=async(...e)=>{var n;const t=await zp(...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 Rp({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 up(t,r);e(m)}catch(m){s(m.message)}finally{a(!1)}}return o.jsx("div",{className:"login-page",children:o.jsxs("div",{className:"login-card",children:[o.jsx("h1",{className:"login-brand",children:"Archivr"}),o.jsx("p",{className:"login-tagline",children:"Sign in to your archive"}),o.jsxs("form",{onSubmit:d,children:[o.jsxs("div",{className:"login-field",children:[o.jsx("label",{className:"login-label",htmlFor:"login-username",children:"Username"}),o.jsx("input",{className:"login-input",id:"login-username",type:"text",value:t,onChange:v=>n(v.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),o.jsxs("div",{className:"login-field",children:[o.jsx("label",{className:"login-label",htmlFor:"login-password",children:"Password"}),o.jsx("input",{className:"login-input",id:"login-password",type:"password",value:r,onChange:v=>l(v.target.value),required:!0,autoComplete:"current-password"})]}),i&&o.jsx("p",{className:"login-error",children:i}),o.jsx("button",{className:"login-submit",type:"submit",disabled:u,children:u?"Signing in…":"Sign in"})]})]})})}function Op({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 op(t,r),e()}catch(w){a(w.message)}finally{v(!1)}}return o.jsx("div",{className:"setup-page",children:o.jsxs("div",{className:"setup-card",children:[o.jsx("h1",{className:"setup-brand",children:"Archivr"}),o.jsx("p",{className:"setup-tagline",children:"Create your owner account to get started."}),o.jsxs("form",{onSubmit:m,children:[o.jsxs("div",{className:"setup-field",children:[o.jsx("label",{className:"setup-label",htmlFor:"setup-username",children:"Username"}),o.jsx("input",{className:"setup-input",id:"setup-username",type:"text",value:t,onChange:h=>n(h.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),o.jsxs("div",{className:"setup-field",children:[o.jsx("label",{className:"setup-label",htmlFor:"setup-password",children:"Password"}),o.jsx("input",{className:"setup-input",id:"setup-password",type:"password",value:r,onChange:h=>l(h.target.value),required:!0,autoComplete:"new-password"})]}),o.jsxs("div",{className:"setup-field",children:[o.jsx("label",{className:"setup-label",htmlFor:"setup-confirm",children:"Confirm password"}),o.jsx("input",{className:"setup-input",id:"setup-confirm",type:"password",value:i,onChange:h=>s(h.target.value),required:!0,autoComplete:"new-password"})]}),u&&o.jsx("p",{className:"setup-error",children:u}),o.jsx("button",{className:"setup-submit",type:"submit",disabled:d,children:d?"Creating account…":"Create account"})]})]})})}function Dp({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 ap(),u(null),window.location.reload()}return o.jsxs("header",{className:"topbar",children:[o.jsx("div",{className:"brand",children:"Archivr"}),o.jsx("div",{className:"switcher",children:o.jsx("select",{"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:"user-name",children:s.display_name||s.username}),o.jsx("button",{className:"logout-btn",onClick:v,disabled:a,children:a?"Logging out…":"Log out"})]})]})}function Fp({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 C=l.current;if(!C)return;const p=()=>{clearInterval(w.current),n()};return C.addEventListener("close",p),()=>C.removeEventListener("close",p)},[n]),y.useEffect(()=>{const C=l.current;C&&(e?(s(""),a(null),h(null),v(!1),clearInterval(w.current),C.open||C.showModal()):C.open&&C.close())},[e]);async function x(){if(!i.trim()){a("Enter a locator.");return}v(!0),a(null),h(null);try{const C=await lp(t,i.trim());h("running"),w.current=setInterval(async()=>{var p;try{const c=await ip(t,C.job_uid);c.status==="completed"?(clearInterval(w.current),w.current=null,v(!1),h("completed"),(p=l.current)==null||p.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(C){a(C.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:C=>s(C.target.value),onKeyDown:C=>{C.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 C;return(C=l.current)==null?void 0:C.close()},children:"Cancel"}),o.jsx("button",{type:"button",className:"capture-submit",onClick:x,disabled:d,children:S()})]})]})})}function vc(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 yc(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:yc(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:gc(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:$t(e.title)||$t(e.entry_uid)})]}),o.jsx("div",{className:"col-type",children:o.jsx("span",{className:"type-pill",children:$t(e.entity_kind)})}),o.jsx("div",{className:"col-size",children:vc(e.total_artifact_bytes)}),o.jsx("div",{className:"url-cell col-url",children:$t(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 Ip(e){if(!e)return"—";try{return new Date(e).toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return e}}function Up({status:e}){const t=e==="completed"?"run-status--completed":e==="failed"?"run-status--failed":e==="running"?"run-status--running":"";return o.jsx("span",{className:`run-status ${t}`,children:e||"—"})}function Ap({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.length===0?o.jsx("tr",{children:o.jsx("td",{colSpan:5,style:{color:"var(--muted)",padding:"24px 16px",textAlign:"center"},children:"No runs yet."})}):e.map((t,n)=>o.jsxs("tr",{children:[o.jsx("td",{children:Ip(t.started_at)}),o.jsx("td",{children:o.jsx(Up,{status: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 Vp=4;function Bp({archives:e}){const{currentUser:t}=y.useContext(Pl)??{},n=t&&(t.role_bits&Vp)!==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,C]=y.useState(""),[p,c]=y.useState(""),[f,g]=y.useState(null),[j,P]=y.useState(!1),[T,z]=y.useState(""),[U,D]=y.useState(""),[J,Ce]=y.useState(null),[ge,ye]=y.useState(!1),Ue=y.useCallback(async()=>{if(n){v(!0),h(null);try{const[k,R]=await Promise.all([yp(),Sp()]);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 xp(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 Ve(k){if(k.preventDefault(),!w.trim()||!S){g("Username and password required");return}P(!0),g(null);try{await wp(w.trim(),S,p.trim()||void 0),x(""),C(""),c(""),await Ue()}catch(R){g(R.message)}finally{P(!1)}}async function E(k){if(k.preventDefault(),!T.trim()||!U.trim()){Ce("Slug and name required");return}ye(!0),Ce(null);try{await kp(T.trim(),U.trim()),z(""),D(""),await Ue()}catch(R){Ce(R.message)}finally{ye(!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:"view-tabs",children:[o.jsx("button",{className:`view-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),o.jsx("button",{className:`view-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),o.jsx("button",{className:`view-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),m&&o.jsx("div",{className:"form-msg form-msg--err",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:Ve,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=>C(k.target.value),required:!0}),o.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:p,onChange:k=>c(k.target.value)}),f&&o.jsx("div",{className:"form-msg form-msg--err",children:f}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:j,children:j?"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:T,onChange:k=>z(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:"form-msg form-msg--err",children:J}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:ge,children:ge?"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 wc({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(wc,{node:u,tagFilter:t,onTagFilterSet:n,onViewChange:r},u.tag.tag_uid))})})]})}function Hp({tagNodes:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){return o.jsx("section",{id:"tags-view",className:"view is-active",children:o.jsxs("div",{className:"tag-tree",children:[o.jsxs("div",{className:"tag-tree-header",children:[o.jsx("span",{className:"tag-tree-title",children:"Tags"}),t&&o.jsxs("span",{className:"tag-tree-active",children:["Filtering: ",t]})]}),e.length===0?o.jsx("p",{className:"muted",style:{padding:"8px 0"},children:"No tags yet."}):o.jsx("ul",{className:"tag-tree-list",children:e.map(l=>o.jsx(wc,{node:l,tagFilter:t,onTagFilterSet:n,onViewChange:r},l.tag.tag_uid))})]})})}const Fn=[{value:0,label:"Private"},{value:2,label:"Users only"},{value:3,label:"Public"}],Wp=e=>{var t;return((t=Fn.find(n=>n.value===e))==null?void 0:t.label)??String(e)};function Qp({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,C]=y.useState(""),[p,c]=y.useState(""),[f,g]=y.useState(2),[j,P]=y.useState(!1),[T,z]=y.useState(null),[U,D]=y.useState(""),[J,Ce]=y.useState(2),[ge,ye]=y.useState(!1),[Ue,Ae]=y.useState(null),[Ve,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_",F=y.useCallback(async()=>{if(e){l(!0),s(null);try{const _=await Np(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(()=>{F()},[F]),y.useEffect(()=>{se(u)},[u,se]),y.useEffect(()=>{Ve&&A.current&&A.current.focus()},[Ve]);async function Re(_){_.preventDefault();const I=S.trim(),Be=p.trim();if(!(!I||!Be)){P(!0),z(null);try{const zt=await jp(e,I,Be,f);C(""),c(""),g(2),await F(),a(zt.collection_uid)}catch(zt){z(zt.message)}finally{P(!1)}}}async function we(){const _=k.trim();if(!_||!O){E(!1);return}try{await eu(e,O.collection_uid,{name:_}),await F(),v(I=>I&&{...I,name:_})}catch(I){s(I.message)}finally{E(!1)}}async function xc(_){if(O)try{await eu(e,O.collection_uid,{default_visibility_bits:_}),await F(),v(I=>I&&{...I,default_visibility_bits:_})}catch(I){s(I.message)}}async function Sc(){if(O&&window.confirm(`Delete collection "${O.name}"? Entries will not be deleted.`))try{await Lp(e,O.collection_uid),a(null),v(null),await F()}catch(_){s(_.message)}}async function kc(_){_.preventDefault();const I=U.trim();if(!(!I||!O)){ye(!0),Ae(null);try{await Ep(e,O.collection_uid,I,J),D(""),await se(O.collection_uid)}catch(Be){Ae(Be.message)}finally{ye(!1)}}}async function Nc(_){if(O)try{await _p(e,O.collection_uid,_),await se(O.collection_uid)}catch(I){x(I.message)}}async function jc(_,I){if(O)try{await Pp(e,O.collection_uid,_,I),v(Be=>Be&&{...Be,entries:Be.entries.map(zt=>zt.entry_uid===_?{...zt,collection_visibility_bits:I}:zt)})}catch(Be){x(Be.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-sidebar-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:Wp(_.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:[Ve?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:Sc,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:_=>xc(Number(_.target.value)),disabled:de,children:Fn.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=>jc(_.entry_uid,Number(I.target.value)),children:Fn.map(I=>o.jsx("option",{value:I.value,children:I.label},I.value))}),!de&&o.jsx("button",{className:"coll-entry-remove",onClick:()=>Nc(_.entry_uid),title:"Remove from collection",children:"×"})]})]},_.entry_uid))}))]}),!de&&o.jsxs("form",{className:"coll-add-entry-form",onSubmit:kc,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:_=>Ce(Number(_.target.value)),children:Fn.map(_=>o.jsx("option",{value:_.value,children:_.label},_.value))}),o.jsx("button",{className:"coll-add-btn",type:"submit",disabled:ge,children:ge?"…":"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",children:[o.jsx("summary",{children:"+ Create collection"}),o.jsxs("form",{className:"coll-create-form",onSubmit:Re,children:[o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"coll-name",children:"Name"}),o.jsx("input",{className:"capture-input",id:"coll-name",type:"text",value:S,onChange:_=>{C(_.target.value),p||c(_.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"coll-slug",children:"Slug"}),o.jsx("input",{className:"capture-input",id:"coll-slug",type:"text",value:p,onChange:_=>c(_.target.value),placeholder:"my-collection",required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"coll-vis",children:"Default visibility"}),o.jsx("select",{className:"capture-input",id:"coll-vis",style:{height:42},value:f,onChange:_=>g(Number(_.target.value)),children:Fn.map(_=>o.jsx("option",{value:_.value,children:_.label},_.value))})]}),T&&o.jsx("div",{className:"collections-error",children:T}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:j,children:j?"Creating…":"Create collection"})]})]})]}):o.jsx("div",{className:"view-placeholder",children:"Select an archive."})}const Kp=4;function Yp(){const{currentUser:e,setCurrentUser:t}=y.useContext(Pl)??{},n=e&&(e.role_bits&Kp)!==0,[r,l]=y.useState("profile"),i=["profile","tokens",...n?["instance"]:[]],s={profile:"Profile",tokens:"API Tokens",instance:"Instance"};return o.jsxs("section",{className:"admin-view",children:[o.jsx("h1",{children:"Settings"}),o.jsx("div",{className:"view-tabs",children:i.map(u=>o.jsx("button",{className:`view-tab${r===u?" is-active":""}`,onClick:()=>l(u),children:s[u]},u))}),r==="profile"&&o.jsx(Xp,{currentUser:e,setCurrentUser:t}),r==="tokens"&&o.jsx(Gp,{}),r==="instance"&&n&&o.jsx(Jp,{})]})}function Xp({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),[C,p]=y.useState(null);async function c(g){g.preventDefault(),i(!0),u(null);try{await dp(n),t(j=>({...j,display_name:n||null})),u({ok:!0,text:"Saved."})}catch(j){u({ok:!1,text:j.message})}finally{i(!1)}}async function f(g){if(g.preventDefault(),v!==h){p({ok:!1,text:"Passwords do not match."});return}S(!0),p(null);try{await fp(a,v),d(""),m(""),w(""),p({ok:!0,text:"Password changed."})}catch(j){p({ok:!1,text:j.message})}finally{S(!1)}}return o.jsxs("div",{style:{maxWidth:440},children:[o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"Display Name"}),o.jsxs("form",{onSubmit:c,children:[o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"display-name",children:"Name shown in the UI"}),o.jsx("input",{className:"field-input",id:"display-name",placeholder:(e==null?void 0:e.username)??"",value:n,onChange:g=>r(g.target.value)})]}),s&&o.jsx("div",{className:`form-msg form-msg--${s.ok?"ok":"err"}`,children:s.text}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:l,children:l?"Saving…":"Save"})]})]}),o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"Change Password"}),o.jsxs("form",{onSubmit:f,children:[o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"cur-pw",children:"Current password"}),o.jsx("input",{className:"field-input",id:"cur-pw",type:"password",value:a,onChange:g=>d(g.target.value),required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"new-pw",children:"New password"}),o.jsx("input",{className:"field-input",id:"new-pw",type:"password",value:v,onChange:g=>m(g.target.value),required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"confirm-pw",children:"Confirm new password"}),o.jsx("input",{className:"field-input",id:"confirm-pw",type:"password",value:h,onChange:g=>w(g.target.value),required:!0})]}),C&&o.jsx("div",{className:`form-msg form-msg--${C.ok?"ok":"err"}`,children:C.text}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:x,children:x?"Changing…":"Change Password"})]})]})]})}function Gp(){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 pp())}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 C=await hp(s.trim());m(C),u(""),h()}catch(C){i(C.message)}finally{d(!1)}}}async function x(S){try{await mp(S),t(C=>C.filter(p=>p.token_uid!==S))}catch(C){i(C.message)}}return o.jsx("div",{style:{maxWidth:600},children:o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"API Tokens"}),v&&o.jsxs("div",{className:"token-banner",children:[o.jsx("strong",{children:"Token created."})," Copy it now — it won't be shown again.",o.jsx("code",{children:v.raw_token}),o.jsx("button",{className:"token-dismiss",onClick:()=>m(null),children:"Dismiss"})]}),o.jsxs("form",{className:"token-create-row",onSubmit:w,children:[o.jsx("input",{className:"field-input field-input--flex",placeholder:"Token name",value:s,onChange:S=>u(S.target.value),required:!0}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:a,children:a?"Creating…":"Create token"})]}),l&&o.jsx("div",{className:"form-msg form-msg--err",children:l}),n?o.jsx("div",{className:"muted",children:"Loading\\u2026"}):o.jsxs("div",{children:[e.length===0&&o.jsx("div",{className:"muted",children:"No tokens yet."}),e.map(S=>o.jsxs("div",{className:"token-row",children:[o.jsxs("div",{className:"token-row-info",children:[o.jsx("strong",{children:S.name}),o.jsxs("div",{className:"muted",children:["Created ",S.created_at.slice(0,10),S.last_used_at&&` · Last used ${S.last_used_at.slice(0,10)}`]})]}),o.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"4px 10px"},onClick:()=>x(S.token_uid),children:"Revoke"})]},S.token_uid))]})]})})}function Jp(){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 vp())}catch(m){i(m.message)}finally{r(!1)}})()},[]);async function v(m){m.preventDefault(),u(!0),d(null);try{await gp(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:"form-msg form-msg--err",children:l}):e?o.jsx("div",{style:{maxWidth:440},children:o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"Instance Settings"}),o.jsxs("form",{onSubmit:v,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",{className:"checkbox-row",children:[o.jsx("input",{type:"checkbox",checked:!!e[m],onChange:w=>t(x=>({...x,[m]:w.target.checked}))}),h]},m)),o.jsxs("div",{className:"form-field",style:{marginTop:4},children:[o.jsx("label",{className:"form-label",children:"Default entry visibility"}),o.jsxs("select",{className:"field-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"}),o.jsx("option",{value:2,children:"Unlisted"}),o.jsx("option",{value:3,children:"Public"})]})]}),a&&o.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:s,children:s?"Saving…":"Save Settings"})]})]})}):null}const nu={0:"Private",1:"Public",2:"Users only",3:"Public"},Zp=()=>o.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:o.jsx("path",{d:"M7 17 17 7M9 7h8v8"})});function qp({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 f=++S.current;s(null),a([]),Promise.all([tp(e,t.entry_uid),ti(e,t.entry_uid),Tp(e,t.entry_uid)]).then(([g,j,P])=>{f===S.current&&(s(g),a(j),h(P))}).catch(()=>{})},[t,e]);async function C(){const f=d.trim();if(!(!f||!t))try{await np(e,t.entry_uid,f),v(""),x("");const g=await ti(e,t.entry_uid);a(g),l()}catch(g){x(g.message)}}async function p(f){try{await rp(e,t.entry_uid,f);const g=await ti(e,t.entry_uid);a(g),l()}catch{}}const c=i?[["Added",gc(i.summary.archived_at)],["Source",i.summary.source_kind],["Type",i.summary.entity_kind],["Visibility",nu[i.summary.visibility]??i.summary.visibility],["Root",i.structured_root_relpath]]:[];return o.jsxs("aside",{className:"context-rail",children:[o.jsx("div",{className:"rail-eyebrow",children:"Context"}),t?i?o.jsxs(o.Fragment,{children:[o.jsx("h2",{className:"rail-title",children:$t(i.summary.title)||$t(i.summary.entry_uid)}),i.summary.original_url&&o.jsxs("a",{className:"url-tile",href:i.summary.original_url,target:"_blank",rel:"noopener noreferrer",children:[o.jsx("span",{className:"ico",dangerouslySetInnerHTML:{__html:yc(i.summary.source_kind)}}),o.jsx("span",{className:"u-text",children:i.summary.original_url}),o.jsx("span",{className:"ext",children:o.jsx(Zp,{})})]}),o.jsx("div",{className:"meta-list",children:c.filter(([,f])=>f!=null&&f!=="").map(([f,g])=>o.jsxs("div",{className:"meta-item",children:[o.jsx("span",{className:"meta-k",children:f}),o.jsx("span",{className:`meta-v${f==="Root"?" mono":""}`,children:$t(g)})]},f))}),i.artifacts.length>0&&o.jsxs("div",{className:"rail-section",children:[o.jsxs("div",{className:"rail-section-heading",children:["Artifacts ",o.jsx("span",{className:"num",children:i.artifacts.length})]}),o.jsx("ul",{className:"artifact-list",children:i.artifacts.map((f,g)=>o.jsx("li",{children:o.jsxs("a",{href:`/api/archives/${e}/entries/${i.summary.entry_uid}/artifacts/${g}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[o.jsx("span",{className:"artifact-name",children:f.artifact_role.replace(/_/g," ")}),o.jsx("span",{className:"artifact-size",children:f.byte_size!=null?vc(f.byte_size):"—"})]})},g))})]})]}):o.jsx("p",{className:"tags-empty",children:"Loading…"}):o.jsx("p",{className:"tags-empty",children:"Select an entry."}),t&&o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"rail-section",children:[o.jsx("div",{className:"rail-section-heading",children:"Tags"}),u.length===0?o.jsx("p",{className:"tags-empty",children:"No tags yet."}):o.jsx("div",{className:"tags-wrap",children:u.map(f=>o.jsxs("span",{className:"tag-pill",title:f.full_path,children:[f.name,o.jsx("button",{className:"remove",title:`Remove tag ${f.full_path}`,onClick:()=>p(f.tag_uid),children:"×"})]},f.tag_uid))}),w&&o.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:w}),o.jsxs("div",{className:"tag-input-wrap",children:[o.jsx("span",{className:"hash",children:"/"}),o.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:d,onChange:f=>v(f.target.value),onKeyDown:f=>{f.key==="Enter"&&C()}}),o.jsx("button",{className:"tag-add-btn",onClick:C,children:"Add"})]})]}),m.length>0&&o.jsxs("div",{className:"rail-section",children:[o.jsx("div",{className:"rail-section-heading",children:"Collections"}),m.map(f=>o.jsxs("div",{className:"coll-row",children:[o.jsx("span",{className:"coll-name",children:f.collection_uid}),o.jsx("span",{className:"vis-badge",children:nu[f.visibility_bits]??`bits:${f.visibility_bits}`})]},f.collection_uid))]})]})]})}const Pl=y.createContext(null);function bp(){const[e,t]=y.useState("loading"),[n,r]=y.useState(null);y.useEffect(()=>{(async()=>{if(await sp()){t("setup");return}const se=await cp();if(!se){t("login");return}r(se),t("authenticated")})()},[]),y.useEffect(()=>{const F=()=>{r(null),t("login")};return window.addEventListener("auth:expired",F),()=>window.removeEventListener("auth:expired",F)},[]);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),[C,p]=y.useState("archive"),[c,f]=y.useState(""),[g,j]=y.useState(""),[P,T]=y.useState(!1),[z,U]=y.useState([]),[D,J]=y.useState([]),[Ce,ge]=y.useState(!1),ye=y.useCallback(async(F,se,Re)=>{if(F){T(!0);try{let we;se||Re?we=await ep(F,se,Re):we=await bf(F),d(we),j(we.length===0?"No results":`${we.length} result${we.length===1?"":"s"}`)}catch{d([]),j("Search failed. Try again.")}finally{T(!1)}}},[]);y.useEffect(()=>{qf().then(F=>{if(i(F),F.length>0){const se=F[0].id;u(se)}})},[]),y.useEffect(()=>{s&&(S(null),w(null),m(null),Promise.all([ye(s,"",null),bo(s).then(U),ni(s).then(J)]))},[s]),y.useEffect(()=>{if(s===null)return;const F=setTimeout(()=>{ye(s,c,x)},300);return()=>clearTimeout(F)},[c,s]),y.useEffect(()=>{s!==null&&(p("archive"),ye(s,c,x))},[x,s]);const Ue=y.useCallback(F=>{u(F)},[]),Ae=y.useCallback(F=>{p(F),F==="tags"&&s&&ni(s).then(J)},[s]),Ve=y.useCallback(F=>{m(F?F.entry_uid:null),w(F)},[]),E=y.useCallback(F=>{S(F)},[]),k=y.useCallback(()=>{S(null)},[]),R=y.useCallback(()=>{s&&ni(s).then(J)},[s]),A=y.useCallback(()=>{ge(!0)},[]),O=y.useCallback(()=>{ge(!1)},[]),de=y.useCallback(()=>{s&&Promise.all([ye(s,c,x),bo(s).then(U)])},[s,c,x,ye]);return e==="loading"?o.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?o.jsx(Op,{onComplete:()=>t("login")}):e==="login"?o.jsx(Rp,{onLogin:F=>{r(F),t("authenticated")}}):o.jsx(Pl.Provider,{value:{currentUser:n,setCurrentUser:r},children:o.jsxs(o.Fragment,{children:[o.jsx(Dp,{archives:l,archiveId:s,onArchiveChange:Ue,view:C,onViewChange:Ae,onCaptureClick:A}),o.jsxs("main",{className:"app-shell",children:[o.jsxs("div",{className:"workspace",children:[C==="archive"&&o.jsxs("div",{className:"toolbar",children:[o.jsxs("div",{className:"search-field",children:[o.jsx("span",{className:"ico","aria-hidden":"true",children:o.jsxs("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[o.jsx("circle",{cx:"11",cy:"11",r:"7"}),o.jsx("line",{x1:"16.5",y1:"16.5",x2:"21",y2:"21"})]})}),o.jsx("input",{className:"search-input",type:"search","aria-label":"Search archive","aria-busy":P,placeholder:"Search titles, URLs, types, tags…",value:c,onChange:F=>f(F.target.value)}),o.jsx("span",{className:"kbd",children:"⌘K"})]}),o.jsxs("span",{className:"result-count",children:[g&&o.jsxs(o.Fragment,{children:[o.jsx("b",{children:g.split(" ")[0]})," ",g.split(" ").slice(1).join(" ")]}),x&&o.jsxs("button",{className:"tag-filter-badge",onClick:k,children:["× ",x]})]})]}),C==="archive"&&o.jsx($p,{entries:a,selectedEntryUid:v,onSelectEntry:Ve,archiveId:s,tagFilter:x,onClearTagFilter:k,searchQuery:c,onSearchChange:f,resultCount:g,searchBusy:P}),C==="runs"&&o.jsx(Ap,{runs:z}),C==="admin"&&o.jsx(Bp,{archives:l}),C==="tags"&&o.jsx(Hp,{tagNodes:D,tagFilter:x,onTagFilterSet:E,onViewChange:Ae}),C==="collections"&&o.jsx(Qp,{archiveId:s}),C==="settings"&&o.jsx(Yp,{})]}),o.jsx(qp,{archiveId:s,selectedEntry:h,onTagFilterSet:E,tagNodes:D,onTagsRefresh:R})]}),o.jsx(Fp,{open:Ce,archiveId:s,onClose:O,onCaptured:de})]})})}mc(document.getElementById("root")).render(o.jsx(y.StrictMode,{children:o.jsx(bp,{})})); +`+i.stack}return{value:e,source:t,stack:l,digest:null}}function Zl(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Fi(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Ef=typeof WeakMap=="function"?WeakMap:Map;function Va(e,t,n){n=it(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){ul||(ul=!0,Qi=r),Fi(e,t)},n}function Ba(e,t,n){n=it(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Fi(e,t)}}var i=e.stateNode;return i!==null&&typeof i.componentDidCatch=="function"&&(n.callback=function(){Fi(e,t),typeof r!="function"&&(Nt===null?Nt=new Set([this]):Nt.add(this));var s=t.stack;this.componentDidCatch(t.value,{componentStack:s!==null?s:""})}),n}function Do(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Ef;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=Af.bind(null,e,t,n),t.then(e,e))}function Fo(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Mo(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=it(-1,1),t.tag=2,kt(n,t,1))),n.lanes|=1),e)}var _f=ct.ReactCurrentOwner,Se=!1;function fe(e,t,n,r){t.child=e===null?ga(t,null,n,r):vn(t,e.child,n,r)}function $o(e,t,n,r,l){n=n.render;var i=t.ref;return dn(t,l),r=_s(e,t,n,r,i,l),n=Ps(),e!==null&&!Se?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&n&&ms(t),t.flags|=1,fe(e,t,r,l),t.child)}function Io(e,t,n,r,l){if(e===null){var i=n.type;return typeof i=="function"&&!Is(i)&&i.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=i,Ha(e,t,i,r,l)):(e=Vr(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(i=e.child,!(e.lanes&l)){var s=i.memoizedProps;if(n=n.compare,n=n!==null?n:qn,n(s,r)&&e.ref===t.ref)return at(e,t,l)}return t.flags|=1,e=Ct(i,r),e.ref=t.ref,e.return=t,t.child=e}function Ha(e,t,n,r,l){if(e!==null){var i=e.memoizedProps;if(qn(i,r)&&e.ref===t.ref)if(Se=!1,t.pendingProps=r=i,(e.lanes&l)!==0)e.flags&131072&&(Se=!0);else return t.lanes=e.lanes,at(e,t,l)}return Mi(e,t,n,r,l)}function Wa(e,t,n){var r=t.pendingProps,l=r.children,i=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},B(sn,Ee),Ee|=n;else{if(!(n&1073741824))return e=i!==null?i.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,B(sn,Ee),Ee|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=i!==null?i.baseLanes:n,B(sn,Ee),Ee|=r}else i!==null?(r=i.baseLanes|n,t.memoizedState=null):r=n,B(sn,Ee),Ee|=r;return fe(e,t,l,n),t.child}function Qa(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Mi(e,t,n,r,l){var i=Ne(n)?At:ce.current;return i=hn(t,i),dn(t,l),n=_s(e,t,n,r,i,l),r=Ps(),e!==null&&!Se?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,at(e,t,l)):(Q&&r&&ms(t),t.flags|=1,fe(e,t,n,l),t.child)}function Uo(e,t,n,r,l){if(Ne(n)){var i=!0;qr(t)}else i=!1;if(dn(t,l),t.stateNode===null)Ir(e,t),Aa(t,n,r),Di(t,n,r,l),r=!0;else if(e===null){var s=t.stateNode,u=t.memoizedProps;s.props=u;var a=s.context,d=n.contextType;typeof d=="object"&&d!==null?d=$e(d):(d=Ne(n)?At:ce.current,d=hn(t,d));var v=n.getDerivedStateFromProps,m=typeof v=="function"||typeof s.getSnapshotBeforeUpdate=="function";m||typeof s.UNSAFE_componentWillReceiveProps!="function"&&typeof s.componentWillReceiveProps!="function"||(u!==r||a!==d)&&Oo(t,s,r,d),pt=!1;var h=t.memoizedState;s.state=h,rl(t,r,s,l),a=t.memoizedState,u!==r||h!==a||ke.current||pt?(typeof v=="function"&&(Oi(t,n,v,r),a=t.memoizedState),(u=pt||Ro(t,n,u,r,h,a,d))?(m||typeof s.UNSAFE_componentWillMount!="function"&&typeof s.componentWillMount!="function"||(typeof s.componentWillMount=="function"&&s.componentWillMount(),typeof s.UNSAFE_componentWillMount=="function"&&s.UNSAFE_componentWillMount()),typeof s.componentDidMount=="function"&&(t.flags|=4194308)):(typeof s.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=a),s.props=r,s.state=a,s.context=d,r=u):(typeof s.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{s=t.stateNode,wa(e,t),u=t.memoizedProps,d=t.type===t.elementType?u:We(t.type,u),s.props=d,m=t.pendingProps,h=s.context,a=n.contextType,typeof a=="object"&&a!==null?a=$e(a):(a=Ne(n)?At:ce.current,a=hn(t,a));var w=n.getDerivedStateFromProps;(v=typeof w=="function"||typeof s.getSnapshotBeforeUpdate=="function")||typeof s.UNSAFE_componentWillReceiveProps!="function"&&typeof s.componentWillReceiveProps!="function"||(u!==m||h!==a)&&Oo(t,s,r,a),pt=!1,h=t.memoizedState,s.state=h,rl(t,r,s,l);var x=t.memoizedState;u!==m||h!==x||ke.current||pt?(typeof w=="function"&&(Oi(t,n,w,r),x=t.memoizedState),(d=pt||Ro(t,n,d,r,h,x,a)||!1)?(v||typeof s.UNSAFE_componentWillUpdate!="function"&&typeof s.componentWillUpdate!="function"||(typeof s.componentWillUpdate=="function"&&s.componentWillUpdate(r,x,a),typeof s.UNSAFE_componentWillUpdate=="function"&&s.UNSAFE_componentWillUpdate(r,x,a)),typeof s.componentDidUpdate=="function"&&(t.flags|=4),typeof s.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof s.componentDidUpdate!="function"||u===e.memoizedProps&&h===e.memoizedState||(t.flags|=4),typeof s.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&h===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=x),s.props=r,s.state=x,s.context=a,r=d):(typeof s.componentDidUpdate!="function"||u===e.memoizedProps&&h===e.memoizedState||(t.flags|=4),typeof s.getSnapshotBeforeUpdate!="function"||u===e.memoizedProps&&h===e.memoizedState||(t.flags|=1024),r=!1)}return $i(e,t,n,r,i,l)}function $i(e,t,n,r,l,i){Qa(e,t);var s=(t.flags&128)!==0;if(!r&&!s)return l&&jo(t,n,!1),at(e,t,i);r=t.stateNode,_f.current=t;var u=s&&typeof n.getDerivedStateFromError!="function"?null:r.render();return t.flags|=1,e!==null&&s?(t.child=vn(t,e.child,null,i),t.child=vn(t,null,u,i)):fe(e,t,u,i),t.memoizedState=r.state,l&&jo(t,n,!0),t.child}function Ka(e){var t=e.stateNode;t.pendingContext?No(e,t.pendingContext,t.pendingContext!==t.context):t.context&&No(e,t.context,!1),Ns(e,t.containerInfo)}function Ao(e,t,n,r,l){return mn(),gs(l),t.flags|=256,fe(e,t,n,r),t.child}var Ii={dehydrated:null,treeContext:null,retryLane:0};function Ui(e){return{baseLanes:e,cachePool:null,transitions:null}}function Ya(e,t,n){var r=t.pendingProps,l=K.current,i=!1,s=(t.flags&128)!==0,u;if((u=s)||(u=e!==null&&e.memoizedState===null?!1:(l&2)!==0),u?(i=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),B(K,l&1),e===null)return zi(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data==="$!"?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(s=r.children,e=r.fallback,i?(r=t.mode,i=t.child,s={mode:"hidden",children:s},!(r&1)&&i!==null?(i.childLanes=0,i.pendingProps=s):i=Nl(s,r,0,null),e=Ut(e,r,n,null),i.return=t,e.return=t,i.sibling=e,t.child=i,t.child.memoizedState=Ui(n),t.memoizedState=Ii,e):zs(t,s));if(l=e.memoizedState,l!==null&&(u=l.dehydrated,u!==null))return Pf(e,t,s,r,u,l,n);if(i){i=r.fallback,s=t.mode,l=e.child,u=l.sibling;var a={mode:"hidden",children:r.children};return!(s&1)&&t.child!==l?(r=t.child,r.childLanes=0,r.pendingProps=a,t.deletions=null):(r=Ct(l,a),r.subtreeFlags=l.subtreeFlags&14680064),u!==null?i=Ct(u,i):(i=Ut(i,s,n,null),i.flags|=2),i.return=t,r.return=t,r.sibling=i,t.child=r,r=i,i=t.child,s=e.child.memoizedState,s=s===null?Ui(n):{baseLanes:s.baseLanes|n,cachePool:null,transitions:s.transitions},i.memoizedState=s,i.childLanes=e.childLanes&~n,t.memoizedState=Ii,r}return i=e.child,e=i.sibling,r=Ct(i,{mode:"visible",children:r.children}),!(t.mode&1)&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function zs(e,t){return t=Nl({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function Er(e,t,n,r){return r!==null&&gs(r),vn(t,e.child,null,n),e=zs(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Pf(e,t,n,r,l,i,s){if(n)return t.flags&256?(t.flags&=-257,r=Zl(Error(N(422))),Er(e,t,s,r)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(i=r.fallback,l=t.mode,r=Nl({mode:"visible",children:r.children},l,0,null),i=Ut(i,l,s,null),i.flags|=2,r.return=t,i.return=t,r.sibling=i,t.child=r,t.mode&1&&vn(t,e.child,null,s),t.child.memoizedState=Ui(s),t.memoizedState=Ii,i);if(!(t.mode&1))return Er(e,t,s,null);if(l.data==="$!"){if(r=l.nextSibling&&l.nextSibling.dataset,r)var u=r.dgst;return r=u,i=Error(N(419)),r=Zl(i,r,void 0),Er(e,t,s,r)}if(u=(s&e.childLanes)!==0,Se||u){if(r=ne,r!==null){switch(s&-s){case 4:l=2;break;case 16:l=8;break;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:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=l&(r.suspendedLanes|s)?0:l,l!==0&&l!==i.retryLane&&(i.retryLane=l,ut(e,l),Xe(r,e,l,-1))}return $s(),r=Zl(Error(N(421))),Er(e,t,s,r)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=Vf.bind(null,e),l._reactRetry=t,null):(e=i.treeContext,_e=St(l.nextSibling),Pe=t,Q=!0,Ke=null,e!==null&&(Oe[De++]=rt,Oe[De++]=lt,Oe[De++]=Vt,rt=e.id,lt=e.overflow,Vt=t),t=zs(t,r.children),t.flags|=4096,t)}function Vo(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),Ri(e.return,t,n)}function ql(e,t,n,r,l){var i=e.memoizedState;i===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:l}:(i.isBackwards=t,i.rendering=null,i.renderingStartTime=0,i.last=r,i.tail=n,i.tailMode=l)}function Xa(e,t,n){var r=t.pendingProps,l=r.revealOrder,i=r.tail;if(fe(e,t,r.children,n),r=K.current,r&2)r=r&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Vo(e,n,t);else if(e.tag===19)Vo(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(B(K,r),!(t.mode&1))t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&ll(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),ql(t,!1,l,n,i);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&ll(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}ql(t,!0,n,null,i);break;case"together":ql(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function Ir(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function at(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),Ht|=t.lanes,!(n&t.childLanes))return null;if(e!==null&&t.child!==e.child)throw Error(N(153));if(t.child!==null){for(e=t.child,n=Ct(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Ct(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function Tf(e,t,n){switch(t.tag){case 3:Ka(t),mn();break;case 5:xa(t);break;case 1:Ne(t.type)&&qr(t);break;case 4:Ns(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,l=t.memoizedProps.value;B(tl,r._currentValue),r._currentValue=l;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated!==null?(B(K,K.current&1),t.flags|=128,null):n&t.child.childLanes?Ya(e,t,n):(B(K,K.current&1),e=at(e,t,n),e!==null?e.sibling:null);B(K,K.current&1);break;case 19:if(r=(n&t.childLanes)!==0,e.flags&128){if(r)return Xa(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),B(K,K.current),r)break;return null;case 22:case 23:return t.lanes=0,Wa(e,t,n)}return at(e,t,n)}var Ga,Ai,Ja,Za;Ga=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}};Ai=function(){};Ja=function(e,t,n,r){var l=e.memoizedProps;if(l!==r){e=t.stateNode,Mt(et.current);var i=null;switch(n){case"input":l=ui(e,l),r=ui(e,r),i=[];break;case"select":l=X({},l,{value:void 0}),r=X({},r,{value:void 0}),i=[];break;case"textarea":l=di(e,l),r=di(e,r),i=[];break;default:typeof l.onClick!="function"&&typeof r.onClick=="function"&&(e.onclick=Jr)}pi(n,r);var s;n=null;for(d in l)if(!r.hasOwnProperty(d)&&l.hasOwnProperty(d)&&l[d]!=null)if(d==="style"){var u=l[d];for(s in u)u.hasOwnProperty(s)&&(n||(n={}),n[s]="")}else d!=="dangerouslySetInnerHTML"&&d!=="children"&&d!=="suppressContentEditableWarning"&&d!=="suppressHydrationWarning"&&d!=="autoFocus"&&(Qn.hasOwnProperty(d)?i||(i=[]):(i=i||[]).push(d,null));for(d in r){var a=r[d];if(u=l!=null?l[d]:void 0,r.hasOwnProperty(d)&&a!==u&&(a!=null||u!=null))if(d==="style")if(u){for(s in u)!u.hasOwnProperty(s)||a&&a.hasOwnProperty(s)||(n||(n={}),n[s]="");for(s in a)a.hasOwnProperty(s)&&u[s]!==a[s]&&(n||(n={}),n[s]=a[s])}else n||(i||(i=[]),i.push(d,n)),n=a;else d==="dangerouslySetInnerHTML"?(a=a?a.__html:void 0,u=u?u.__html:void 0,a!=null&&u!==a&&(i=i||[]).push(d,a)):d==="children"?typeof a!="string"&&typeof a!="number"||(i=i||[]).push(d,""+a):d!=="suppressContentEditableWarning"&&d!=="suppressHydrationWarning"&&(Qn.hasOwnProperty(d)?(a!=null&&d==="onScroll"&&H("scroll",e),i||u===a||(i=[])):(i=i||[]).push(d,a))}n&&(i=i||[]).push("style",n);var d=i;(t.updateQueue=d)&&(t.flags|=4)}};Za=function(e,t,n,r){n!==r&&(t.flags|=4)};function Tn(e,t){if(!Q)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function ue(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags&14680064,r|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags,r|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Lf(e,t,n){var r=t.pendingProps;switch(vs(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return ue(t),null;case 1:return Ne(t.type)&&Zr(),ue(t),null;case 3:return r=t.stateNode,gn(),W(ke),W(ce),Cs(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),(e===null||e.child===null)&&(jr(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,Ke!==null&&(Xi(Ke),Ke=null))),Ai(e,t),ue(t),null;case 5:js(t);var l=Mt(rr.current);if(n=t.type,e!==null&&t.stateNode!=null)Ja(e,t,n,r,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(t.stateNode===null)throw Error(N(166));return ue(t),null}if(e=Mt(et.current),jr(t)){r=t.stateNode,n=t.type;var i=t.memoizedProps;switch(r[qe]=t,r[tr]=i,e=(t.mode&1)!==0,n){case"dialog":H("cancel",r),H("close",r);break;case"iframe":case"object":case"embed":H("load",r);break;case"video":case"audio":for(l=0;l<\/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,Ga(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,B(K,r?n&1|2:n&1),t):(ue(t),null);case 22:case 23:return Ms(),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(N(156,t.tag))}function zf(e,t){switch(vs(t),t.tag){case 1:return Ne(t.type)&&Zr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return gn(),W(ke),W(ce),Cs(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return js(t),null;case 13:if(W(K),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(N(340));mn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return W(K),null;case 4:return gn(),null;case 10:return xs(t.type._context),null;case 22:case 23:return Ms(),null;case 24:return null;default:return null}}var _r=!1,ae=!1,Rf=typeof WeakSet=="function"?WeakSet:Set,L=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 Vi(e,t,n){try{n()}catch(r){G(e,t,r)}}var Bo=!1;function Of(e,t){if(ji=Yr,e=na(),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(Ci={focusedElem:e,selectionRange:n},Yr=!1,L=t;L!==null;)if(t=L,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,L=e;else for(;L!==null;){t=L;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,C=x.memoizedState,p=t.stateNode,c=p.getSnapshotBeforeUpdate(t.elementType===t.type?S:We(t.type,S),C);p.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(N(163))}}catch(g){G(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,L=e;break}L=t.return}return x=Bo,Bo=!1,x}function Bn(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&&Vi(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 Bi(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 qa(e){var t=e.alternate;t!==null&&(e.alternate=null,qa(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[mf],delete t[vf])),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 ba(e){return e.tag===5||e.tag===3||e.tag===4}function Ho(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||ba(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;)ec(e,t,n),n=n.sibling}function ec(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)&&Vi(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 Rf),t.forEach(function(r){var l=Bf.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*Ff(r/1960))-r,10e?16:e,gt===null)var r=!1;else{if(e=gt,gt=null,al=0,$&6)throw Error(N(331));var l=$;for($|=4,L=e.current;L!==null;){var i=L,s=i.child;if(L.flags&16){var u=i.deletions;if(u!==null){for(var a=0;aZ()-Ds?It(e,0):Os|=n),je(e,t)}function uc(e,t){t===0&&(e.mode&1?(t=yr,yr<<=1,!(yr&130023424)&&(yr=4194304)):t=1);var n=pe();e=ut(e,t),e!==null&&(ar(e,t,n),je(e,n))}function Vf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),uc(e,n)}function Bf(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(N(314))}r!==null&&r.delete(t),uc(e,n)}var ac;ac=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,Tf(e,t,n);Se=!!(e.flags&131072)}else Se=!1,Q&&t.flags&1048576&&pa(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,Ne(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=$i(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=Wf(r),e=We(r,e),l){case 0:t=Mi(null,t,r,e,n);break e;case 1:t=Uo(null,t,r,e,n);break e;case 11:t=$o(null,t,r,e,n);break e;case 14:t=Io(null,t,r,We(r.type,e),n);break e}throw Error(N(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:We(r,l),Mi(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(Ka(t),e===null)throw Error(N(387));r=t.pendingProps,i=t.memoizedState,l=i.element,wa(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=yn(Error(N(423)),t),t=Ao(e,t,r,n,l);break e}else if(r!==l){l=yn(Error(N(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=ga(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 xa(t),e===null&&zi(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),Qa(e,t),fe(e,t,s,n),t.child;case 6:return e===null&&zi(t),null;case 13:return Ya(e,t,n);case 4:return Ns(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),$o(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,B(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(N(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=$e(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 Ha(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,Ne(r)?(e=!0,qr(t)):e=!1,dn(t,n),Aa(t,r,l),Di(t,r,l,n),$i(null,t,r,!0,e,n);case 19:return Xa(e,t,n);case 22:return Wa(e,t,n)}throw Error(N(156,t.tag))};function cc(e,t){return $u(e,t)}function Hf(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 Fe(e,t,n,r){return new Hf(e,t,n,r)}function Is(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Wf(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 Ct(e,t){var n=e.alternate;return n===null?(n=Fe(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 Vr(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=Fe(12,n,t,l|2),e.elementType=li,e.lanes=i,e;case ii:return e=Fe(13,n,t,l),e.elementType=ii,e.lanes=i,e;case si:return e=Fe(19,n,t,l),e.elementType=si,e.lanes=i,e;case xu:return Nl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case yu:s=10;break e;case wu: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(N(130,e==null?e:typeof e,""))}return t=Fe(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Ut(e,t,n,r){return e=Fe(7,e,r,t),e.lanes=n,e}function Nl(e,t,n,r){return e=Fe(22,e,r,t),e.elementType=xu,e.lanes=n,e.stateNode={isHidden:!1},e}function bl(e,t,n){return e=Fe(6,e,null,t),e.lanes=n,e}function ei(e,t,n){return t=Fe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Qf(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 Qf(e,t,n,u,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Fe(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 Kf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(hc)}catch(e){console.error(e)}}hc(),hu.exports=Le;var Zf=hu.exports,mc,qo=Zf;mc=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 qf(){return ve("/api/archives")}async function bf(e){return ve(`/api/archives/${e}/entries`)}async function ep(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 tp(e,t){return ve(`/api/archives/${e}/entries/${t}`)}async function ti(e,t){return ve(`/api/archives/${e}/entries/${t}/tags`)}async function np(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 rp(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 lp(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 ip(e,t){return ve(`/api/archives/${e}/capture_jobs/${t}`)}async function sp(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function op(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 up(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 ap(){await fetch("/api/auth/logout",{method:"POST"})}async function cp(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function dp(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 fp(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 pp(){return ve("/api/auth/tokens")}async function hp(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 mp(e){const t=await fetch(`/api/auth/tokens/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(await t.text())}async function vp(){return ve("/api/admin/instance-settings")}async function gp(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 yp(){return ve("/api/admin/users")}async function wp(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 xp(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 Sp(){return ve("/api/admin/roles")}async function kp(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 Np(e){return ve(`/api/archives/${e}/collections`)}async function jp(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 Ep(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 _p(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 Pp(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 Tp(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 Lp(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 zp=window.fetch;window.fetch=async(...e)=>{var n;const t=await zp(...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 Rp({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 up(t,r);e(m)}catch(m){s(m.message)}finally{a(!1)}}return o.jsx("div",{className:"login-page",children:o.jsxs("div",{className:"login-card",children:[o.jsx("h1",{className:"login-brand",children:"Archivr"}),o.jsx("p",{className:"login-tagline",children:"Sign in to your archive"}),o.jsxs("form",{onSubmit:d,children:[o.jsxs("div",{className:"login-field",children:[o.jsx("label",{className:"login-label",htmlFor:"login-username",children:"Username"}),o.jsx("input",{className:"login-input",id:"login-username",type:"text",value:t,onChange:v=>n(v.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),o.jsxs("div",{className:"login-field",children:[o.jsx("label",{className:"login-label",htmlFor:"login-password",children:"Password"}),o.jsx("input",{className:"login-input",id:"login-password",type:"password",value:r,onChange:v=>l(v.target.value),required:!0,autoComplete:"current-password"})]}),i&&o.jsx("p",{className:"login-error",children:i}),o.jsx("button",{className:"login-submit",type:"submit",disabled:u,children:u?"Signing in…":"Sign in"})]})]})})}function Op({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 op(t,r),e()}catch(w){a(w.message)}finally{v(!1)}}return o.jsx("div",{className:"setup-page",children:o.jsxs("div",{className:"setup-card",children:[o.jsx("h1",{className:"setup-brand",children:"Archivr"}),o.jsx("p",{className:"setup-tagline",children:"Create your owner account to get started."}),o.jsxs("form",{onSubmit:m,children:[o.jsxs("div",{className:"setup-field",children:[o.jsx("label",{className:"setup-label",htmlFor:"setup-username",children:"Username"}),o.jsx("input",{className:"setup-input",id:"setup-username",type:"text",value:t,onChange:h=>n(h.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),o.jsxs("div",{className:"setup-field",children:[o.jsx("label",{className:"setup-label",htmlFor:"setup-password",children:"Password"}),o.jsx("input",{className:"setup-input",id:"setup-password",type:"password",value:r,onChange:h=>l(h.target.value),required:!0,autoComplete:"new-password"})]}),o.jsxs("div",{className:"setup-field",children:[o.jsx("label",{className:"setup-label",htmlFor:"setup-confirm",children:"Confirm password"}),o.jsx("input",{className:"setup-input",id:"setup-confirm",type:"password",value:i,onChange:h=>s(h.target.value),required:!0,autoComplete:"new-password"})]}),u&&o.jsx("p",{className:"setup-error",children:u}),o.jsx("button",{className:"setup-submit",type:"submit",disabled:d,children:d?"Creating account…":"Create account"})]})]})})}function Dp({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 ap(),u(null),window.location.reload()}return o.jsxs("header",{className:"topbar",children:[o.jsx("div",{className:"brand",children:"Archivr"}),o.jsx("div",{className:"switcher",children:o.jsx("select",{"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:"user-name",children:s.display_name||s.username}),o.jsx("button",{className:"logout-btn",onClick:v,disabled:a,children:a?"Logging out…":"Log out"})]})]})}function Fp({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 C=l.current;if(!C)return;const p=()=>{clearInterval(w.current),n()};return C.addEventListener("close",p),()=>C.removeEventListener("close",p)},[n]),y.useEffect(()=>{const C=l.current;C&&(e?(s(""),a(null),h(null),v(!1),clearInterval(w.current),C.open||C.showModal()):C.open&&C.close())},[e]);async function x(){if(!i.trim()){a("Enter a locator.");return}v(!0),a(null),h(null);try{const C=await lp(t,i.trim());h("running"),w.current=setInterval(async()=>{var p;try{const c=await ip(t,C.job_uid);c.status==="completed"?(clearInterval(w.current),w.current=null,v(!1),h("completed"),(p=l.current)==null||p.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(C){a(C.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:C=>s(C.target.value),onKeyDown:C=>{C.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 C;return(C=l.current)==null?void 0:C.close()},children:"Cancel"}),o.jsx("button",{type:"button",className:"capture-submit",onClick:x,disabled:d,children:S()})]})]})})}function vc(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 yc(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:yc(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:gc(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:$t(e.title)||$t(e.entry_uid)})]}),o.jsx("div",{className:"col-type",children:o.jsx("span",{className:"type-pill",children:$t(e.entity_kind)})}),o.jsx("div",{className:"col-size",children:vc(e.total_artifact_bytes)}),o.jsx("div",{className:"url-cell col-url",children:$t(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 Ip(e){if(!e)return"—";try{return new Date(e).toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return e}}function Up({status:e}){const t=e==="completed"?"run-status--completed":e==="failed"?"run-status--failed":e==="running"?"run-status--running":"";return o.jsx("span",{className:`run-status ${t}`,children:e||"—"})}function Ap({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.length===0?o.jsx("tr",{children:o.jsx("td",{colSpan:5,style:{color:"var(--muted)",padding:"24px 16px",textAlign:"center"},children:"No runs yet."})}):e.map((t,n)=>o.jsxs("tr",{children:[o.jsx("td",{children:Ip(t.started_at)}),o.jsx("td",{children:o.jsx(Up,{status: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 Vp=4;function Bp({archives:e}){const{currentUser:t}=y.useContext(Pl)??{},n=t&&(t.role_bits&Vp)!==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,C]=y.useState(""),[p,c]=y.useState(""),[f,g]=y.useState(null),[j,P]=y.useState(!1),[T,z]=y.useState(""),[U,D]=y.useState(""),[J,Ce]=y.useState(null),[ge,ye]=y.useState(!1),Ue=y.useCallback(async()=>{if(n){v(!0),h(null);try{const[k,R]=await Promise.all([yp(),Sp()]);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 xp(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 Ve(k){if(k.preventDefault(),!w.trim()||!S){g("Username and password required");return}P(!0),g(null);try{await wp(w.trim(),S,p.trim()||void 0),x(""),C(""),c(""),await Ue()}catch(R){g(R.message)}finally{P(!1)}}async function E(k){if(k.preventDefault(),!T.trim()||!U.trim()){Ce("Slug and name required");return}ye(!0),Ce(null);try{await kp(T.trim(),U.trim()),z(""),D(""),await Ue()}catch(R){Ce(R.message)}finally{ye(!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:"view-tabs",children:[o.jsx("button",{className:`view-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),o.jsx("button",{className:`view-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),o.jsx("button",{className:`view-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),m&&o.jsx("div",{className:"form-msg form-msg--err",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:Ve,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=>C(k.target.value),required:!0}),o.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:p,onChange:k=>c(k.target.value)}),f&&o.jsx("div",{className:"form-msg form-msg--err",children:f}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:j,children:j?"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:T,onChange:k=>z(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:"form-msg form-msg--err",children:J}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:ge,children:ge?"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 wc({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(wc,{node:u,tagFilter:t,onTagFilterSet:n,onViewChange:r},u.tag.tag_uid))})})]})}function Hp({tagNodes:e,tagFilter:t,onTagFilterSet:n,onViewChange:r}){return o.jsx("section",{id:"tags-view",className:"view is-active",children:o.jsxs("div",{className:"tag-tree",children:[o.jsxs("div",{className:"tag-tree-header",children:[o.jsx("span",{className:"tag-tree-title",children:"Tags"}),t&&o.jsxs("span",{className:"tag-tree-active",children:["Filtering: ",t]})]}),e.length===0?o.jsx("p",{className:"muted",style:{padding:"8px 0"},children:"No tags yet."}):o.jsx("ul",{className:"tag-tree-list",children:e.map(l=>o.jsx(wc,{node:l,tagFilter:t,onTagFilterSet:n,onViewChange:r},l.tag.tag_uid))})]})})}const Fn=[{value:0,label:"Private"},{value:2,label:"Users only"},{value:3,label:"Public"}],Wp=e=>{var t;return((t=Fn.find(n=>n.value===e))==null?void 0:t.label)??String(e)};function Qp({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,C]=y.useState(""),[p,c]=y.useState(""),[f,g]=y.useState(2),[j,P]=y.useState(!1),[T,z]=y.useState(null),[U,D]=y.useState(""),[J,Ce]=y.useState(2),[ge,ye]=y.useState(!1),[Ue,Ae]=y.useState(null),[Ve,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_",F=y.useCallback(async()=>{if(e){l(!0),s(null);try{const _=await Np(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(()=>{F()},[F]),y.useEffect(()=>{se(u)},[u,se]),y.useEffect(()=>{Ve&&A.current&&A.current.focus()},[Ve]);async function Re(_){_.preventDefault();const I=S.trim(),Be=p.trim();if(!(!I||!Be)){P(!0),z(null);try{const zt=await jp(e,I,Be,f);C(""),c(""),g(2),await F(),a(zt.collection_uid)}catch(zt){z(zt.message)}finally{P(!1)}}}async function we(){const _=k.trim();if(!_||!O){E(!1);return}try{await eu(e,O.collection_uid,{name:_}),await F(),v(I=>I&&{...I,name:_})}catch(I){s(I.message)}finally{E(!1)}}async function xc(_){if(O)try{await eu(e,O.collection_uid,{default_visibility_bits:_}),await F(),v(I=>I&&{...I,default_visibility_bits:_})}catch(I){s(I.message)}}async function Sc(){if(O&&window.confirm(`Delete collection "${O.name}"? Entries will not be deleted.`))try{await Lp(e,O.collection_uid),a(null),v(null),await F()}catch(_){s(_.message)}}async function kc(_){_.preventDefault();const I=U.trim();if(!(!I||!O)){ye(!0),Ae(null);try{await Ep(e,O.collection_uid,I,J),D(""),await se(O.collection_uid)}catch(Be){Ae(Be.message)}finally{ye(!1)}}}async function Nc(_){if(O)try{await _p(e,O.collection_uid,_),await se(O.collection_uid)}catch(I){x(I.message)}}async function jc(_,I){if(O)try{await Pp(e,O.collection_uid,_,I),v(Be=>Be&&{...Be,entries:Be.entries.map(zt=>zt.entry_uid===_?{...zt,collection_visibility_bits:I}:zt)})}catch(Be){x(Be.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-sidebar-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:Wp(_.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:[Ve?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:Sc,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:_=>xc(Number(_.target.value)),disabled:de,children:Fn.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=>jc(_.entry_uid,Number(I.target.value)),children:Fn.map(I=>o.jsx("option",{value:I.value,children:I.label},I.value))}),!de&&o.jsx("button",{className:"coll-entry-remove",onClick:()=>Nc(_.entry_uid),title:"Remove from collection",children:"×"})]})]},_.entry_uid))}))]}),!de&&o.jsxs("form",{className:"coll-add-entry-form",onSubmit:kc,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:_=>Ce(Number(_.target.value)),children:Fn.map(_=>o.jsx("option",{value:_.value,children:_.label},_.value))}),o.jsx("button",{className:"coll-add-btn",type:"submit",disabled:ge,children:ge?"…":"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",children:[o.jsx("summary",{children:"+ Create collection"}),o.jsxs("form",{className:"coll-create-form",onSubmit:Re,children:[o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"coll-name",children:"Name"}),o.jsx("input",{className:"capture-input",id:"coll-name",type:"text",value:S,onChange:_=>{C(_.target.value),p||c(_.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"coll-slug",children:"Slug"}),o.jsx("input",{className:"capture-input",id:"coll-slug",type:"text",value:p,onChange:_=>c(_.target.value),placeholder:"my-collection",required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"coll-vis",children:"Default visibility"}),o.jsx("select",{className:"capture-input",id:"coll-vis",style:{height:42},value:f,onChange:_=>g(Number(_.target.value)),children:Fn.map(_=>o.jsx("option",{value:_.value,children:_.label},_.value))})]}),T&&o.jsx("div",{className:"collections-error",children:T}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:j,children:j?"Creating…":"Create collection"})]})]})]}):o.jsx("div",{className:"view-placeholder",children:"Select an archive."})}const Kp=4;function Yp(){const{currentUser:e,setCurrentUser:t}=y.useContext(Pl)??{},n=e&&(e.role_bits&Kp)!==0,[r,l]=y.useState("profile"),i=["profile","tokens",...n?["instance"]:[]],s={profile:"Profile",tokens:"API Tokens",instance:"Instance"};return o.jsxs("section",{className:"admin-view",children:[o.jsx("h1",{children:"Settings"}),o.jsx("div",{className:"view-tabs",children:i.map(u=>o.jsx("button",{className:`view-tab${r===u?" is-active":""}`,onClick:()=>l(u),children:s[u]},u))}),r==="profile"&&o.jsx(Xp,{currentUser:e,setCurrentUser:t}),r==="tokens"&&o.jsx(Gp,{}),r==="instance"&&n&&o.jsx(Jp,{})]})}function Xp({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),[C,p]=y.useState(null);async function c(g){g.preventDefault(),i(!0),u(null);try{await dp(n),t(j=>({...j,display_name:n||null})),u({ok:!0,text:"Saved."})}catch(j){u({ok:!1,text:j.message})}finally{i(!1)}}async function f(g){if(g.preventDefault(),v!==h){p({ok:!1,text:"Passwords do not match."});return}S(!0),p(null);try{await fp(a,v),d(""),m(""),w(""),p({ok:!0,text:"Password changed."})}catch(j){p({ok:!1,text:j.message})}finally{S(!1)}}return o.jsxs("div",{style:{maxWidth:440},children:[o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"Display Name"}),o.jsxs("form",{onSubmit:c,children:[o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"display-name",children:"Name shown in the UI"}),o.jsx("input",{className:"field-input",id:"display-name",placeholder:(e==null?void 0:e.username)??"",value:n,onChange:g=>r(g.target.value)})]}),s&&o.jsx("div",{className:`form-msg form-msg--${s.ok?"ok":"err"}`,children:s.text}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:l,children:l?"Saving…":"Save"})]})]}),o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"Change Password"}),o.jsxs("form",{onSubmit:f,children:[o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"cur-pw",children:"Current password"}),o.jsx("input",{className:"field-input",id:"cur-pw",type:"password",value:a,onChange:g=>d(g.target.value),required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"new-pw",children:"New password"}),o.jsx("input",{className:"field-input",id:"new-pw",type:"password",value:v,onChange:g=>m(g.target.value),required:!0})]}),o.jsxs("div",{className:"form-field",children:[o.jsx("label",{className:"form-label",htmlFor:"confirm-pw",children:"Confirm new password"}),o.jsx("input",{className:"field-input",id:"confirm-pw",type:"password",value:h,onChange:g=>w(g.target.value),required:!0})]}),C&&o.jsx("div",{className:`form-msg form-msg--${C.ok?"ok":"err"}`,children:C.text}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:x,children:x?"Changing…":"Change Password"})]})]})]})}function Gp(){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 pp())}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 C=await hp(s.trim());m(C),u(""),h()}catch(C){i(C.message)}finally{d(!1)}}}async function x(S){try{await mp(S),t(C=>C.filter(p=>p.token_uid!==S))}catch(C){i(C.message)}}return o.jsx("div",{style:{maxWidth:600},children:o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"API Tokens"}),v&&o.jsxs("div",{className:"token-banner",children:[o.jsx("strong",{children:"Token created."})," Copy it now — it won't be shown again.",o.jsx("code",{children:v.raw_token}),o.jsx("button",{className:"token-dismiss",onClick:()=>m(null),children:"Dismiss"})]}),o.jsxs("form",{className:"token-create-row",onSubmit:w,children:[o.jsx("input",{className:"field-input field-input--flex",placeholder:"Token name",value:s,onChange:S=>u(S.target.value),required:!0}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:a,children:a?"Creating…":"Create token"})]}),l&&o.jsx("div",{className:"form-msg form-msg--err",children:l}),n?o.jsx("div",{className:"muted",children:"Loading\\u2026"}):o.jsxs("div",{children:[e.length===0&&o.jsx("div",{className:"muted",children:"No tokens yet."}),e.map(S=>o.jsxs("div",{className:"token-row",children:[o.jsxs("div",{className:"token-row-info",children:[o.jsx("strong",{children:S.name}),o.jsxs("div",{className:"muted",children:["Created ",S.created_at.slice(0,10),S.last_used_at&&` · Last used ${S.last_used_at.slice(0,10)}`]})]}),o.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"4px 10px"},onClick:()=>x(S.token_uid),children:"Revoke"})]},S.token_uid))]})]})})}function Jp(){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 vp())}catch(m){i(m.message)}finally{r(!1)}})()},[]);async function v(m){m.preventDefault(),u(!0),d(null);try{await gp(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:"form-msg form-msg--err",children:l}):e?o.jsx("div",{style:{maxWidth:440},children:o.jsxs("div",{className:"form-section",children:[o.jsx("h2",{children:"Instance Settings"}),o.jsxs("form",{onSubmit:v,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",{className:"checkbox-row",children:[o.jsx("input",{type:"checkbox",checked:!!e[m],onChange:w=>t(x=>({...x,[m]:w.target.checked}))}),h]},m)),o.jsxs("div",{className:"form-field",style:{marginTop:4},children:[o.jsx("label",{className:"form-label",children:"Default entry visibility"}),o.jsxs("select",{className:"field-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"}),o.jsx("option",{value:2,children:"Unlisted"}),o.jsx("option",{value:3,children:"Public"})]})]}),a&&o.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text}),o.jsx("button",{className:"btn-primary",type:"submit",disabled:s,children:s?"Saving…":"Save Settings"})]})]})}):null}const nu={0:"Private",1:"Public",2:"Users only",3:"Public"},Zp=()=>o.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:o.jsx("path",{d:"M7 17 17 7M9 7h8v8"})});function qp({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 f=++S.current;s(null),a([]),Promise.all([tp(e,t.entry_uid),ti(e,t.entry_uid),Tp(e,t.entry_uid)]).then(([g,j,P])=>{f===S.current&&(s(g),a(j),h(P))}).catch(()=>{})},[t,e]);async function C(){const f=d.trim();if(!(!f||!t))try{await np(e,t.entry_uid,f),v(""),x("");const g=await ti(e,t.entry_uid);a(g),l()}catch(g){x(g.message)}}async function p(f){try{await rp(e,t.entry_uid,f);const g=await ti(e,t.entry_uid);a(g),l()}catch{}}const c=i?[["Added",gc(i.summary.archived_at)],["Source",i.summary.source_kind],["Type",i.summary.entity_kind],["Visibility",nu[i.summary.visibility]??i.summary.visibility],["Root",i.structured_root_relpath]]:[];return o.jsxs("aside",{className:"context-rail",children:[o.jsx("div",{className:"rail-eyebrow",children:"Context"}),t?i?o.jsxs(o.Fragment,{children:[o.jsx("h2",{className:"rail-title",children:$t(i.summary.title)||$t(i.summary.entry_uid)}),i.summary.original_url&&o.jsxs("a",{className:"url-tile",href:i.summary.original_url,target:"_blank",rel:"noopener noreferrer",children:[o.jsx("span",{className:"ico",dangerouslySetInnerHTML:{__html:yc(i.summary.source_kind)}}),o.jsx("span",{className:"u-text",children:i.summary.original_url}),o.jsx("span",{className:"ext",children:o.jsx(Zp,{})})]}),o.jsx("div",{className:"meta-list",children:c.filter(([,f])=>f!=null&&f!=="").map(([f,g])=>o.jsxs("div",{className:"meta-item",children:[o.jsx("span",{className:"meta-k",children:f}),o.jsx("span",{className:`meta-v${f==="Root"?" mono":""}`,children:$t(g)})]},f))}),i.artifacts.length>0&&o.jsxs("div",{className:"rail-section",children:[o.jsxs("div",{className:"rail-section-heading",children:["Artifacts ",o.jsx("span",{className:"num",children:i.artifacts.length})]}),o.jsx("ul",{className:"artifact-list",children:i.artifacts.map((f,g)=>o.jsx("li",{children:o.jsxs("a",{href:`/api/archives/${e}/entries/${i.summary.entry_uid}/artifacts/${g}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[o.jsx("span",{className:"artifact-name",children:f.artifact_role.replace(/_/g," ")}),o.jsx("span",{className:"artifact-size",children:f.byte_size!=null?vc(f.byte_size):"—"})]})},g))})]})]}):o.jsx("p",{className:"tags-empty",children:"Loading…"}):o.jsx("p",{className:"tags-empty",children:"Select an entry."}),t&&o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"rail-section",children:[o.jsx("div",{className:"rail-section-heading",children:"Tags"}),u.length===0?o.jsx("p",{className:"tags-empty",children:"No tags yet."}):o.jsx("div",{className:"tags-wrap",children:u.map(f=>o.jsxs("span",{className:"tag-pill",title:f.full_path,children:[f.name,o.jsx("button",{className:"remove",title:`Remove tag ${f.full_path}`,onClick:()=>p(f.tag_uid),children:"×"})]},f.tag_uid))}),w&&o.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:w}),o.jsxs("div",{className:"tag-input-wrap",children:[o.jsx("span",{className:"hash",children:"/"}),o.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:d,onChange:f=>v(f.target.value),onKeyDown:f=>{f.key==="Enter"&&C()}}),o.jsx("button",{className:"tag-add-btn",onClick:C,children:"Add"})]})]}),m.length>0&&o.jsxs("div",{className:"rail-section",children:[o.jsx("div",{className:"rail-section-heading",children:"Collections"}),m.map(f=>o.jsxs("div",{className:"coll-row",children:[o.jsx("span",{className:"coll-name",children:f.collection_uid}),o.jsx("span",{className:"vis-badge",children:nu[f.visibility_bits]??`bits:${f.visibility_bits}`})]},f.collection_uid))]})]})]})}const Pl=y.createContext(null);function bp(){const[e,t]=y.useState("loading"),[n,r]=y.useState(null);y.useEffect(()=>{(async()=>{if(await sp()){t("setup");return}const se=await cp();if(!se){t("login");return}r(se),t("authenticated")})()},[]),y.useEffect(()=>{const F=()=>{r(null),t("login")};return window.addEventListener("auth:expired",F),()=>window.removeEventListener("auth:expired",F)},[]);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),[C,p]=y.useState("archive"),[c,f]=y.useState(""),[g,j]=y.useState(""),[P,T]=y.useState(!1),[z,U]=y.useState([]),[D,J]=y.useState([]),[Ce,ge]=y.useState(!1),ye=y.useCallback(async(F,se,Re)=>{if(F){T(!0);try{let we;se||Re?we=await ep(F,se,Re):we=await bf(F),d(we),j(we.length===0?"No results":`${we.length} result${we.length===1?"":"s"}`)}catch{d([]),j("Search failed. Try again.")}finally{T(!1)}}},[]);y.useEffect(()=>{e==="authenticated"&&qf().then(F=>{if(i(F),F.length>0){const se=F[0].id;u(se)}})},[e]),y.useEffect(()=>{s&&(S(null),w(null),m(null),Promise.all([ye(s,"",null),bo(s).then(U),ni(s).then(J)]))},[s]),y.useEffect(()=>{if(s===null)return;const F=setTimeout(()=>{ye(s,c,x)},300);return()=>clearTimeout(F)},[c,s]),y.useEffect(()=>{s!==null&&(p("archive"),ye(s,c,x))},[x,s]);const Ue=y.useCallback(F=>{u(F)},[]),Ae=y.useCallback(F=>{p(F),F==="tags"&&s&&ni(s).then(J)},[s]),Ve=y.useCallback(F=>{m(F?F.entry_uid:null),w(F)},[]),E=y.useCallback(F=>{S(F)},[]),k=y.useCallback(()=>{S(null)},[]),R=y.useCallback(()=>{s&&ni(s).then(J)},[s]),A=y.useCallback(()=>{ge(!0)},[]),O=y.useCallback(()=>{ge(!1)},[]),de=y.useCallback(()=>{s&&Promise.all([ye(s,c,x),bo(s).then(U)])},[s,c,x,ye]);return e==="loading"?o.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?o.jsx(Op,{onComplete:()=>t("login")}):e==="login"?o.jsx(Rp,{onLogin:F=>{r(F),t("authenticated")}}):o.jsx(Pl.Provider,{value:{currentUser:n,setCurrentUser:r},children:o.jsxs(o.Fragment,{children:[o.jsx(Dp,{archives:l,archiveId:s,onArchiveChange:Ue,view:C,onViewChange:Ae,onCaptureClick:A}),o.jsxs("main",{className:"app-shell",children:[o.jsxs("div",{className:"workspace",children:[C==="archive"&&o.jsxs("div",{className:"toolbar",children:[o.jsxs("div",{className:"search-field",children:[o.jsx("span",{className:"ico","aria-hidden":"true",children:o.jsxs("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[o.jsx("circle",{cx:"11",cy:"11",r:"7"}),o.jsx("line",{x1:"16.5",y1:"16.5",x2:"21",y2:"21"})]})}),o.jsx("input",{className:"search-input",type:"search","aria-label":"Search archive","aria-busy":P,placeholder:"Search titles, URLs, types, tags…",value:c,onChange:F=>f(F.target.value)}),o.jsx("span",{className:"kbd",children:"⌘K"})]}),o.jsxs("span",{className:"result-count",children:[g&&o.jsxs(o.Fragment,{children:[o.jsx("b",{children:g.split(" ")[0]})," ",g.split(" ").slice(1).join(" ")]}),x&&o.jsxs("button",{className:"tag-filter-badge",onClick:k,children:["× ",x]})]})]}),C==="archive"&&o.jsx($p,{entries:a,selectedEntryUid:v,onSelectEntry:Ve,archiveId:s,tagFilter:x,onClearTagFilter:k,searchQuery:c,onSearchChange:f,resultCount:g,searchBusy:P}),C==="runs"&&o.jsx(Ap,{runs:z}),C==="admin"&&o.jsx(Bp,{archives:l}),C==="tags"&&o.jsx(Hp,{tagNodes:D,tagFilter:x,onTagFilterSet:E,onViewChange:Ae}),C==="collections"&&o.jsx(Qp,{archiveId:s}),C==="settings"&&o.jsx(Yp,{})]}),o.jsx(qp,{archiveId:s,selectedEntry:h,onTagFilterSet:E,tagNodes:D,onTagsRefresh:R})]}),o.jsx(Fp,{open:Ce,archiveId:s,onClose:O,onCaptured:de})]})})}mc(document.getElementById("root")).render(o.jsx(y.StrictMode,{children:o.jsx(bp,{})})); diff --git a/crates/archivr-server/static/index.html b/crates/archivr-server/static/index.html index 25acac9..d036919 100644 --- a/crates/archivr-server/static/index.html +++ b/crates/archivr-server/static/index.html @@ -4,7 +4,7 @@ Archivr - +