From 6377daadae096a2512ef7372235efe22effac1f9 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:57:29 +0200 Subject: [PATCH] feat: add capturing w/ parent-child entries (#32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(core): YouTube playlist/channel/YTM-playlist capture with parent–child entries - ytdlp: add fetch_playlist_info() using yt-dlp -J --flat-playlist for reliable container title + shallow entry list; normalize item URLs via webpage_url → absolute url → id fallback (domain inferred from container URL so YTM stays on music.youtube.com) - capture: add record_container_entry() (no blob, no primary_media artifact); extend record_media_entry() with parent_entry_id/root_entry_id params (all existing single-item call sites pass None, None) - capture: implement YouTubePlaylist / YouTubeChannel / YouTubeMusicPlaylist capture path replacing the two not-implemented stubs: fetch playlist info → create container entry (reusing existing run + item) → per-child run items (parent_item_id = container item) → download each video/track as a child entry; per-child failures are non-fatal; perform_capture returns result.status reflecting actual run outcome so capture_handler marks the job correctly - archive: add child_count i64 to EntrySummary (col 12 in all listing queries); add get_entry_summary() private helper; fix get_entry_detail() to use get_entry_summary() so child entries are resolvable via the detail endpoint; add list_child_entries(conn, uid, caller_bits) with the same admin/collection visibility predicate as list_root_entries archive_runs.requested_count stays 1 (one user locator); discovered/ completed/failed_count reflect container item + N video items via refresh_run_counters. * feat(server,frontend): expose children endpoint + expand UI for container entries server: - add GET /api/archives/:id/entries/:uid/children → list_entry_children, calling list_child_entries with caller_bits so visibility model is enforced - fix capture_handler: use result.status ("completed"/"failed") to set job status rather than always "completed", mirroring rearchive_handler; this surfaces partial playlist failures to the polling client frontend: - api.js: add fetchEntryChildren(archiveId, entryUid) - EntryRow: one outer div.entry-row-outer (display:block) keeps nth-child striping correct; inner div.entry-row-main is the flex row with all column cells and event handling; .child-entries sits below inside the outer wrapper - expand chevron appears when entry.child_count > 0; clicking fetches children lazily and renders ChildRow components reusing .col-* flex widths - child-count badge shown next to title on container entries - styles.css: scoped CSS with #entries-body > .entry-row-outer selectors (higher specificity than > div) to override flex on outer wrapper; inner row and column rules replicated at correct depth; nth-child, is-selected, is-multi-selected, url-cell hover all handled * fix(frontend): make child entry rows interactive ChildRow now receives onRowClick and selectedUids from EntryRow (which receives selectedUids from EntriesView alongside the existing booleans). Clicking a child row invokes onRowClick(child, e) so it flows through handleRowClick → selectEntry → fetchEntryDetail exactly as a root entry would. Shift-range selection gracefully degrades to single-select since child entries are not in the root entries array. Selected/multi-selected visual state is wired: .child-entry-row.is-selected shows the same #eee2d2 background + accent outline as root rows; hover restores full opacity. Frontend static assets rebuilt. * feat(core): playlist per-item quality + incremental sync ytdlp.rs: - Add PlaylistItemProbe / PlaylistProbeResult (pub, serde::Serialize) - Add private available_video_heights_from_value() helper for Value entries - Add probe_playlist_qualities(): yt-dlp -J (full metadata, no flat flag) returns per-video quality lists in one subprocess call capture.rs: - Add per_item_quality: HashMap and sync: bool to CaptureConfig (both Default; keyed by yt-dlp video ID, not URL) - Add pub locator_to_playlist_url(): validates only the three playlist sources, expands shorthands; keeps locator_to_ytdlp_url's no-playlists contract - Playlist capture block: sync-aware container resolution - sync + existing container → reuse it via complete_archive_run_item, skip already-archived children (by canonical URL) before creating run items so refresh_run_counters only counts new items - sync + no container → create normally (first sync run) - non-sync → always create fresh container (existing behaviour) - Per-item quality: config.per_item_quality.get(id) falls back to child_quality archive.rs: - Add get_archived_playlist_child_urls(): returns HashSet of canonical URLs of all children under any container matching the playlist canonical URL - Add find_container_entry_id_by_canonical_url(): returns most-recent container entry id (parent_entry_id IS NULL) for a given canonical URL routes.rs: stub per_item_quality/sync on both CaptureConfig sites (server agent will wire body fields in Phase 2) * fix(core)+test: propagate sync query errors; cover new playlist/sync functions archive.rs: - get_archived_playlist_child_urls: collect() as rusqlite::Result> instead of filter_map(ok) so row-level errors surface rather than silently skipping and causing duplicate downloads capture.rs: - match get_archived_playlist_child_urls result and fail_run on error instead of unwrap_or_default, preventing silent re-downloads on DB failure Tests added to capture.rs: - locator_to_playlist_url_accepts_playlist_shorthands (yt:playlist/, ytm:playlist/, full URL) - locator_to_playlist_url_accepts_channel_shorthands (yt:@handle) - locator_to_playlist_url_rejects_non_playlist_sources (single video, tweet, web page) Tests added to archive.rs (all use in-memory DB via make_tag_test_db): - find_container_entry_id_returns_none_when_absent - find_container_entry_id_returns_root_entry - find_container_entry_id_ignores_child_entries (child with parent_entry_id set) - get_archived_playlist_child_urls_empty_when_no_playlist - get_archived_playlist_child_urls_returns_children - get_archived_playlist_child_urls_excludes_other_playlists * feat(server,frontend): playlist quality selector + per-video overrides + sync UI routes.rs: - CaptureBody gains per_item_quality (HashMap, serde(default)) and sync (bool, serde(default)); both validated before use - per_item_quality values validated against same quality predicate as top-level quality field ("best"|"audio"|"NNNp") so bad per-video values are rejected at the API boundary rather than silently falling through to quality_format - capture_handler threads body.per_item_quality + body.sync into CaptureConfig (replaces hardcoded empty stubs); rearchive_handler keeps empty defaults - New POST /api/archives/:id/captures/probe-playlist: calls probe_playlist_qualities via spawn_blocking; 400 for non-playlist locator, 502 on yt-dlp failure, returns PlaylistProbeResult as JSON api.js: - probePlaylist(archiveId, locator): POST probe-playlist endpoint - submitCapture: forwards per_item_quality (non-empty) and sync:true from extraExtensions param added to submitBgJob CaptureDialog.jsx: - isPlaylistSource(): detects yt:/youtube: playlist/@/channel, ytm:playlist/, YouTube/YTM HTTP(S) URLs with list= param or channel pathnames - makeItem(): 6 new playlist state fields - applyPlaylistQuality(): conflict logic — videos that can reach selected quality get it set; videos that can't and have no prior selection are left null (conflict); videos with a prior selection keep it when quality is raised - hasConflict(): any playlistItems entry with quality===null - updateLocator(): isPlaylistSource branch with 800ms debounce→probePlaylist; existing isVideoSource path unchanged - Archive button disabled when anyConflict or any probe in flight - Per-video expand list with individual quality selects, conflict badges, sync toggle (appears after probe completes) styles.css: playlist expansion, conflict, sync toggle CSS * fix(core): ignore per_item_quality for YTM playlist items YouTube Music playlists force child_quality = Some("audio") because yt-dlp can't download DRM-free audio-only tracks any other way. The previous per_item_quality lookup could override this with e.g. "best", defeating the invariant. Guard the lookup behind !is_audio so YTM items are always downloaded as audio regardless of what the caller sends. * fix(frontend): exclude /watch from isPlaylistSource youtube.com/watch?v=...&list=... and music.youtube.com/watch are single videos in the backend (Source::YouTubeVideo / YouTubeMusicTrack) regardless of the list param. Previously isPlaylistSource returned true for these, which would have triggered the playlist probe path while the video probe was already running, and the render would attempt to show playlist UI on an item whose playlistProbeState stays idle. Guard: if pathname === '/watch', return false before the list-param check. * fix(frontend): tighten isPlaylistSource to mirror backend routing exactly Previous fix excluded /watch but still returned true for any youtube.com URL with a ?list= param (e.g. /shorts/xxx?list=yyy). Backend determine_source only routes to YouTubePlaylist on /playlist?list=... and to YouTubeChannel on /@handle, /channel/, /c/, /user/ paths — everything else is a single item. Rewrite the HTTP block to match: - youtube.com: pathname==='/playlist' && list param → playlist : /@, /channel/, /c/, /user/ → channel : anything else (incl. /watch&list=, /shorts?list=) → false - music.youtube.com: pathname==='/playlist' && list param → YTM playlist : /watch → single track (falls through to false) * fix(frontend): guard handleArchive against Enter-key bypass of disabled state The Archive button is disabled when anyConflict || anyProbing, but onKeyDown on the locator input calls onSubmit() → handleArchive() directly, bypassing the button's disabled check entirely. Add the same conditions as early returns inside handleArchive itself, operating on toSubmit (the items that would actually be submitted) so the guard is tight — items with no locator are already excluded by the toSubmit filter. * fix(frontend): drop m.youtube.com from isPlaylistSource Backend determine_source playlist/channel regex only matches (?:www\.)?youtube\.com — mobile URLs hitting m.youtube.com would be probed as playlist in the UI but captured as Source::Url server-side. Remove m.youtube.com from the detector to keep frontend and backend in exact agreement. Add backend support when needed. * fix(frontend): audio-only conflict handling in playlist quality selector applyPlaylistQuality('audio'): - Only sets quality='audio' on items where has_audio=true - Items with has_audio=false: keep prior selection if set, else null (conflict) — same rule as unsupported height, blocks archive until user explicitly picks a quality for those items Playlist-level 'Audio only' option: - Changed hasAnyAudio → allHaveAudio (every item must have audio) - When any item lacks audio, the option is hidden entirely so the selector can never create immediate conflicts just by appearing * fix(frontend): add yt:user/ to isPlaylistSource shorthand detection Backend determine_source routes yt:user/... (and youtube:user/...) to YouTubeChannel — already covered by the yt: shorthand block for playlist/, @, channel/, c/ but missing user/. Old-style user channel URLs would capture correctly server-side but never show the playlist quality/sync UI. * fix(frontend): block playlist submission unless probe is done Previous guard only blocked while playlistProbeState==='probing'. Two remaining bypass paths: - idle: 800ms debounce not yet fired after URL typed - error: probe failed — no per-video quality data available Change anyProbing and handleArchive guard to: isPlaylistSource(locator) && playlistProbeState !== 'done' This means idle/probing/error all block submission for playlist items. error is intentionally blocking — without quality data the per-video requirement can't be satisfied; user must retry or remove the URL. * fix(frontend): accurate error message when playlist probe fails Previous text said 'using best quality' implying the capture would proceed, but probe error now blocks submission. Replace with 'Probe failed — edit URL to retry' in orange (capture-quality-hint--error) so the disabled button and the message are consistent. * fix(frontend): exact quality match in applyPlaylistQuality Replace maxHeight >= newHeight (cap check) with item.qualities.includes(newQ) (exact match). A video with [2160p, 1080p] does not support 1440p; the previous logic would mark it as supporting any quality up to 2160p and submit '1440p' which yt-dlp silently downloads as 1080p — misrepresenting the selected quality. With exact match, unsupported qualities correctly fall through to the conflict path (keep prior selection or null), enforcing the same manual- choice requirement as any other unsupported quality. Per-row selects are unaffected: they already render only pi.qualities (the video's actual available formats), no maxHeight logic involved. * fix(frontend): move playlist expand chevron to left of input User asked for the chevron to be on the left of the playlist input, not tucked after the quality selector on the right. - Remove chevron from qualityEl (it was between the quality select and the remove button) - Add it as the first child of capture-row-main, before the , when isPlaylistSource && playlistProbeState === 'done' - Show a same-width placeholder span while probing/idle/error so the input does not jump left when the chevron appears after probe completes - Add capture-playlist-toggle--left modifier (flex-shrink:0, tighter padding) and .capture-playlist-toggle-placeholder (fixed 22px width) * doc(server): scope per_item_quality guarantee to the UI The server validates per_item_quality value shapes but does not enforce that every playlist item has an entry. Items without an override get the global quality as a yt-dlp cap with graceful fallback. The 'must choose quality for unsupported videos' invariant is a UI constraint enforced by the frontend before submission. A direct API caller bypassing the UI accepts yt-dlp's standard cap-and-fallback behavior. Document this scope explicitly so the gap is intentional, not accidental. * fix(frontend): prevent 'reading some of undefined' crash from stale sessionStorage Old captureItems entries saved before the playlist fields were added have playlistItems=undefined (missing key). The hasConflict guard checked !== null, which undefined passes, then called .some() on undefined → TypeError. Two-part fix: 1. sessionStorage restore: merge each saved item over makeItem() defaults so any missing fields (playlistItems, playlistProbeState, etc.) are filled with their correct initial values before the item is used 2. hasConflict: use Array.isArray() instead of !== null so undefined is also safely rejected — defence-in-depth for any future field gap * fix(core): playlist total size includes children; per_item_quality as include-set archive.rs: total_artifact_bytes for root entries now adds a correlated subquery summing children's blob bytes so playlist/channel containers show the real download size instead of 0. capture.rs: non-empty per_item_quality map now acts as an include-set — items whose yt-dlp ID is absent are skipped entirely. This wires the UI's per-video delete button to actual capture exclusion. Empty map preserves the existing behaviour (download everything). routes.rs + capture.rs doc: comments updated to reflect both semantics (empty = all / non-empty = only listed IDs) accurately. * fix(frontend): QA fixes — playlist UX, selection stroke, URL expand CaptureDialog.jsx: - Placeholder no longer appears on idle playlist rows (only during probing); chevron shows only after probe completes — no left-padding while the user is still typing - Per-video delete button added to expanded playlist list; removes item from playlistItems so its ID is absent from per_item_quality on submit - anyEmptyPlaylist guard: Archive button disabled + handleArchive early- return when all videos have been deleted (empty map would otherwise silently download everything) styles.css: - Selection stroke switched from outline to box-shadow:inset everywhere (entry-row-outer, child-entry-row, legacy flat-div selector) — guaranteed inside element bounds, no layout interference - URL cell overflow only on :hover; removed is-selected .url-cell rule that was expanding child URLs when their parent was selected - Playlist items redesign: separator lines instead of background fills, amber left-border for conflicts, thin scrollbar, tighter padding - Remove button: opacity 0.3 always-visible baseline; full opacity on hover/:focus-visible; forced to 1 on coarse-pointer (touch) devices * fix(frontend): no left gap on playlist rows until chevron exists Remove the probing-state placeholder span entirely. The chevron renders only when playlistProbeState === 'done'; all other states (idle, probing, error) render null. The small layout shift when the chevron appears after probe is acceptable; blank padding while there is no chevron is not. * fix(frontend): clear box-shadow on entry-row-outer to prevent double stroke The flat selector '#entries-body > div.is-selected' already applies box-shadow to .entry-row-outer (it IS a direct child div). The outer override rule only cleared 'outline', so the box-shadow leaked through, wrapping the entire parent+children block with a second stroke. Add box-shadow: none to both .is-selected and .is-multi-selected on .entry-row-outer so the stroke sits only on .entry-row-main. * docs: document per-video exclude in README YouTube playlists section * fix(frontend): scope selection background to entry-row-main only Moving background:#eee2d2 off .entry-row-outer onto > .entry-row-main so that expanded child entries don't inherit the selection highlight. Outer wrapper now explicitly unsets background (cancelling the flat-div cascade rule) and clears outline+box-shadow. Both the selection colour and the inset stroke live on .entry-row-main only. * fix(frontend): alternating stripe backgrounds on child entry rows Child rows were inheriting the parent entry's stripe color, making the expanded list look like one flat block. Apply the same odd/even palette as root entries (var(--paper-3) / #f2ede5) so each video row is visually distinct within the expanded group. * fix(core): delete_entry correctly nulls FK for child entries archive_run_items.produced_entry_id has no ON DELETE action so it must be manually nulled before the entry row is deleted. The old query used WHERE root_entry_id = entry_id, which finds descendants of a root but returns nothing when entry_id IS a child (children have no sub-children, so no row has root_entry_id = child_id). The DELETE then failed under foreign_keys=ON. Fix: add OR produced_entry_id = ?1 so the entry's own run_item FK is always cleared before deletion, regardless of whether it is a root or a child. The subtree subquery is kept for the root-deletion case where all child run_items also need nulling. * fix(core+frontend): child entry selection and delete correctness database.rs — delete_entry: - subtree_ids now uses WHERE id = ?1 OR root_entry_id = ?1 so the entry itself is always included; previously a child deletion passed an empty vec to cascade_cached_bytes_after_subtree_delete (no grandchildren exist), leaving cached_bytes stale on entries sharing that child's blobs App.jsx — handleRowClick: - Shift-range now queries DOM order (#entries-body [data-entry-uid]) instead of entries.findIndex(); child rows are in the DOM but not in the root entries array, so findIndex always returned -1 for them - Ctrl/meta branch computes next set before the state update so selectEntry can fire synchronously for child rows; auto-snap only restores root entries, so ctrl-clicking a lone child never loaded its detail panel — now calls selectEntry(entry) when the child ends up as the sole selection, selectEntry(null) on multi or deselect - selectedUids added to handleRowClick's useCallback dep array * fix(frontend): resolve detail entry for any remaining child after ctrl-deselect Add entryCacheRef (uid→entry Map) populated on every row click. When ctrl/meta-deselecting leaves exactly one other entry selected, look up the remaining UID in the cache before falling back to the root entries array. Without this, deselecting from a multi-selection where the remaining entry is a child row left selectedEntry null (auto-snap only searches root entries). * fix(frontend): child row stripes, deleted-child visibility, selection completeness EntryRow.jsx / ChildRow: - Index-based light/dark classes (child-entry-row--light/dark) replace nth-child rules; parity comes from children.map idx so no sibling — including the loading div — can shift the stripe order - Loading div moved outside .child-entries so it never affects child row ordering at all - Accept deletedUids prop; filter expanded children array before render so deleted children disappear immediately without waiting for reload EntriesView.jsx: thread deletedUids through to EntryRow App.jsx: - Add deletedUids state; handleEntryDeleted/handleBulkDeleted populate it - isRoot/hasChildDelete computed from entries before setEntries (safe in StrictMode — no side-effects inside updater functions) - Child delete triggers loadEntries to refresh stale parent child_count and total_artifact_bytes - handleRowClick ctrl/meta cache-miss branch uses det.summary (not det) from fetchEntryDetail; archiveId added to dep array - handleRowClick dep array includes archiveId * fix(frontend): add inset stroke to child-entry-row.is-multi-selected * fix(frontend): suppress mouse-click focus ring on entry expand button * fix(frontend): index-based stripes for root and child rows Root rows: EntriesView passes rowIndex from entries.map to EntryRow, which applies entry-row-outer--light/dark. Retires both nth-child stripe blocks so skeleton rows can never shift the first real entry to dark. Skeleton rows keep a :not(.entry-row-outer) nth-child fallback. Child rows: colours changed from the warm root palette (paper-3/#f2ede5) to cooler near-whites (#fafaf8/#f2f0ec) so children are visually distinct from their parent row regardless of which stripe the parent sits on. * feat(core+frontend): bare yt:ID resolves to YouTube video determine_source: yt:ID / youtube:ID with no prefix and exactly 11 chars [A-Za-z0-9_-] → YouTubeVideo via is_youtube_video_id helper. Reserved prefixes (playlist/, channel/, c/, user/, @) still fire first so they are unaffected by the fallback. expand_shorthand_to_url: bare yt:ID expands to watch?v=ID using the same predicate, consistent with how ytm:ID → music.youtube.com/watch. isVideoSource (frontend): same 11-char /^[A-Za-z0-9_-]{11}$/ regex so yt:ID triggers the quality probe and capture-row guards identically to yt:video/ID. Tests: test_is_youtube_video_id covers valid IDs (alphanumeric, with _ and -), too-short, too-long, and invalid-char cases using genuinely invalid fixtures. test_youtube_sources adds bare-ID cases and confirms reserved prefixes (playlist/, @) are not affected. * fix(core+frontend): exclude avatar blobs from tweet % cached display tweet/tweet_thread entries have avatar artifacts that are always deduplicated from the first capture of each author. Counting them in the cached-bytes percentage makes it artificially high (or incorrect when the real media content is new but avatars are cached). database.rs: - refresh_entry_cached_bytes: AND ea.artifact_role != 'avatar' - cascade_cached_bytes_after_delete: same filter - cascade_cached_bytes_after_subtree_delete: same filter - Initial cached_bytes migration: same filter - Re-migration (else branch): recomputes cached_bytes for existing entries that have avatar artifacts, scoped to only those entries archive.rs: - EntrySummary gains cacheable_bytes: i64 — non-avatar total bytes, computed inline in every SQL query as the denominator for % cached - ENTRY_SELECT_COLS adds cacheable_bytes at index 13 (with children subquery, same as total_artifact_bytes) - list_root_entries adds same expression - All 6 row-mapping closures include cacheable_bytes: row.get(13)? EntryRow.jsx: - SIZE display stays: formatBytes(entry.total_artifact_bytes) - % cached badge uses cacheable_bytes as denominator: cached_bytes / cacheable_bytes * 100 * feat(frontend): j/k keyboard navigation for entries; test avatar cached_bytes App.jsx — j/k handler: - Fires on keydown when not focused on INPUT/TEXTAREA/SELECT/contenteditable - Ignores meta/ctrl/alt modifier combos - Uses DOM order (#entries-body [data-entry-uid]) so expanded child rows participate, matching the shift-range selection logic - Resolves target entry via entryCacheRef → root entries array → fetchEntryDetail(..).summary on cache miss - Scrolls target into view (block: nearest); updates lastAnchorIndexRef so subsequent shift-click ranges start from the keyboard-navigated row archive.rs — cached_bytes_excludes_avatar_blobs test: - Two tweet entries sharing an avatar blob (100B) and a media blob (900B) - Asserts refresh_entry_cached_bytes sets cached_bytes = 900 (not 1000) - Asserts list_root_entries summary: cached_bytes=900, cacheable_bytes=900, total_artifact_bytes=1000 — SIZE includes avatar, % cached denominator and numerator both exclude it * fix(frontend): guard j/k uncached-child fetch with monotonic token Rapid j/k over child rows that aren't in entryCacheRef triggers fetchEntryDetail calls in parallel. Without a guard the last one to settle wins, desyncing selectedUids (highlight) from selectedEntry (detail panel/URL). Fix: - jkSeqRef (monotonic counter) incremented before each server fetch; the .then() guard tok === jkSeqRef.current drops results from superseded navigations - handleRowClick increments jkSeqRef.current so any click also cancels an in-flight j/k fetch * fix(frontend): guard ctrl/meta cache-miss fetch; add / search shortcut App.jsx ctrl/meta branch: cache-miss fetchEntryDetail now captures tok = ++jkSeqRef.current before the fetch and gates selectEntry on tok === jkSeqRef.current — same pattern as the j/k handler — so a slow response after a later click/navigate can't overwrite selection. / key: reuses the Cmd+K/Ctrl+K handler path (focus+select search input, or pendingSearchFocus + archive view switch). Guards: editable targets (INPUT/TEXTAREA/SELECT/contentEditable) and modifier keys are checked before preventDefault() so typing / in inputs is untouched. * feat(core): attempt SpotifyTrack via yt-dlp instead of hard-failing Previously all Spotify sources returned an error claiming yt-dlp cannot download DRM-protected audio. yt-dlp does have a Spotify extractor (experimental, content-dependent), so refusing upfront is worse than trying and letting it report the real failure. SpotifyTrack now goes through the same yt-dlp metadata + audio-quality download path as YouTubeMusicTrack. SpotifyAlbum and SpotifyPlaylist still return an explicit error — fetch_playlist_info has YouTube-specific URL fallback logic that would produce bogus child URLs for Spotify flat entries; those sources need dedicated container handling first. * chore: remove docs/superpowers from repo and gitignore whitelist * feat(core+frontend): Spotify album/playlist capture via yt-dlp container path Previously SpotifyAlbum and SpotifyPlaylist hard-failed with a DRM error. yt-dlp does support Spotify (experimentally), so they now go through the same probe/container/child path as YouTube playlists. ytdlp.rs: - Extract normalize_item_url() helper used by both fetch_playlist_info and probe_playlist_qualities; eliminates the duplicate URL-normalization blocks and the YouTube-specific fallback_host variable - Fallback logic is now platform-aware: YouTube/YTM bare IDs → watch URL, Spotify → open.spotify.com/track/{id}, unknown → skip with warning capture.rs: - locator_to_playlist_url: accept SpotifyAlbum | SpotifyPlaylist so the probe-playlist endpoint accepts Spotify album/playlist URLs - Container branch: add SpotifyAlbum | SpotifyPlaylist to the matches! - is_audio: true for SpotifyAlbum | SpotifyPlaylist (audio-only, like YTM) - child_source: SpotifyTrack for Spotify containers (not YouTubeMusicTrack) - generate_entry_title and record_media_entry both use the correct child source so entity_kind, source_kind, and representation_kind are right CaptureDialog.jsx: - isPlaylistSource: recognise open.spotify.com/album/ and /playlist/, and spotify:album:ID / spotify:playlist:ID shorthands, so Spotify containers get the probe UI, per-track excludes, and sync toggle * fix(core+server): partial playlist refresh + child visibility inheritance database.rs: - finish_archive_run: reverted 'partial' status (violates CHECK constraint); restored binary completed/failed - get_run_completed_count(): new helper for callers that need to distinguish partial success without touching the DB status enum capture.rs: - CaptureResult gains completed_count: i64 (0 for single-item captures; populated from get_run_completed_count for container/playlist captures) - All CaptureResult construction sites updated routes.rs: - Playlist job_status mapping: mark job 'completed' when completed_count > 0 (even if status == 'failed'), so partially-successful playlist captures trigger onCaptured and show the archived entries without a manual reload. Truly zero-success runs (completed_count == 0, status == 'failed') stay failed as expected. - probe_playlist_handler error message updated to mention Spotify archive.rs (list_child_entries): - Add third OR arm: children are visible when their parent container is in a collection visible to the caller. Fixes empty child list for non-admin users with access to a playlist container but not its newly-created children (which are all in the default 'private' collection). * fix(server): use completed_count to distinguish partial from total failure finish_archive_run is binary (completed/failed) — a playlist where some tracks succeed and some fail returns status='failed'. Without this fix, the job mapping treated any failed status as a job failure, causing onCaptured to never fire and leaving successfully archived entries hidden until a manual reload. Now: job is 'failed' only when status='failed' && completed_count==0. Partial runs (completed_count > 0) map to a completed job so the UI refreshes and shows the entries that were successfully captured. * fix(core+server): exclude container from child success count; rename field; tests database.rs: - get_run_completed_child_count(): renamed from get_run_completed_count and scoped to child items only (parent_item_id IS NOT NULL). The container run item is always completed first, so the old function inflated the count by at least 1 for every playlist, making all-video-failed runs appear as partial successes. - New regression test: completed_child_count_excludes_container — completes both a root and a child item, asserts DB completed_count==2 while get_run_completed_child_count==1. capture.rs: - CaptureResult.completed_count renamed to completed_child_count to match the function and make the semantics unambiguous at the call site. routes.rs: - job_status decision now uses result.completed_child_count == 0 so that a playlist where every video fails (child_count==0) is correctly reported as a failed job, not a completed one. archive.rs: - New regression test: list_child_entries_inherits_parent_visibility — enrolls only the container in a USER-visible collection, asserts the child is visible to a USER caller and invisible to a GUEST caller. --- AGENTS.md | 3 + ARCHIVR-MENTAL-MODEL.md | 12 + crates/archivr-core/src/archive.rs | 432 +++- crates/archivr-core/src/capture.rs | 475 +++- crates/archivr-core/src/database.rs | 105 +- crates/archivr-core/src/downloader/ytdlp.rs | 258 +++ crates/archivr-server/src/routes.rs | 104 +- .../static/assets/index-D1LSSXsT.js | 47 - .../static/assets/index-D8ic-z4p.css | 1 + .../static/assets/index-DKombv9L.css | 1 - .../static/assets/index-DLdY9nrw.css | 1 - .../static/assets/index-De3b80Fv.js | 47 + .../static/assets/index-YmIQCrug.js | 47 - crates/archivr-server/static/index.html | 4 +- docs/README.md | 10 +- .../plans/2026-06-25-auth-foundation.md | 1977 ----------------- .../2026-06-25-auth-foundation-design.md | 403 ---- frontend/src/App.jsx | 147 +- frontend/src/api.js | 19 + frontend/src/components/CaptureDialog.jsx | 340 ++- frontend/src/components/EntriesView.jsx | 7 +- frontend/src/components/EntryRow.jsx | 174 +- frontend/src/styles.css | 269 ++- 23 files changed, 2243 insertions(+), 2640 deletions(-) delete mode 100644 crates/archivr-server/static/assets/index-D1LSSXsT.js create mode 100644 crates/archivr-server/static/assets/index-D8ic-z4p.css delete mode 100644 crates/archivr-server/static/assets/index-DKombv9L.css delete mode 100644 crates/archivr-server/static/assets/index-DLdY9nrw.css create mode 100644 crates/archivr-server/static/assets/index-De3b80Fv.js delete mode 100644 crates/archivr-server/static/assets/index-YmIQCrug.js delete mode 100644 docs/superpowers/plans/2026-06-25-auth-foundation.md delete mode 100644 docs/superpowers/specs/2026-06-25-auth-foundation-design.md diff --git a/AGENTS.md b/AGENTS.md index 5d42913..420772c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,8 @@ Three crates with a strict ownership split — **core owns truth; CLI and server Capture flow: locator → `determine_source()` (`crates/archivr-core/src/capture.rs`) routes by platform/shorthand (`yt:`, `x:`, `tweet:` …) → platform downloader (`downloader/ytdlp.rs`, `tweets.rs`, `singlefile.rs`, `http.rs`, `local.rs`) stages into `temp/` → SHA3-256 dedup (`hash.rs`, `downloader/store.rs`) moves blobs to `raw/A/B/HASH.EXT` → rows written to `archivr.sqlite` (runs, entries, artifacts, blobs) → served via `/api/archives/:id/...`. `CaptureConfig` carries per-request toggles (uBlock, reader mode, Freedium mirror, etc.); when `via_freedium` is set, the fetch URL is rewritten through `freedium-mirror.cfd` while the canonical DB URL stays the original locator. +YouTube playlists and channels produce a **parent container entry** with each video captured as a child entry. `downloader/ytdlp.rs` handles the playlist probe (fetching per-video quality metadata before archiving), the multi-video download loop, and sync mode (skipping already-archived videos when re-archiving a playlist or channel). + Per-archive layout (created by `archivr init`): `.archivr/` (name, store_path, `archivr.sqlite`) + sibling `store/` (`raw/`, `raw_tweets/`, `structured/`, `temp/`). Server-level auth lives in a **separate** `archivr-auth.sqlite` (users, sessions, API tokens, role bits GUEST=1/USER=2/ADMIN=4/OWNER=8). The server mounts multiple archives from a TOML registry (`crates/archivr-server/src/registry.rs`); routes are parameterized by `:archive_id`. @@ -78,6 +80,7 @@ No CI is configured; no rustfmt.toml/clippy.toml — default `cargo fmt`/`clippy - `crates/archivr-server/src/main.rs` — server bootstrap: config load, archive mounting, auth DB init, stalled-job recovery (running → failed on startup). - `crates/archivr-server/src/routes.rs` — all HTTP handlers and the router; grep here first for API work. - `crates/archivr-core/src/capture.rs` — `perform_capture()`, `Source` enum, shorthand parsing. +- `crates/archivr-core/src/downloader/ytdlp.rs` — yt-dlp integration; YouTube playlist/channel probe and download, sync mode logic. - `crates/archivr-core/src/database.rs` — single source of truth for all SQLite schema and queries (both archive and auth DBs). - `frontend/src/App.jsx` / `frontend/src/api.js` — frontend root state and API surface. - `docker/config.example.toml` — server config schema: `bind`, `auth_db_path`, repeated `[[archives]]` (`id`, `label`, `archive_path`). diff --git a/ARCHIVR-MENTAL-MODEL.md b/ARCHIVR-MENTAL-MODEL.md index ec90e05..eae3d4c 100644 --- a/ARCHIVR-MENTAL-MODEL.md +++ b/ARCHIVR-MENTAL-MODEL.md @@ -64,6 +64,17 @@ label = "Personal" archive_path = "/path/to/archive/.archivr" ``` +## Entry Nesting + +Entries support a two-level parent/child hierarchy. A **container entry** (playlist or channel) holds zero or more child entries (individual videos). Container entries have no primary media artifact of their own; their `total_artifact_bytes` is the sum of their children's bytes. + +Rules: +- Maximum nesting depth is 2 (root → child). Children cannot have children. +- The UI shows child entries collapsed under their parent, expandable with a chevron. +- Container entries are created by playlist/channel captures. Single-video and all other source types produce a standalone root entry with no children. + +If a feature touches how entries are parented or how the UI groups them, start in `archivr-core` (`database.rs` for schema, `archive.rs` for listing, `capture.rs` for creation). + ## How To Run It There are two user-facing binaries: @@ -155,6 +166,7 @@ sequenceDiagram | Capture orchestration, `Source` routing, `CaptureConfig` | `crates/archivr-core/src/capture.rs` | | Archive opening, listing entries, entry detail, runs | `crates/archivr-core/src/archive.rs` | | Download/save behavior | `crates/archivr-core/src/downloader/` | +| YouTube playlist/channel download, playlist probe, sync mode | `crates/archivr-core/src/downloader/ytdlp.rs` and `capture.rs` | | CLI commands, argument parsing, terminal output | `crates/archivr-cli/src/main.rs` | | Server API routes | `crates/archivr-server/src/routes.rs` | | Auth model (users, sessions, tokens, roles) | `crates/archivr-server/src/auth.rs` | diff --git a/crates/archivr-core/src/archive.rs b/crates/archivr-core/src/archive.rs index dd16e66..c299230 100644 --- a/crates/archivr-core/src/archive.rs +++ b/crates/archivr-core/src/archive.rs @@ -30,6 +30,10 @@ pub struct EntrySummary { pub has_favicon: bool, /// Bytes of blobs already on disk from an earlier entry (precomputed at capture time). pub cached_bytes: i64, + /// Number of direct child entries; 0 for non-container entries. + pub child_count: i64, + /// Total non-avatar artifact bytes (query-time; used as denominator for cache-hit %). + pub cacheable_bytes: i64, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] @@ -212,10 +216,12 @@ pub fn list_root_entries( e.visibility, si.canonical_url, COUNT(ea.id) AS artifact_count, - COALESCE(SUM(b.byte_size), 0) AS total_artifact_bytes, + COALESCE(SUM(b.byte_size), 0) + COALESCE((SELECT SUM(b2.byte_size) FROM archived_entries c2 JOIN entry_artifacts ea2 ON ea2.entry_id = c2.id JOIN blobs b2 ON b2.id = ea2.blob_id WHERE c2.parent_entry_id = e.id), 0) AS total_artifact_bytes, NULL AS parent_entry_uid, EXISTS(SELECT 1 FROM entry_artifacts fav WHERE fav.entry_id = e.id AND fav.artifact_role = 'favicon') AS has_favicon, - e.cached_bytes + e.cached_bytes, + (SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count, + COALESCE(SUM(CASE WHEN ea.artifact_role != 'avatar' THEN b.byte_size ELSE 0 END), 0) + COALESCE((SELECT SUM(b2.byte_size) FROM archived_entries c2 JOIN entry_artifacts ea2 ON ea2.entry_id = c2.id JOIN blobs b2 ON b2.id = ea2.blob_id WHERE c2.parent_entry_id = e.id), 0) AS cacheable_bytes FROM archived_entries e JOIN source_identities si ON si.id = e.source_identity_id LEFT JOIN entry_artifacts ea ON ea.entry_id = e.id @@ -248,6 +254,8 @@ pub fn list_root_entries( parent_entry_uid: row.get(9)?, has_favicon: row.get::<_, i64>(10)? != 0, cached_bytes: row.get(11)?, + child_count: row.get(12)?, + cacheable_bytes: row.get(13)?, }) })? .collect::>>()?; @@ -255,6 +263,40 @@ pub fn list_root_entries( Ok(entries) } +/// Fetches one `EntrySummary` for any entry (root or child) by uid. +/// Returns `None` if not found. +fn get_entry_summary( + conn: &rusqlite::Connection, + entry_uid: &str, +) -> Result> { + let sql = format!( + "{} {} WHERE e.entry_uid = ?1 GROUP BY e.id", + ENTRY_SELECT_COLS, ENTRY_FROM_JOINS, + ); + let mut stmt = conn.prepare(&sql)?; + let result = stmt + .query_row([entry_uid], |row| { + Ok(EntrySummary { + entry_uid: row.get(0)?, + archived_at: row.get(1)?, + source_kind: row.get(2)?, + entity_kind: row.get(3)?, + title: row.get(4)?, + visibility: row.get(5)?, + original_url: row.get(6)?, + artifact_count: row.get(7)?, + total_artifact_bytes: row.get(8)?, + parent_entry_uid: row.get(9)?, + has_favicon: row.get::<_, i64>(10)? != 0, + cached_bytes: row.get(11)?, + child_count: row.get(12)?, + cacheable_bytes: row.get(13)?, + }) + }) + .optional()?; + Ok(result) +} + pub fn get_entry_detail( conn: &rusqlite::Connection, entry_uid: &str, @@ -279,9 +321,7 @@ pub fn get_entry_detail( return Ok(None); }; - let summary = list_root_entries(conn, u32::MAX)? - .into_iter() - .find(|entry| entry.entry_uid == entry_uid) + let summary = get_entry_summary(conn, entry_uid)? .context("entry disappeared while loading detail")?; let mut stmt = conn.prepare( @@ -438,12 +478,121 @@ pub fn list_entries_for_collection( parent_entry_uid: row.get(9)?, has_favicon: row.get::<_, i64>(10)? != 0, cached_bytes: row.get(11)?, + child_count: row.get(12)?, + cacheable_bytes: row.get(13)?, }) })? .collect::>>()?; Ok(entries) } +/// Returns the direct children of the entry identified by `parent_entry_uid`, +/// ordered ascending by `archived_at, id` (preserves playlist ordinal feel). +/// Returns an empty vec if the parent has no children or does not exist. +pub fn list_child_entries( + conn: &rusqlite::Connection, + parent_entry_uid: &str, + caller_bits: u32, +) -> Result> { + let sql = format!( + "{} {} \ + WHERE e.parent_entry_id = (SELECT id FROM archived_entries WHERE entry_uid = ?1) \ + AND (\ + CAST(?2 AS INTEGER) & 12 != 0 \ + OR EXISTS (\ + SELECT 1 FROM collection_entries ce \ + WHERE ce.entry_id = e.id \ + AND ce.visibility_bits & CAST(?2 AS INTEGER) != 0\ + )\ + OR EXISTS (\ + SELECT 1 FROM collection_entries ce_p \ + WHERE ce_p.entry_id = (SELECT id FROM archived_entries WHERE entry_uid = ?1)\ + AND ce_p.visibility_bits & CAST(?2 AS INTEGER) != 0\ + )\ + ) \ + GROUP BY e.id \ + ORDER BY e.archived_at ASC, e.id ASC", + ENTRY_SELECT_COLS, ENTRY_FROM_JOINS, + ); + let mut stmt = conn.prepare(&sql)?; + let entries = stmt + .query_map( + rusqlite::params![parent_entry_uid, caller_bits as i64], + |row| { + Ok(EntrySummary { + entry_uid: row.get(0)?, + archived_at: row.get(1)?, + source_kind: row.get(2)?, + entity_kind: row.get(3)?, + title: row.get(4)?, + visibility: row.get(5)?, + original_url: row.get(6)?, + artifact_count: row.get(7)?, + total_artifact_bytes: row.get(8)?, + parent_entry_uid: row.get(9)?, + has_favicon: row.get::<_, i64>(10)? != 0, + cached_bytes: row.get(11)?, + child_count: row.get(12)?, + cacheable_bytes: row.get(13)?, + }) + }, + )? + .collect::>>()?; + Ok(entries) +} + +/// Returns the set of canonical URLs for all child entries archived under +/// **any** container entry whose own canonical URL matches `playlist_canonical_url`. +/// +/// Used in sync mode so the playlist capture path can skip videos that were +/// already downloaded in a previous run of the same playlist. +pub fn get_archived_playlist_child_urls( + conn: &rusqlite::Connection, + playlist_canonical_url: &str, +) -> Result> { + let mut stmt = conn.prepare( + "SELECT si_child.canonical_url \ + FROM archived_entries child \ + JOIN source_identities si_child ON si_child.id = child.source_identity_id \ + WHERE child.parent_entry_id IN ( \ + SELECT e.id \ + FROM archived_entries e \ + JOIN source_identities si ON si.id = e.source_identity_id \ + WHERE si.canonical_url = ?1 \ + AND e.parent_entry_id IS NULL \ + )", + )?; + let urls = stmt + .query_map([playlist_canonical_url], |row| row.get::<_, String>(0))? + .collect::>>()?; + Ok(urls) +} + +/// Finds the most recent container entry (parent_entry_id IS NULL) whose +/// canonical URL matches `canonical_url`. Returns its row id, or None if +/// no such entry exists. +/// +/// Used in sync mode to reuse an existing playlist/channel container instead +/// of creating a duplicate root entry on every sync run. +pub fn find_container_entry_id_by_canonical_url( + conn: &rusqlite::Connection, + canonical_url: &str, +) -> Result> { + let mut stmt = conn.prepare( + "SELECT e.id \ + FROM archived_entries e \ + JOIN source_identities si ON si.id = e.source_identity_id \ + WHERE si.canonical_url = ?1 \ + AND e.parent_entry_id IS NULL \ + ORDER BY e.archived_at DESC \ + LIMIT 1", + )?; + let id = stmt + .query_row([canonical_url], |row| row.get::<_, i64>(0)) + .optional()?; + Ok(id) +} + /// Resolves an artifact to its absolute on-disk path under `store_path`. /// /// `artifact.relpath` is a store-relative path (e.g. `raw/a/b/abc.pdf`). @@ -552,10 +701,12 @@ pub fn parse_search_query(raw: &str) -> Result { const ENTRY_SELECT_COLS: &str = "SELECT e.entry_uid, e.archived_at, e.source_kind, e.entity_kind, e.title, \ e.visibility, si.canonical_url, COUNT(ea.id) AS artifact_count, \ - COALESCE(SUM(b.byte_size), 0) AS total_artifact_bytes, \ + COALESCE(SUM(b.byte_size), 0) + COALESCE((SELECT SUM(b2.byte_size) FROM archived_entries c2 JOIN entry_artifacts ea2 ON ea2.entry_id = c2.id JOIN blobs b2 ON b2.id = ea2.blob_id WHERE c2.parent_entry_id = e.id), 0) AS total_artifact_bytes, \ parent.entry_uid AS parent_entry_uid, \ EXISTS(SELECT 1 FROM entry_artifacts fav WHERE fav.entry_id = e.id AND fav.artifact_role = 'favicon') AS has_favicon, \ - e.cached_bytes"; + e.cached_bytes, \ + (SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count, \ + COALESCE(SUM(CASE WHEN ea.artifact_role != 'avatar' THEN b.byte_size ELSE 0 END), 0) + COALESCE((SELECT SUM(b2.byte_size) FROM archived_entries c2 JOIN entry_artifacts ea2 ON ea2.entry_id = c2.id JOIN blobs b2 ON b2.id = ea2.blob_id WHERE c2.parent_entry_id = e.id), 0) AS cacheable_bytes"; const ENTRY_FROM_JOINS: &str = "FROM archived_entries e \ JOIN source_identities si ON si.id = e.source_identity_id \ @@ -663,6 +814,8 @@ pub fn search_entries( parent_entry_uid: row.get(9)?, has_favicon: row.get::<_, i64>(10)? != 0, cached_bytes: row.get(11)?, + child_count: row.get(12)?, + cacheable_bytes: row.get(13)?, }) })? .collect::>>()?; @@ -870,7 +1023,9 @@ pub fn entries_for_tag( COALESCE(SUM(b.byte_size), 0) AS total_artifact_bytes, parent.entry_uid AS parent_entry_uid, EXISTS(SELECT 1 FROM entry_artifacts fav WHERE fav.entry_id = e.id AND fav.artifact_role = 'favicon') AS has_favicon, - e.cached_bytes + e.cached_bytes, + (SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count, + COALESCE(SUM(CASE WHEN ea.artifact_role != 'avatar' THEN b.byte_size ELSE 0 END), 0) AS cacheable_bytes FROM archived_entries e JOIN source_identities si ON si.id = e.source_identity_id LEFT JOIN entry_artifacts ea ON ea.entry_id = e.id @@ -896,6 +1051,8 @@ pub fn entries_for_tag( parent_entry_uid: row.get(9)?, has_favicon: row.get::<_, i64>(10)? != 0, cached_bytes: row.get(11)?, + child_count: row.get(12)?, + cacheable_bytes: row.get(13)?, }) })? .collect::>>()?; @@ -1618,4 +1775,263 @@ mod tests { ); assert_eq!(results[0].entry_uid, parent.entry_uid); } + // ── sync helper tests ──────────────────────────────────────────────────────── + + #[test] + fn find_container_entry_id_returns_none_when_absent() { + let (conn, _, _) = make_tag_test_db(); + let result = find_container_entry_id_by_canonical_url( + &conn, + "https://www.youtube.com/playlist?list=PLnobody", + ) + .unwrap(); + assert!(result.is_none(), "should return None when no entry exists"); + } + + #[test] + fn find_container_entry_id_returns_root_entry() { + let (conn, user_id, run_id) = make_tag_test_db(); + let playlist_url = "https://www.youtube.com/playlist?list=PLtest"; + let container = make_entry_in_db(&conn, user_id, run_id, None, None, "My Playlist", playlist_url); + let result = find_container_entry_id_by_canonical_url(&conn, playlist_url) + .unwrap() + .expect("should find the container"); + assert_eq!(result, container.id); + } + + #[test] + fn find_container_entry_id_ignores_child_entries() { + let (conn, user_id, run_id) = make_tag_test_db(); + let child_url = "https://www.youtube.com/watch?v=abc123"; + // Create a parent container and a child whose canonical URL happens to be + // what we're querying — should NOT be returned since it has parent_entry_id set. + let parent = make_entry_in_db(&conn, user_id, run_id, None, None, "PL", "https://example.com/pl"); + let _child = make_entry_in_db(&conn, user_id, run_id, Some(parent.id), Some(parent.id), "Vid", child_url); + let result = find_container_entry_id_by_canonical_url(&conn, child_url).unwrap(); + assert!(result.is_none(), "child entry should not be returned as a container"); + } + + #[test] + fn get_archived_playlist_child_urls_empty_when_no_playlist() { + let (conn, _, _) = make_tag_test_db(); + let urls = get_archived_playlist_child_urls( + &conn, + "https://www.youtube.com/playlist?list=PLnone", + ) + .unwrap(); + assert!(urls.is_empty()); + } + + #[test] + fn get_archived_playlist_child_urls_returns_children() { + let (conn, user_id, run_id) = make_tag_test_db(); + let playlist_url = "https://www.youtube.com/playlist?list=PLchildren"; + let container = make_entry_in_db(&conn, user_id, run_id, None, None, "Playlist", playlist_url); + + let child1_url = "https://www.youtube.com/watch?v=vid1"; + let child2_url = "https://www.youtube.com/watch?v=vid2"; + make_entry_in_db(&conn, user_id, run_id, Some(container.id), Some(container.id), "Vid 1", child1_url); + make_entry_in_db(&conn, user_id, run_id, Some(container.id), Some(container.id), "Vid 2", child2_url); + + let urls = get_archived_playlist_child_urls(&conn, playlist_url).unwrap(); + assert_eq!(urls.len(), 2); + assert!(urls.contains(child1_url), "should contain vid1"); + assert!(urls.contains(child2_url), "should contain vid2"); + } + + #[test] + fn get_archived_playlist_child_urls_excludes_other_playlists() { + let (conn, user_id, run_id) = make_tag_test_db(); + let pl_a = "https://www.youtube.com/playlist?list=PLA"; + let pl_b = "https://www.youtube.com/playlist?list=PLB"; + let container_a = make_entry_in_db(&conn, user_id, run_id, None, None, "PL A", pl_a); + let container_b = make_entry_in_db(&conn, user_id, run_id, None, None, "PL B", pl_b); + + let vid_a = "https://www.youtube.com/watch?v=forA"; + let vid_b = "https://www.youtube.com/watch?v=forB"; + make_entry_in_db(&conn, user_id, run_id, Some(container_a.id), Some(container_a.id), "A Vid", vid_a); + make_entry_in_db(&conn, user_id, run_id, Some(container_b.id), Some(container_b.id), "B Vid", vid_b); + + let urls_a = get_archived_playlist_child_urls(&conn, pl_a).unwrap(); + assert_eq!(urls_a.len(), 1); + assert!(urls_a.contains(vid_a)); + assert!(!urls_a.contains(vid_b), "should not include children of other playlists"); + } + + #[test] + fn cached_bytes_excludes_avatar_blobs() { + let (conn, user_id, run_id) = make_tag_test_db(); + + // entry_a is older (created first, gets the lower id) + let entry_a = make_entry_in_db( + &conn, + user_id, + run_id, + None, + None, + "Tweet A", + "https://twitter.com/user/status/1", + ); + // entry_b is newer (created second, higher id — tiebreak on id when archived_at is equal) + let entry_b = make_entry_in_db( + &conn, + user_id, + run_id, + None, + None, + "Tweet B", + "https://twitter.com/user/status/2", + ); + + // Shared avatar blob: 100 bytes + let avatar_blob_id = database::upsert_blob( + &conn, + &database::BlobRecord { + sha256: "avatar_sha256_test".to_string(), + byte_size: 100, + mime_type: Some("image/jpeg".to_string()), + extension: Some("jpg".to_string()), + raw_relpath: "raw/av/at/avatar.jpg".to_string(), + }, + ) + .unwrap(); + + // Shared media blob: 900 bytes + let media_blob_id = database::upsert_blob( + &conn, + &database::BlobRecord { + sha256: "media_sha256_test".to_string(), + byte_size: 900, + mime_type: Some("image/png".to_string()), + extension: Some("png".to_string()), + raw_relpath: "raw/me/di/media.png".to_string(), + }, + ) + .unwrap(); + + // Attach both artifacts to entry_a + database::add_entry_artifact( + &conn, + &database::NewArtifact { + entry_id: entry_a.id, + artifact_role: "avatar".to_string(), + storage_area: "raw".to_string(), + relpath: "raw/av/at/avatar.jpg".to_string(), + blob_id: Some(avatar_blob_id), + logical_path: None, + metadata_json: None, + }, + ) + .unwrap(); + database::add_entry_artifact( + &conn, + &database::NewArtifact { + entry_id: entry_a.id, + artifact_role: "primary_media".to_string(), + storage_area: "raw".to_string(), + relpath: "raw/me/di/media.png".to_string(), + blob_id: Some(media_blob_id), + logical_path: None, + metadata_json: None, + }, + ) + .unwrap(); + + // Attach both artifacts to entry_b (same blobs — simulates shared avatar/media) + database::add_entry_artifact( + &conn, + &database::NewArtifact { + entry_id: entry_b.id, + artifact_role: "avatar".to_string(), + storage_area: "raw".to_string(), + relpath: "raw/av/at/avatar.jpg".to_string(), + blob_id: Some(avatar_blob_id), + logical_path: None, + metadata_json: None, + }, + ) + .unwrap(); + database::add_entry_artifact( + &conn, + &database::NewArtifact { + entry_id: entry_b.id, + artifact_role: "primary_media".to_string(), + storage_area: "raw".to_string(), + relpath: "raw/me/di/media.png".to_string(), + blob_id: Some(media_blob_id), + logical_path: None, + metadata_json: None, + }, + ) + .unwrap(); + + // Recompute cached_bytes for entry_b. + // The query excludes avatar-role artifacts and counts only non-avatar blobs + // that are already held by an earlier entry. entry_a (lower id) owns the + // same media blob, so cached_bytes for entry_b should be 900, not 1000. + database::refresh_entry_cached_bytes(&conn, entry_b.id).unwrap(); + + // --- Assert raw DB value --- + let cached_bytes_db: i64 = conn + .query_row( + "SELECT cached_bytes FROM archived_entries WHERE id = ?1", + [entry_b.id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!( + cached_bytes_db, 900, + "cached_bytes should be 900 (media only); avatar blob must be excluded" + ); + + // --- Assert list_root_entries summary for entry_b --- + let entries = list_root_entries(&conn, 12).unwrap(); // 12 = ADMIN bits + let summary_b = entries + .iter() + .find(|e| e.entry_uid == entry_b.entry_uid) + .expect("entry_b must appear in list_root_entries"); + + assert_eq!( + summary_b.cached_bytes, 900, + "summary cached_bytes must equal 900 (precomputed, avatar excluded)" + ); + assert_eq!( + summary_b.cacheable_bytes, 900, + "cacheable_bytes (non-avatar total) must be 900 for entry_b" + ); + assert_eq!( + summary_b.total_artifact_bytes, 1000, + "total_artifact_bytes must be 1000 (avatar 100 + media 900)" + ); + } + + #[test] + fn list_child_entries_inherits_parent_visibility() { + // Regression: non-admin users who can see a playlist container through a + // collection must also see its children. The child is auto-enrolled in the + // default collection with private visibility bits — it has no *visible* + // collection membership of its own, so it would be filtered out without + // the parent-collection OR arm in list_child_entries. + let (conn, user_id, run_id) = make_tag_test_db(); + + let container = make_entry_in_db(&conn, user_id, run_id, None, None, + "Playlist", "https://example.com/pl"); + let child = make_entry_in_db(&conn, user_id, run_id, + Some(container.id), Some(container.id), + "Video 1", "https://example.com/pl/v1"); + + // Enroll the container (but NOT the child) in a USER-visible collection (bits=2). + let coll = database::create_collection(&conn, "My List", "my-list", 2).unwrap(); + database::add_entry_to_collection(&conn, coll.id, container.id, 2).unwrap(); + + // USER caller (bits=2): child must be visible through parent's collection. + let children = list_child_entries(&conn, &container.entry_uid, 2).unwrap(); + assert_eq!(children.len(), 1, "child must be visible via parent collection"); + assert_eq!(children[0].entry_uid, child.entry_uid); + + // GUEST caller (bits=1): parent collection has bits=2, so child must NOT be visible. + let guest_children = list_child_entries(&conn, &container.entry_uid, 1).unwrap(); + assert!(guest_children.is_empty(), "guest must not see children of a USER-only collection"); + } + } diff --git a/crates/archivr-core/src/capture.rs b/crates/archivr-core/src/capture.rs index 73fd9bd..012abca 100644 --- a/crates/archivr-core/src/capture.rs +++ b/crates/archivr-core/src/capture.rs @@ -7,7 +7,7 @@ use std::{ fs, path::{Path, PathBuf}, }; -use crate::{archive::ArchivePaths, database, downloader, twitter::parse_tweet_id}; +use crate::{archive::{self, ArchivePaths}, database, downloader, twitter::parse_tweet_id}; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Source { @@ -37,8 +37,11 @@ pub enum Source { pub struct CaptureResult { pub run_uid: String, pub status: String, + /// Number of successfully completed child items (playlist/container captures only). + /// Zero for single-item captures. Used by the server to distinguish a fully-failed + /// playlist run from a partial success without needing a new DB status value. + pub completed_child_count: i64, /// `true` when uBlock was requested but the extension path was not found. - /// The capture succeeded without ad-blocking; the UI should warn the user. pub ublock_skipped: bool, /// `true` when cookie-consent extension was requested but the path was not found. pub cookie_ext_skipped: bool, @@ -90,6 +93,17 @@ pub struct CaptureConfig { /// Route WebPage captures through the Freedium mirror to bypass paywalls. /// The original locator is still recorded in the DB; only the fetch URL changes. pub via_freedium: bool, + /// Per-item quality overrides and include-set for playlist captures. + /// Keys are yt-dlp video IDs (e.g. "dQw4w9WgXcQ"); values are quality strings + /// accepted by `quality_format` ("best", "audio", "1080p", etc.). + /// - Empty map: all playlist items are downloaded using the global `quality`. + /// - Non-empty map: ONLY items whose ID appears as a key are downloaded; + /// items absent from the map are skipped entirely. This is how the UI's + /// per-video delete button excludes specific videos from a capture. + pub per_item_quality: HashMap, + /// When true, skip playlist items whose URL is already archived as a child + /// of any container entry with the same canonical playlist URL. + pub sync: bool, } /// Resolves which cookies apply to `url` by evaluating all rules in ordinal order. @@ -200,6 +214,12 @@ fn generate_entry_title(source: Source, meta: &PlatformMetadata) -> String { Source::Other => "Archived Content".to_string(), } } +/// Returns true when `s` is a valid bare YouTube video ID: +/// exactly 11 characters from the set `[A-Za-z0-9_-]`. +fn is_youtube_video_id(s: &str) -> bool { + s.len() == 11 && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') +} + fn expand_shorthand_to_url(path: &str, source: &Source) -> String { // YouTube shorthands: yt:video/ID, yt:playlist/ID, yt:@handle, yt:channel/ID, etc. @@ -227,6 +247,10 @@ fn expand_shorthand_to_url(path: &str, source: &Source) -> String { if let Some(handle) = after.strip_prefix("@") { return format!("https://www.youtube.com/@{handle}"); } + // bare yt:ID — validated 11-char video ID + if is_youtube_video_id(after) { + return format!("https://www.youtube.com/watch?v={after}"); + } } } @@ -317,6 +341,11 @@ fn determine_source(path: &str) -> Source { { return Source::YouTubeChannel; } + + // bare yt:ID — exactly 11 chars [A-Za-z0-9_-], no slash/@ prefixes + if is_youtube_video_id(after_scheme) { + return Source::YouTubeVideo; + } } // Shorthand scheme: ytm: @@ -536,6 +565,21 @@ pub fn locator_to_ytdlp_url(locator: &str) -> Option { } } +/// Returns the canonical URL for playlist/channel locators that yt-dlp can +/// expand with `--flat-playlist`, or `None` for non-playlist sources. +/// Intentionally separate from `locator_to_ytdlp_url`, which excludes playlists. +pub fn locator_to_playlist_url(locator: &str) -> Option { + let source = determine_source(locator); + match source { + Source::YouTubePlaylist + | Source::YouTubeChannel + | Source::YouTubeMusicPlaylist + | Source::SpotifyAlbum + | Source::SpotifyPlaylist => Some(expand_shorthand_to_url(locator, &source)), + _ => None, + } +} + fn hash_exists(hash: &str, file_extension: &str, store_path: &Path) -> Result { let path = store_path.join(raw_relative_path_from_hash(hash, file_extension)?); Ok(path.exists()) @@ -660,6 +704,8 @@ fn record_media_entry( file_extension: &str, byte_size: i64, title: Option, + parent_entry_id: Option, + root_entry_id: Option, ) -> Result { debug_assert!(run.run_uid.starts_with("run_")); debug_assert!(item.item_uid.starts_with("item_")); @@ -686,8 +732,8 @@ fn record_media_entry( &database::NewEntry { source_identity_id, archive_run_id: run.id, - parent_entry_id: None, - root_entry_id: None, + parent_entry_id, + root_entry_id, created_by_user_id: user_id, owned_by_user_id: user_id, source_kind: source_kind.to_string(), @@ -720,6 +766,59 @@ fn record_media_entry( Ok(entry) } +/// Creates a playlist/channel container entry with no blob and no primary-media artifact. +/// Calls `complete_archive_run_item` on the provided item. +fn record_container_entry( + conn: &rusqlite::Connection, + store_path: &Path, + user_id: i64, + run: &database::ArchiveRun, + item: &database::ArchiveRunItem, + requested_locator: &str, + canonical_locator: &str, + source: Source, + title: Option, + playlist_id: &str, + uploader: Option<&str>, +) -> Result { + let (source_kind, entity_kind, representation_kind) = source_metadata(source); + let source_identity_id = database::upsert_source_identity( + conn, + source_kind, + entity_kind, + Some(playlist_id), + Some(canonical_locator), + canonical_locator, + )?; + let entry = database::create_archived_entry( + conn, + &database::NewEntry { + source_identity_id, + 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: source_kind.to_string(), + entity_kind: entity_kind.to_string(), + title, + visibility: "private".to_string(), + representation_kind: representation_kind.to_string(), + source_metadata_json: json!({ + "requested_locator": requested_locator, + "canonical_locator": canonical_locator, + "playlist_id": playlist_id, + "uploader": uploader, + }) + .to_string(), + display_metadata_json: None, + }, + )?; + create_structured_root(store_path, &entry)?; + database::complete_archive_run_item(conn, item.id, entry.id)?; + Ok(entry) +} + /// Extracts PlatformMetadata from a tweet JSON string. /// Returns Default on any parse failure. fn tweet_metadata_from_json(json_str: &str) -> PlatformMetadata { @@ -957,25 +1056,273 @@ pub fn perform_capture( )); } - // Sources: Spotify — not downloadable; Spotify audio is DRM-protected. - if matches!(source, Source::SpotifyTrack | Source::SpotifyAlbum | Source::SpotifyPlaylist) { - return Err(fail_run( - &conn, - &run, - &item, - "Spotify downloads are not supported: Spotify audio is DRM-protected and cannot \ - be downloaded by yt-dlp. Archive the equivalent YouTube Music track instead.", - )); - } + // Sources: YouTube playlists, YouTube channels, YouTube Music playlists, + // Spotify albums, Spotify playlists — probe via yt-dlp flat-playlist, + // create a root container entry, then download each track as a child entry. + // `run` and `item` are already created above; `item` acts as the container item. + if matches!( + source, + Source::YouTubePlaylist + | Source::YouTubeChannel + | Source::YouTubeMusicPlaylist + | Source::SpotifyAlbum + | Source::SpotifyPlaylist + ) { + // `canonical_url` already holds the expanded URL (same value as `path` later). + let playlist_info = match downloader::ytdlp::fetch_playlist_info(&canonical_url, &cookies) { + Ok(info) => info, + Err(e) => { + return Err(fail_run( + &conn, + &run, + &item, + &format!("Failed to fetch playlist info: {e:#}"), + )); + } + }; - // Sources: YouTube Music Playlist — container expansion not yet implemented. - if source == Source::YouTubeMusicPlaylist { - return Err(fail_run( - &conn, - &run, - &item, - "YouTube Music playlist archiving is not yet implemented.", - )); + let container_title = playlist_info + .title + .clone() + .or_else(|| playlist_info.uploader.clone().map(|u| format!("{u} (channel)"))); + + // Sync mode: reuse an existing container so we don't create a duplicate + // root entry on every sync run. Non-sync always creates a fresh container. + let (container_id, already_archived) = if config.sync { + match archive::find_container_entry_id_by_canonical_url(&conn, &canonical_url) { + Err(e) => { + return Err(fail_run( + &conn, &run, &item, + &format!("Failed to query existing container: {e:#}"), + )); + } + Ok(Some(existing_id)) => { + // Container already exists — mark the run item done pointing at it; + // don't call record_container_entry (that would create a duplicate). + if let Err(e) = database::complete_archive_run_item(&conn, item.id, existing_id) { + return Err(fail_run( + &conn, &run, &item, + &format!("Failed to complete run item for existing container: {e:#}"), + )); + } + let archived = match archive::get_archived_playlist_child_urls(&conn, &canonical_url) { + Ok(set) => set, + Err(e) => return Err(fail_run( + &conn, &run, &item, + &format!("Failed to query archived playlist children: {e:#}"), + )), + }; + (existing_id, archived) + } + Ok(None) => { + // First sync run for this playlist — create the container normally. + let e = match record_container_entry( + &conn, store_path, user_id, &run, &item, locator, &canonical_url, + source, container_title, &playlist_info.playlist_id, + playlist_info.uploader.as_deref(), + ) { + Ok(e) => e, + Err(e) => return Err(fail_run( + &conn, &run, &item, + &format!("Failed to create container entry: {e:#}"), + )), + }; + (e.id, std::collections::HashSet::new()) + } + } + } else { + let e = match record_container_entry( + &conn, store_path, user_id, &run, &item, locator, &canonical_url, + source, container_title, &playlist_info.playlist_id, + playlist_info.uploader.as_deref(), + ) { + Ok(e) => e, + Err(e) => return Err(fail_run( + &conn, &run, &item, + &format!("Failed to create container entry: {e:#}"), + )), + }; + (e.id, std::collections::HashSet::new()) + }; + + let is_audio = matches!( + source, + Source::YouTubeMusicPlaylist | Source::SpotifyAlbum | Source::SpotifyPlaylist + ); + let child_quality: Option<&str> = if is_audio { Some("audio") } else { quality }; + let child_entity_kind = if is_audio { "music" } else { "video" }; + + for (ordinal, playlist_item) in playlist_info.items.iter().enumerate() { + // Sync: skip items already archived — no run item created, so + // refresh_run_counters sees only newly-attempted items. + if config.sync && already_archived.contains(&playlist_item.url) { + continue; + } + // Per-item exclusion: when per_item_quality is non-empty (frontend + // probe ran and the user confirmed the item list), only download + // items whose ID appears in the map. IDs absent from a non-empty + // map were removed by the user via the delete button before submit. + if !config.per_item_quality.is_empty() + && !config.per_item_quality.contains_key(&playlist_item.id) + { + continue; + } + + + let child_timestamp = format!( + "{}-{}", + Local::now().format("%Y-%m-%dT%H-%M-%S%.3f"), + Uuid::new_v4().simple(), + ); + + let child_item = match database::create_archive_run_item( + &conn, + run.id, + Some(item.id), + ordinal as i64, + &playlist_item.url, + Some(&playlist_item.url), + source_kind, + child_entity_kind, + ) { + Ok(i) => i, + Err(e) => { + eprintln!("warn: playlist item {} create_run_item failed: {e:#}", playlist_item.url); + continue; + } + }; + + // Fetch metadata for the child title (best-effort). + let child_meta_json = downloader::ytdlp::fetch_metadata(&playlist_item.url, &cookies); + let child_title: Option = match &child_meta_json { + Some(json) => { + let meta = downloader::metadata::extract_from_ytdlp_json(json); + Some(generate_entry_title( + match source { + Source::SpotifyAlbum | Source::SpotifyPlaylist => Source::SpotifyTrack, + _ if is_audio => Source::YouTubeMusicTrack, + _ => Source::YouTubeVideo, + }, + &meta, + )) + } + None => playlist_item.title.clone(), + }; + + // Per-item quality override keyed by yt-dlp video ID; fall back to + // playlist-level quality. YTM playlists are always audio-only — + // per_item_quality overrides are ignored so the audio-only guarantee + // cannot be bypassed by a caller sending e.g. "best". + let effective_child_quality: Option<&str> = if is_audio { + Some("audio") + } else { + config + .per_item_quality + .get(&playlist_item.id) + .map(String::as_str) + .or(child_quality) + }; + + // Download the media. + match downloader::ytdlp::download( + playlist_item.url.clone(), + store_path, + &child_timestamp, + effective_child_quality, + &cookies, + ) { + Ok((hash, file_extension)) => { + let temp_file = store_path + .join("temp") + .join(&child_timestamp) + .join(format!("{child_timestamp}{file_extension}")); + let byte_size = match fs::metadata(&temp_file) { + Ok(m) => m.len() as i64, + Err(e) => { + eprintln!("warn: stat child temp file: {e:#}"); + let _ = database::fail_archive_run_item( + &conn, child_item.id, + &format!("failed to stat downloaded file: {e:#}"), + ); + continue; + } + }; + if !hash_exists(&hash, &file_extension, store_path).unwrap_or(false) { + if let Err(e) = move_temp_to_raw(&temp_file, &hash, store_path) { + eprintln!("warn: move_temp_to_raw child: {e:#}"); + let _ = database::fail_archive_run_item( + &conn, child_item.id, + &format!("failed to move downloaded file: {e:#}"), + ); + continue; + } + } + let _ = fs::remove_dir_all(store_path.join("temp").join(&child_timestamp)); + + let child_source = match source { + Source::SpotifyAlbum | Source::SpotifyPlaylist => Source::SpotifyTrack, + _ if is_audio => Source::YouTubeMusicTrack, + _ => Source::YouTubeVideo, + }; + match record_media_entry( + &conn, + store_path, + user_id, + &run, + &child_item, + &playlist_item.url, + &playlist_item.url, + child_source, + &hash, + &file_extension, + byte_size, + child_title, + Some(container_id), + Some(container_id), + ) { + Ok(child_entry) => { + let _ = database::refresh_entry_cached_bytes(&conn, child_entry.id); + } + Err(e) => { + eprintln!("warn: record child entry: {e:#}"); + let _ = database::fail_archive_run_item( + &conn, child_item.id, + &format!("failed to record entry: {e:#}"), + ); + } + } + } + Err(e) => { + let _ = fs::remove_dir_all(store_path.join("temp").join(&child_timestamp)); + eprintln!("warn: yt-dlp child download failed for {}: {e:#}", playlist_item.url); + let _ = database::fail_archive_run_item( + &conn, child_item.id, + &format!("yt-dlp download failed: {e:#}"), + ); + } + } + } + + database::refresh_entry_cached_bytes(&conn, container_id)?; + database::finish_archive_run(&conn, run.id)?; + + let run_status: String = conn + .query_row( + "SELECT status FROM archive_runs WHERE id = ?1", + [run.id], + |row| row.get(0), + ) + .unwrap_or_else(|_| "completed".to_string()); + let completed_child_count: i64 = database::get_run_completed_child_count(&conn, run.id) + .unwrap_or(0); + + return Ok(CaptureResult { + run_uid: run.run_uid.clone(), + status: run_status, + completed_child_count, + ublock_skipped: false, + cookie_ext_skipped: false, + }); } // Source: generic HTTP/S file URL @@ -1018,12 +1365,15 @@ pub fn perform_capture( &file_extension, byte_size, title_hint, + None, + None, )?; database::refresh_entry_cached_bytes(&conn, entry.id)?; database::finish_archive_run(&conn, run.id)?; return Ok(CaptureResult { run_uid: run.run_uid.clone(), status: "completed".to_string(), + completed_child_count: 0, ublock_skipped: false, cookie_ext_skipped: false, }); @@ -1144,8 +1494,10 @@ pub fn perform_capture( &html_hash, &file_extension, byte_size, - entry_title, - )?; + entry_title, + None, + None, + )?; // 5. Add favicon artifact if we captured one. if let Some((fav_relpath, fav_blob_id)) = favicon_info { @@ -1195,6 +1547,7 @@ pub fn perform_capture( return Ok(CaptureResult { run_uid: run.run_uid.clone(), status: "completed".to_string(), + completed_child_count: 0, ublock_skipped: result.ublock_skipped, cookie_ext_skipped: result.cookie_ext_skipped, }); @@ -1252,6 +1605,7 @@ pub fn perform_capture( return Ok(CaptureResult { run_uid: run.run_uid.clone(), status: "completed".to_string(), + completed_child_count: 0, ublock_skipped: false, cookie_ext_skipped: false, }); @@ -1275,6 +1629,7 @@ pub fn perform_capture( let ytdlp_metadata_json: Option = match source { Source::YouTubeVideo | Source::YouTubeMusicTrack + | Source::SpotifyTrack | Source::X | Source::Instagram | Source::Facebook @@ -1315,7 +1670,7 @@ pub fn perform_capture( } } } - Source::YouTubeMusicTrack => { + Source::YouTubeMusicTrack | Source::SpotifyTrack => { // Music tracks are always audio-only regardless of the caller's quality hint. match downloader::ytdlp::download(path.clone(), store_path, ×tamp, Some("audio"), &cookies) { Ok(result) => result, @@ -1342,14 +1697,7 @@ pub fn perform_capture( } } } - Source::YouTubePlaylist | Source::YouTubeChannel => { - return Err(fail_run( - &conn, - &run, - &item, - "Playlist and channel container expansion are not yet implemented.", - )); - } + Source::YouTubePlaylist | Source::YouTubeChannel => unreachable!(), _ => unreachable!(), }; let temp_file = store_path @@ -1402,6 +1750,8 @@ pub fn perform_capture( &file_extension, byte_size, entry_title, + None, + None, )?; database::refresh_entry_cached_bytes(&conn, media_entry.id)?; database::finish_archive_run(&conn, run.id)?; @@ -1409,6 +1759,7 @@ pub fn perform_capture( Ok(CaptureResult { run_uid: run.run_uid.clone(), status: "completed".to_string(), + completed_child_count: 0, ublock_skipped: false, cookie_ext_skipped: false, }) @@ -1790,6 +2141,15 @@ mod tests { url: "youtube:@CoreDumpped", expected: Source::YouTubeChannel, }, + // Bare video ID — exactly 11 chars [A-Za-z0-9_-] + TestCase { url: "yt:dQw4w9WgXcQ", expected: Source::YouTubeVideo }, + TestCase { url: "youtube:dQw4w9WgXcQ", expected: Source::YouTubeVideo }, + TestCase { url: "yt:a_b-c_d-e_4", expected: Source::YouTubeVideo }, + // Non-ID: wrong length (9 chars) → Other + TestCase { url: "yt:not-video", expected: Source::Other }, + // Reserved prefixes still route correctly when segment looks like an ID + TestCase { url: "yt:playlist/dQw4w9WgXcQ", expected: Source::YouTubePlaylist }, + TestCase { url: "yt:@dQw4w9WgXcQ", expected: Source::YouTubeChannel }, ]; for case in &shorthand_cases { @@ -1802,6 +2162,18 @@ mod tests { } } + #[test] + fn test_is_youtube_video_id() { + assert!(is_youtube_video_id("dQw4w9WgXcQ"), "canonical ID"); + assert!(is_youtube_video_id("a_b-c_d-e_4"), "11-char ID with _ and -"); + assert!(!is_youtube_video_id(""), "empty"); + assert!(!is_youtube_video_id("short"), "too short"); + assert!(!is_youtube_video_id("toolong12345"), "too long (12 chars)"); + assert!(!is_youtube_video_id("dQw4w9WgXc!"), "invalid char (! at pos 11)"); + assert!(!is_youtube_video_id("not-video"), "9 chars — too short"); + assert!(!is_youtube_video_id("dQw4w9WgXC!!"), "12 chars + bad char"); + } + #[test] fn test_youtube_music_sources() { // --- determine_source --- @@ -2326,4 +2698,41 @@ mod tests { ); } + #[test] + fn locator_to_playlist_url_accepts_playlist_shorthands() { + // yt: playlist shorthand + assert_eq!( + locator_to_playlist_url("yt:playlist/PLtest123"), + Some("https://www.youtube.com/playlist?list=PLtest123".to_string()), + ); + // YouTube Music playlist shorthand + assert_eq!( + locator_to_playlist_url("ytm:playlist/PLmus456"), + Some("https://music.youtube.com/playlist?list=PLmus456".to_string()), + ); + // Full YT playlist URL passes through (expand_shorthand_to_url is identity for full URLs) + let full = "https://www.youtube.com/playlist?list=PLabc"; + assert_eq!(locator_to_playlist_url(full), Some(full.to_string())); + } + + #[test] + fn locator_to_playlist_url_accepts_channel_shorthands() { + let url = locator_to_playlist_url("yt:@MyChan"); + assert!(url.is_some(), "channel @ shorthand should return Some"); + let u = url.unwrap(); + assert!(u.contains("youtube.com"), "should be a youtube URL: {u}"); + } + + #[test] + fn locator_to_playlist_url_rejects_non_playlist_sources() { + // Single video + assert_eq!(locator_to_playlist_url("yt:dQw4w9WgXcQ"), None); + // Tweet + assert_eq!(locator_to_playlist_url("tweet:1234567890"), None); + // YTM track + assert_eq!(locator_to_playlist_url("ytm:MntbN1DdEP0"), None); + // Plain web URL + assert_eq!(locator_to_playlist_url("https://example.com/page"), None); + } + } diff --git a/crates/archivr-core/src/database.rs b/crates/archivr-core/src/database.rs index e158734..9f74074 100644 --- a/crates/archivr-core/src/database.rs +++ b/crates/archivr-core/src/database.rs @@ -390,6 +390,7 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> { JOIN blobs b ON b.id = ea.blob_id WHERE ea.entry_id = archived_entries.id AND ea.blob_id IS NOT NULL + AND ea.artifact_role != 'avatar' AND EXISTS ( SELECT 1 FROM entry_artifacts ea2 @@ -401,6 +402,33 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> { ) );", )?; + } else { + // Re-migration: strip avatar blobs from cached_bytes on entries that + // already had the column populated before this filter was introduced. + // Scoped to entries with avatar artifacts only; fast and idempotent. + conn.execute_batch( + "UPDATE archived_entries + SET cached_bytes = ( + SELECT COALESCE(SUM(b.byte_size), 0) + FROM entry_artifacts ea + JOIN blobs b ON b.id = ea.blob_id + WHERE ea.entry_id = archived_entries.id + AND ea.blob_id IS NOT NULL + AND ea.artifact_role != 'avatar' + AND EXISTS ( + SELECT 1 + FROM entry_artifacts ea2 + JOIN archived_entries e2 ON e2.id = ea2.entry_id + WHERE ea2.blob_id = ea.blob_id + AND (e2.archived_at < archived_entries.archived_at + OR (e2.archived_at = archived_entries.archived_at + AND e2.id < archived_entries.id)) + ) + ) + WHERE id IN ( + SELECT DISTINCT entry_id FROM entry_artifacts WHERE artifact_role = 'avatar' + );", + )?; } // Migration: add notes_json column to existing capture_jobs tables. @@ -1409,11 +1437,7 @@ pub fn finish_archive_run(conn: &Connection, run_id: i64) -> Result<()> { [run_id], |row| row.get(0), )?; - let status = if failed_count > 0 { - "failed" - } else { - "completed" - }; + let status = if failed_count > 0 { "failed" } else { "completed" }; conn.execute( "UPDATE archive_runs SET status = ?1, finished_at = ?2 WHERE id = ?3", params![status, now_timestamp(), run_id], @@ -1421,6 +1445,20 @@ pub fn finish_archive_run(conn: &Connection, run_id: i64) -> Result<()> { Ok(()) } +/// Returns the number of completed **child** archive_run_items for a run. +/// +/// Excludes the container item (parent_item_id IS NULL) so that a playlist +/// where every video fails still returns 0 even though the container item +/// itself was completed before child downloads started. +pub fn get_run_completed_child_count(conn: &Connection, run_id: i64) -> Result { + Ok(conn.query_row( + "SELECT COUNT(*) FROM archive_run_items + WHERE run_id = ?1 AND status = 'completed' AND parent_item_id IS NOT NULL", + [run_id], + |row| row.get(0), + )?) +} + pub fn fail_archive_run(conn: &Connection, run_id: i64, error_summary: &str) -> Result<()> { refresh_run_counters(conn, run_id)?; conn.execute( @@ -1482,6 +1520,7 @@ pub fn refresh_entry_cached_bytes(conn: &Connection, entry_id: i64) -> Result<() JOIN archived_entries e ON e.id = ea.entry_id WHERE ea.entry_id = ?1 AND ea.blob_id IS NOT NULL + AND ea.artifact_role != 'avatar' AND EXISTS ( SELECT 1 FROM entry_artifacts ea2 @@ -1518,6 +1557,7 @@ pub fn cascade_cached_bytes_after_delete(conn: &Connection, entry_id: i64) -> Re JOIN blobs b ON b.id = ea.blob_id WHERE ea.entry_id = archived_entries.id AND ea.blob_id IS NOT NULL + AND ea.artifact_role != 'avatar' AND EXISTS ( SELECT 1 FROM entry_artifacts ea3 @@ -1570,6 +1610,7 @@ fn cascade_cached_bytes_after_subtree_delete(conn: &Connection, subtree_ids: &[i JOIN blobs b ON b.id = ea.blob_id WHERE ea.entry_id = archived_entries.id AND ea.blob_id IS NOT NULL + AND ea.artifact_role != 'avatar' AND EXISTS ( SELECT 1 FROM entry_artifacts ea3 @@ -1627,9 +1668,14 @@ pub fn delete_entry(conn: &Connection, entry_uid: &str) -> Result { None => return Ok(false), }; - // Collect the full subtree while rows still exist. + // Collect the full subtree (entry itself + any descendants) while rows still exist. + // Must include the entry itself: for a child entry root_entry_id = ?1 returns nothing + // (no grandchildren), so without `id = ?1` the set would be empty and + // cascade_cached_bytes_after_subtree_delete would not recalculate shared-blob totals. let subtree_ids: Vec = { - let mut stmt = conn.prepare("SELECT id FROM archived_entries WHERE root_entry_id = ?1")?; + let mut stmt = conn.prepare( + "SELECT id FROM archived_entries WHERE id = ?1 OR root_entry_id = ?1", + )?; stmt.query_map([entry_id], |row| row.get(0))? .collect::>()? }; @@ -1638,12 +1684,15 @@ pub fn delete_entry(conn: &Connection, entry_uid: &str) -> Result { // shared blobs with any subtree member, excluding every subtree ID simultaneously. cascade_cached_bytes_after_subtree_delete(conn, &subtree_ids)?; - // Null the FK that has no ON DELETE action (covers root and all descendants). + // Null the FK that has no ON DELETE action. Must cover: + // - The entry itself (child entry: root_entry_id = playlist root, not self) + // - All descendants (root entry: children have root_entry_id = entry_id) conn.execute( "UPDATE archive_run_items SET produced_entry_id = NULL - WHERE produced_entry_id IN ( - SELECT id FROM archived_entries WHERE root_entry_id = ?1 - )", + WHERE produced_entry_id = ?1 + OR produced_entry_id IN ( + SELECT id FROM archived_entries WHERE root_entry_id = ?1 + )", [entry_id], )?; @@ -3896,4 +3945,38 @@ mod tests { "file must be protected because artifact.relpath references it directly" ); } + #[test] + fn completed_child_count_excludes_container() { + // Regression: get_run_completed_child_count must not count the root container + // item. Scenario: complete both the container item and one child item; total + // DB completed_count == 2, but completed_child_count must == 1. + let c = conn(); + let user_id = ensure_default_user(&c).unwrap(); + let run = create_archive_run(&c, user_id, 2).unwrap(); + + // Root item (parent_item_id IS NULL) — mirrors what record_container_entry does. + let root_item = create_archive_run_item( + &c, run.id, None, 0, "https://example.com/pl", None, "youtube", "playlist", + ).unwrap(); + // Child item (parent_item_id IS NOT NULL). + let child_item = create_archive_run_item( + &c, run.id, Some(root_item.id), 1, "https://example.com/v1", None, "youtube", "video", + ).unwrap(); + + // Complete both — marks archive_runs.completed_count = 2. + c.execute( + "UPDATE archive_run_items SET status = 'completed' WHERE id IN (?1, ?2)", + rusqlite::params![root_item.id, child_item.id], + ).unwrap(); + refresh_run_counters(&c, run.id).unwrap(); + + let total: i64 = c.query_row( + "SELECT completed_count FROM archive_runs WHERE id = ?1", [run.id], |r| r.get(0), + ).unwrap(); + assert_eq!(total, 2, "both items completed: DB counter must be 2"); + + let child_count = get_run_completed_child_count(&c, run.id).unwrap(); + assert_eq!(child_count, 1, "only the child item must be counted"); + } + } diff --git a/crates/archivr-core/src/downloader/ytdlp.rs b/crates/archivr-core/src/downloader/ytdlp.rs index 87d4b68..26c091d 100644 --- a/crates/archivr-core/src/downloader/ytdlp.rs +++ b/crates/archivr-core/src/downloader/ytdlp.rs @@ -6,10 +6,50 @@ use std::{ process::Command, }; use uuid::Uuid; +use serde_json; use crate::downloader::cookies::{domain_from_url, write_netscape_cookie_file}; use crate::hash::hash_file; +/// A single item in a flat playlist listing from `fetch_playlist_info`. +#[derive(Debug)] +pub struct PlaylistItem { + pub id: String, + pub url: String, + pub title: Option, + pub uploader: Option, +} + +/// Container metadata returned by `fetch_playlist_info`. +#[derive(Debug)] +pub struct PlaylistInfo { + pub playlist_id: String, + pub title: Option, + pub uploader: Option, + pub items: Vec, +} + +/// Per-item quality data returned by `probe_playlist_qualities`. +#[derive(Debug, serde::Serialize)] +pub struct PlaylistItemProbe { + pub id: String, + pub url: String, + pub title: Option, + /// Available video heights as strings (e.g. "1080p"), sorted highest-first. + /// Empty vec means audio-only (no video track). + pub qualities: Vec, + pub has_audio: bool, +} + +/// Full playlist probe result with per-item quality data. +#[derive(Debug, serde::Serialize)] +pub struct PlaylistProbeResult { + pub playlist_id: String, + pub title: Option, + pub uploader: Option, + pub items: Vec, +} + /// Returns the yt-dlp `-f` format selector for `quality`. /// /// - `"audio"` → prefers native Opus/WebM (most efficient), then native @@ -74,6 +114,23 @@ pub fn available_video_heights(json: &str) -> Vec { heights } +/// Extracts distinct video heights from a serde_json entry Value's `formats` array, +/// sorted highest-first. Audio-only formats (vcodec == "none") are excluded. +fn available_video_heights_from_value(entry: &serde_json::Value) -> Vec { + let Some(fmts) = entry.get("formats").and_then(|v| v.as_array()) else { + return vec![]; + }; + let mut heights: Vec = fmts + .iter() + .filter(|f| f.get("vcodec").and_then(|c| c.as_str()).unwrap_or("none") != "none") + .filter_map(|f| f.get("height").and_then(|h| h.as_u64()).map(|h| h as u32)) + .filter(|&h| h > 0) + .collect(); + heights.sort_unstable_by(|a, b| b.cmp(a)); + heights.dedup(); + heights +} + /// Returns true when the yt-dlp `--dump-json` response contains at least one /// format with a real audio codec (i.e. `acodec != "none"`). pub fn has_audio_track(json: &str) -> bool { @@ -241,6 +298,207 @@ pub fn fetch_metadata(path: &str, cookies: &HashMap) -> Option Option { + let is_abs = |s: &str| s.starts_with("http://") || s.starts_with("https://"); + if let Some(u) = entry.get("webpage_url").and_then(|v| v.as_str()).filter(|s| is_abs(s)) { + return Some(u.to_owned()); + } + if let Some(u) = entry.get("url").and_then(|v| v.as_str()).filter(|s| is_abs(s)) { + return Some(u.to_owned()); + } + // Bare-ID fallback keyed on the container's platform. + if container_url.contains("music.youtube.com") { + Some(format!("https://music.youtube.com/watch?v={id}")) + } else if container_url.contains("youtube.com") || container_url.contains("youtu.be") { + Some(format!("https://www.youtube.com/watch?v={id}")) + } else if container_url.contains("open.spotify.com") { + Some(format!("https://open.spotify.com/track/{id}")) + } else { + eprintln!("warn: skipping playlist item {id:?} — no absolute URL from yt-dlp"); + None + } +} + +/// Runs `yt-dlp -J --flat-playlist ` and parses the single-JSON result. +/// +/// `-J` / `--dump-single-json` returns one JSON object for the whole +/// container with reliable top-level `title` / `uploader` fields plus an +/// `entries` array of shallow per-item objects. +/// +/// Returns an error if yt-dlp fails, the output is not valid JSON, or +/// the root `_type` is not `"playlist"`. +pub fn fetch_playlist_info(url: &str, cookies: &HashMap) -> Result { + let ytdlp = std::env::var("ARCHIVR_YT_DLP").unwrap_or_else(|_| "yt-dlp".to_string()); + + let cookie_file: Option = if !cookies.is_empty() { + let domain = domain_from_url(url); + let p = std::env::temp_dir() + .join(format!("archivr-cookies-{}.txt", Uuid::new_v4().simple())); + write_netscape_cookie_file(cookies, &domain, &p) + .context("failed to write yt-dlp cookie file")?; + Some(p) + } else { + None + }; + + let mut cmd = std::process::Command::new(&ytdlp); + cmd.arg("-J").arg("--flat-playlist"); + if let Some(cf) = &cookie_file { + cmd.arg("--cookies").arg(cf); + } + cmd.arg(url); + + let out = cmd.output(); + if let Some(cf) = &cookie_file { + let _ = std::fs::remove_file(cf); + } + let out = out.with_context(|| format!("failed to spawn {ytdlp}"))?; + if !out.status.success() { + let stderr = String::from_utf8_lossy(&out.stderr); + bail!("yt-dlp -J --flat-playlist failed for {url}: {stderr}"); + } + + let json: serde_json::Value = serde_json::from_slice(&out.stdout) + .context("yt-dlp -J output is not valid JSON")?; + + let ty = json.get("_type").and_then(|v| v.as_str()).unwrap_or(""); + if ty != "playlist" { + bail!("yt-dlp output _type is {ty:?}, expected \"playlist\""); + } + + let playlist_id = json + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let title = json.get("title").and_then(|v| v.as_str()).map(str::to_owned); + let uploader = json.get("uploader").and_then(|v| v.as_str()).map(str::to_owned); + + let raw_entries = json + .get("entries") + .and_then(|v| v.as_array()) + .map(|a| a.as_slice()) + .unwrap_or(&[]); + + let mut items = Vec::with_capacity(raw_entries.len()); + for entry in raw_entries { + if entry.is_null() { + continue; // unavailable/private item in flat listing + } + let id = match entry.get("id").and_then(|v| v.as_str()) { + Some(s) => s.to_owned(), + None => continue, + }; + let item_url = match normalize_item_url(entry, &id, url) { + Some(u) => u, + None => continue, + }; + let item_title = entry.get("title").and_then(|v| v.as_str()).map(str::to_owned); + let item_uploader = entry.get("uploader").and_then(|v| v.as_str()).map(str::to_owned); + items.push(PlaylistItem { id, url: item_url, title: item_title, uploader: item_uploader }); + } + + Ok(PlaylistInfo { playlist_id, title, uploader, items }) +} + +/// Runs `yt-dlp -J ` (full metadata, NOT --flat-playlist) and returns +/// per-item quality data for every entry in the playlist. +/// +/// This makes one yt-dlp subprocess call that fetches full format data for +/// all videos — expensive for large playlists but gives accurate per-video +/// quality lists. Intended for pre-capture quality selection only. +pub fn probe_playlist_qualities( + url: &str, + cookies: &HashMap, +) -> Result { + let ytdlp = std::env::var("ARCHIVR_YT_DLP").unwrap_or_else(|_| "yt-dlp".to_string()); + + let cookie_file: Option = if !cookies.is_empty() { + let domain = domain_from_url(url); + let p = std::env::temp_dir() + .join(format!("archivr-cookies-{}.txt", Uuid::new_v4().simple())); + write_netscape_cookie_file(cookies, &domain, &p) + .context("failed to write yt-dlp cookie file")?; + Some(p) + } else { + None + }; + + let mut cmd = std::process::Command::new(&ytdlp); + cmd.arg("-J"); // full metadata — NOT --flat-playlist + if let Some(cf) = &cookie_file { + cmd.arg("--cookies").arg(cf); + } + cmd.arg(url); + + let out = cmd.output(); + if let Some(cf) = &cookie_file { + let _ = std::fs::remove_file(cf); + } + let out = out.with_context(|| format!("failed to spawn {ytdlp}"))?; + if !out.status.success() { + let stderr = String::from_utf8_lossy(&out.stderr); + bail!("yt-dlp -J failed for {url}: {stderr}"); + } + + let json: serde_json::Value = serde_json::from_slice(&out.stdout) + .context("yt-dlp -J output is not valid JSON")?; + + let ty = json.get("_type").and_then(|v| v.as_str()).unwrap_or(""); + if ty != "playlist" { + bail!("yt-dlp output _type is {ty:?}, expected \"playlist\""); + } + + let playlist_id = json.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let title = json.get("title").and_then(|v| v.as_str()).map(str::to_owned); + let uploader = json.get("uploader").and_then(|v| v.as_str()).map(str::to_owned); + + let raw_entries = json + .get("entries") + .and_then(|v| v.as_array()) + .map(|a| a.as_slice()) + .unwrap_or(&[]); + + let mut items = Vec::with_capacity(raw_entries.len()); + for entry in raw_entries { + if entry.is_null() { continue; } + let id = match entry.get("id").and_then(|v| v.as_str()) { + Some(s) => s.to_owned(), + None => continue, + }; + let item_url = match normalize_item_url(entry, &id, url) { + Some(u) => u, + None => continue, + }; + let item_title = entry.get("title").and_then(|v| v.as_str()).map(str::to_owned); + let heights = available_video_heights_from_value(entry); + let qualities: Vec = heights.iter().map(|h| format!("{h}p")).collect(); + let has_audio = entry + .get("formats").and_then(|v| v.as_array()) + .map(|fmts| fmts.iter().any(|f| { + f.get("acodec").and_then(|c| c.as_str()).unwrap_or("none") != "none" + })) + .unwrap_or(false); + items.push(PlaylistItemProbe { id, url: item_url, title: item_title, qualities, has_audio }); + } + + Ok(PlaylistProbeResult { playlist_id, title, uploader, items }) +} + #[cfg(test)] mod tests { use super::{available_video_heights, has_audio_track, quality_format}; diff --git a/crates/archivr-server/src/routes.rs b/crates/archivr-server/src/routes.rs index 9df6c52..3021e4b 100644 --- a/crates/archivr-server/src/routes.rs +++ b/crates/archivr-server/src/routes.rs @@ -241,6 +241,10 @@ pub fn app_with_state(state: AppState) -> Router { .patch(patch_entry_handler) .delete(delete_entry_handler), ) + .route( + "/api/archives/:archive_id/entries/:entry_uid/children", + get(list_entry_children), + ) .route( "/api/archives/:archive_id/entries/:entry_uid/artifacts/:artifact_index", get(serve_artifact), @@ -260,6 +264,10 @@ pub fn app_with_state(state: AppState) -> Router { "/api/archives/:archive_id/captures/probe", get(probe_handler), ) + .route( + "/api/archives/:archive_id/captures/probe-playlist", + post(probe_playlist_handler), + ) .route( "/api/archives/:archive_id/capture_jobs/:job_uid", get(get_capture_job_handler), @@ -404,6 +412,18 @@ async fn list_entries( Ok(Json(archive::list_root_entries(&conn, caller_bits)?)) } +async fn list_entry_children( + State(state): State, + auth: AuthUser, + Path((archive_id, entry_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 caller_bits = auth_to_caller_bits(&auth); + Ok(Json(archive::list_child_entries(&conn, &entry_uid, caller_bits)?)) +} + async fn search_entries_handler( State(state): State, auth: AuthUser, @@ -749,6 +769,13 @@ struct CaptureBody { modal_closer_enabled: Option, /// Route through Freedium mirror for WebPage captures. Absent = true (on by default). via_freedium: Option, + /// Per-video quality overrides for playlist captures. + /// Keys are yt-dlp video IDs; values are quality strings ("best", "1080p", "audio", etc.). + #[serde(default)] + per_item_quality: std::collections::HashMap, + /// When true, skip playlist items already archived under an existing container. + #[serde(default)] + sync: bool, } #[derive(Debug, serde::Deserialize)] @@ -756,6 +783,11 @@ struct ProbeQuery { locator: String, } +#[derive(Debug, serde::Deserialize)] +struct ProbePlaylistBody { + locator: String, +} + #[derive(Debug, serde::Deserialize)] struct LoginBody { username: String, @@ -826,6 +858,28 @@ async fn capture_handler( )); } } + { + let is_valid_quality = |q: &str| { + q == "best" + || q == "audio" + || q.strip_suffix('p') + .and_then(|n| n.parse::().ok()) + .is_some() + }; + if let Some(bad) = body.per_item_quality.values().find(|q| !is_valid_quality(q)) { + return Err(ApiError::bad_request(&format!( + "invalid per_item_quality value {bad:?}: must be \"best\", \"audio\", or a height string like \"1080p\"" + ))); + } + } + // per_item_quality semantics (enforced in capture.rs): + // - Absent or empty map: all playlist items are downloaded; quality is the + // global `quality` field applied as a yt-dlp cap with graceful fallback. + // - Non-empty map: ONLY items whose yt-dlp ID appears as a key are downloaded; + // absent IDs are skipped. This is how the frontend's delete-item button works. + // The "must choose quality for unsupported videos" invariant is enforced by the + // frontend before submission; a direct API caller bypassing the UI accepts + // yt-dlp's standard cap-and-fallback behavior for items it includes. let mounted = mounted_archive(&state, &archive_id)?; let archive_paths = archive::read_archive_paths(&mounted.archive_path).map_err(ApiError::from)?; @@ -863,6 +917,8 @@ async fn capture_handler( modal_closer_enabled: Some(effective_modal_closer), reader_mode: body.reader_mode.unwrap_or(false), via_freedium: body.via_freedium.unwrap_or(true), + per_item_quality: body.per_item_quality.clone(), + sync: body.sync, }; // Spawn background capture. @@ -902,10 +958,19 @@ async fn capture_handler( notes_str = serde_json::Value::Object(notes_map).to_string(); Some(¬es_str) }; + // A partial playlist (some items succeeded, some failed) has status="failed" + // but completed_child_count > 0. Treat it as a completed job so onCaptured + // fires and the archived entries appear. Only mark failed when no child + // succeeded (completed_child_count == 0). + let job_status = if result.status == "failed" && result.completed_child_count == 0 { + "failed" + } else { + "completed" + }; database::update_capture_job_status( &conn, &job_uid_bg, - "completed", + job_status, Some(&result.run_uid), None, notes, @@ -981,6 +1046,8 @@ async fn rearchive_handler( modal_closer_enabled: None, reader_mode: false, via_freedium: false, + per_item_quality: std::collections::HashMap::new(), + sync: false, }; let job_uid_bg = job_uid.clone(); @@ -1109,6 +1176,41 @@ async fn probe_handler( }))) } +async fn probe_playlist_handler( + State(state): State, + auth_user: AuthUser, + Path(archive_id): Path, + Json(body): Json, +) -> Result, ApiError> { + auth_user.require_role(ROLE_USER)?; + let locator = body.locator.trim().to_string(); + if locator.is_empty() { + return Err(ApiError::bad_request("locator must not be empty")); + } + // Validate it's a playlist/channel source and expand shorthands. + let canonical_url = capture::locator_to_playlist_url(&locator) + .ok_or_else(|| ApiError::bad_request("locator is not a YouTube playlist, channel, YTM playlist, or Spotify album/playlist"))?; + // Verify archive exists. + let _ = mounted_archive(&state, &archive_id)?; + // Resolve cookies. + let cookie_rules = match database::open_auth_db(&state.auth_db_path) { + Ok(conn) => database::list_cookie_rules(&conn).unwrap_or_default(), + Err(_) => vec![], + }; + let cookies = capture::resolve_cookies_for_url(&cookie_rules, &canonical_url); + // Shell out to yt-dlp in a blocking task. + let result = tokio::task::spawn_blocking(move || { + downloader::ytdlp::probe_playlist_qualities(&canonical_url, &cookies) + }) + .await + .map_err(|_| ApiError::internal("probe-playlist task panicked"))? + .map_err(|e| ApiError { + status: StatusCode::BAD_GATEWAY, + message: format!("playlist probe failed: {e:#}"), + })?; + Ok(Json(result)) +} + async fn auth_setup_status( State(state): State, ) -> Result, ApiError> { diff --git a/crates/archivr-server/static/assets/index-D1LSSXsT.js b/crates/archivr-server/static/assets/index-D1LSSXsT.js deleted file mode 100644 index 8439d82..0000000 --- a/crates/archivr-server/static/assets/index-D1LSSXsT.js +++ /dev/null @@ -1,47 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&r(a)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var hu={exports:{}},ts={},mu={exports:{}},G={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Ur=Symbol.for("react.element"),Hd=Symbol.for("react.portal"),Wd=Symbol.for("react.fragment"),Vd=Symbol.for("react.strict_mode"),Qd=Symbol.for("react.profiler"),Kd=Symbol.for("react.provider"),Xd=Symbol.for("react.context"),Jd=Symbol.for("react.forward_ref"),qd=Symbol.for("react.suspense"),Yd=Symbol.for("react.memo"),Gd=Symbol.for("react.lazy"),Ka=Symbol.iterator;function Zd(e){return e===null||typeof e!="object"?null:(e=Ka&&e[Ka]||e["@@iterator"],typeof e=="function"?e:null)}var gu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},vu=Object.assign,yu={};function Jn(e,t,n){this.props=e,this.context=t,this.refs=yu,this.updater=n||gu}Jn.prototype.isReactComponent={};Jn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Jn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function xu(){}xu.prototype=Jn.prototype;function qi(e,t,n){this.props=e,this.context=t,this.refs=yu,this.updater=n||gu}var Yi=qi.prototype=new xu;Yi.constructor=qi;vu(Yi,Jn.prototype);Yi.isPureReactComponent=!0;var Xa=Array.isArray,wu=Object.prototype.hasOwnProperty,Gi={current:null},ku={key:!0,ref:!0,__self:!0,__source:!0};function ju(e,t,n){var r,l={},i=null,a=null;if(t!=null)for(r in t.ref!==void 0&&(a=t.ref),t.key!==void 0&&(i=""+t.key),t)wu.call(t,r)&&!ku.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,A=R[q];if(0>>1;ql(Re,F))wel(je,Re)?(R[q]=je,R[we]=F,q=we):(R[q]=Re,R[de]=F,q=de);else if(wel(je,F))R[q]=je,R[we]=F,q=we;else break e}}return T}function l(R,T){var F=R.sortIndex-T.sortIndex;return F!==0?F:R.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var a=Date,u=a.now();e.unstable_now=function(){return a.now()-u}}var o=[],c=[],g=1,h=null,m=3,y=!1,w=!1,k=!1,j=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,d=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v(R){for(var T=n(c);T!==null;){if(T.callback===null)r(c);else if(T.startTime<=R)r(c),T.sortIndex=T.expirationTime,t(o,T);else break;T=n(c)}}function x(R){if(k=!1,v(R),!w)if(n(o)!==null)w=!0,ie(_);else{var T=n(c);T!==null&&ee(x,T.startTime-R)}}function _(R,T){w=!1,k&&(k=!1,p(S),S=-1),y=!0;var F=m;try{for(v(T),h=n(o);h!==null&&(!(h.expirationTime>T)||R&&!H());){var q=h.callback;if(typeof q=="function"){h.callback=null,m=h.priorityLevel;var A=q(h.expirationTime<=T);T=e.unstable_now(),typeof A=="function"?h.callback=A:h===n(o)&&r(o),v(T)}else r(o);h=n(o)}if(h!==null)var ge=!0;else{var de=n(c);de!==null&&ee(x,de.startTime-T),ge=!1}return ge}finally{h=null,m=F,y=!1}}var b=!1,C=null,S=-1,D=5,L=-1;function H(){return!(e.unstable_now()-LR||125q?(R.sortIndex=F,t(c,R),n(o)===null&&R===n(c)&&(k?(p(S),S=-1):k=!0,ee(x,F-q))):(R.sortIndex=A,t(o,R),w||y||(w=!0,ie(_))),R},e.unstable_shouldYield=H,e.unstable_wrapCallback=function(R){var T=m;return function(){var F=m;m=T;try{return R.apply(this,arguments)}finally{m=F}}}})(Eu);Cu.exports=Eu;var df=Cu.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var ff=f,et=df;function z(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ni=Object.prototype.hasOwnProperty,pf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,qa={},Ya={};function hf(e){return ni.call(Ya,e)?!0:ni.call(qa,e)?!1:pf.test(e)?Ya[e]=!0:(qa[e]=!0,!1)}function mf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function gf(e,t,n,r){if(t===null||typeof t>"u"||mf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function We(e,t,n,r,l,i,a){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=a}var ze={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ze[e]=new We(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ze[t]=new We(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ze[e]=new We(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ze[e]=new We(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ze[e]=new We(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ze[e]=new We(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ze[e]=new We(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ze[e]=new We(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ze[e]=new We(e,5,!1,e.toLowerCase(),null,!1,!1)});var ea=/[\-:]([a-z])/g;function ta(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ea,ta);ze[t]=new We(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ea,ta);ze[t]=new We(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ea,ta);ze[t]=new We(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ze[e]=new We(e,1,!1,e.toLowerCase(),null,!1,!1)});ze.xlinkHref=new We("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ze[e]=new We(e,1,!1,e.toLowerCase(),null,!0,!0)});function na(e,t,n,r){var l=ze.hasOwnProperty(t)?ze[t]:null;(l!==null?l.type!==0:r||!(2u||l[a]!==i[u]){var o=` -`+l[a].replace(" at new "," at ");return e.displayName&&o.includes("")&&(o=o.replace("",e.displayName)),o}while(1<=a&&0<=u);break}}}finally{Ts=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?dr(e):""}function vf(e){switch(e.tag){case 5:return dr(e.type);case 16:return dr("Lazy");case 13:return dr("Suspense");case 19:return dr("SuspenseList");case 0:case 2:case 15:return e=Ps(e.type,!1),e;case 11:return e=Ps(e.type.render,!1),e;case 1:return e=Ps(e.type,!0),e;default:return""}}function ii(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 Nn:return"Fragment";case Sn:return"Portal";case ri:return"Profiler";case ra:return"StrictMode";case li:return"Suspense";case si:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case bu:return(e.displayName||"Context")+".Consumer";case Pu:return(e._context.displayName||"Context")+".Provider";case la:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case sa:return t=e.displayName||null,t!==null?t:ii(e.type)||"Memo";case $t:t=e._payload,e=e._init;try{return ii(e(t))}catch{}}return null}function yf(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 ii(t);case 8:return t===ra?"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 Jt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function zu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function xf(e){var t=zu(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(a){r=""+a,i.call(this,a)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(a){r=""+a},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Zr(e){e._valueTracker||(e._valueTracker=xf(e))}function Ru(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=zu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Pl(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 ai(e,t){var n=t.checked;return me({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Za(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Jt(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 Du(e,t){t=t.checked,t!=null&&na(e,"checked",t,!1)}function oi(e,t){Du(e,t);var n=Jt(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")?ui(e,t.type,n):t.hasOwnProperty("defaultValue")&&ui(e,t.type,Jt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function eo(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 ui(e,t,n){(t!=="number"||Pl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var fr=Array.isArray;function $n(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=el.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Cr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var gr={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},wf=["Webkit","ms","Moz","O"];Object.keys(gr).forEach(function(e){wf.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),gr[t]=gr[e]})});function Fu(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||gr.hasOwnProperty(e)&&gr[e]?(""+t).trim():t+"px"}function Mu(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Fu(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var kf=me({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 fi(e,t){if(t){if(kf[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(z(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(z(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(z(61))}if(t.style!=null&&typeof t.style!="object")throw Error(z(62))}}function pi(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 hi=null;function ia(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var mi=null,In=null,On=null;function ro(e){if(e=Vr(e)){if(typeof mi!="function")throw Error(z(280));var t=e.stateNode;t&&(t=is(t),mi(e.stateNode,e.type,t))}}function Au(e){In?On?On.push(e):On=[e]:In=e}function Bu(){if(In){var e=In,t=On;if(On=In=null,ro(e),t)for(e=0;e>>=0,e===0?32:31-(zf(e)/Rf|0)|0}var tl=64,nl=4194304;function pr(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 Rl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,a=n&268435455;if(a!==0){var u=a&~l;u!==0?r=pr(u):(i&=a,i!==0&&(r=pr(i)))}else a=n&~l,a!==0?r=pr(a):i!==0&&(r=pr(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 Hr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-gt(t),e[t]=n}function Of(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=yr),po=" ",ho=!1;function ac(e,t){switch(e){case"keyup":return dp.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function oc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var _n=!1;function pp(e,t){switch(e){case"compositionend":return oc(t);case"keypress":return t.which!==32?null:(ho=!0,po);case"textInput":return e=t.data,e===po&&ho?null:e;default:return null}}function hp(e,t){if(_n)return e==="compositionend"||!ha&&ac(e,t)?(e=sc(),xl=da=Mt=null,_n=!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=yo(n)}}function fc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?fc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function pc(){for(var e=window,t=Pl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Pl(e.document)}return t}function ma(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 Sp(e){var t=pc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&fc(n.ownerDocument.documentElement,n)){if(r!==null&&ma(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=xo(n,i);var a=xo(n,r);l&&a&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==a.node||e.focusOffset!==a.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(a.node,a.offset)):(t.setEnd(a.node,a.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,Cn=null,ki=null,wr=null,ji=!1;function wo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ji||Cn==null||Cn!==Pl(r)||(r=Cn,"selectionStart"in r&&ma(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}),wr&&zr(wr,r)||(wr=r,r=Il(ki,"onSelect"),0Pn||(e.current=Ti[Pn],Ti[Pn]=null,Pn--)}function ae(e,t){Pn++,Ti[Pn]=e.current,e.current=t}var qt={},Fe=Gt(qt),Ke=Gt(!1),dn=qt;function Hn(e,t){var n=e.type.contextTypes;if(!n)return qt;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 Xe(e){return e=e.childContextTypes,e!=null}function Fl(){ue(Ke),ue(Fe)}function Eo(e,t,n){if(Fe.current!==qt)throw Error(z(168));ae(Fe,t),ae(Ke,n)}function jc(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(z(108,yf(e)||"Unknown",l));return me({},n,r)}function Ml(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||qt,dn=Fe.current,ae(Fe,e),ae(Ke,Ke.current),!0}function To(e,t,n){var r=e.stateNode;if(!r)throw Error(z(169));n?(e=jc(e,t,dn),r.__reactInternalMemoizedMergedChildContext=e,ue(Ke),ue(Fe),ae(Fe,e)):ue(Ke),ae(Ke,n)}var _t=null,as=!1,Hs=!1;function Sc(e){_t===null?_t=[e]:_t.push(e)}function $p(e){as=!0,Sc(e)}function Zt(){if(!Hs&&_t!==null){Hs=!0;var e=0,t=se;try{var n=_t;for(se=1;e>=a,l-=a,Ct=1<<32-gt(t)+l|n<S?(D=C,C=null):D=C.sibling;var L=m(p,C,v[S],x);if(L===null){C===null&&(C=D);break}e&&C&&L.alternate===null&&t(p,C),d=i(L,d,S),b===null?_=L:b.sibling=L,b=L,C=D}if(S===v.length)return n(p,C),fe&&nn(p,S),_;if(C===null){for(;SS?(D=C,C=null):D=C.sibling;var H=m(p,C,L.value,x);if(H===null){C===null&&(C=D);break}e&&C&&H.alternate===null&&t(p,C),d=i(H,d,S),b===null?_=H:b.sibling=H,b=H,C=D}if(L.done)return n(p,C),fe&&nn(p,S),_;if(C===null){for(;!L.done;S++,L=v.next())L=h(p,L.value,x),L!==null&&(d=i(L,d,S),b===null?_=L:b.sibling=L,b=L);return fe&&nn(p,S),_}for(C=r(p,C);!L.done;S++,L=v.next())L=y(C,p,S,L.value,x),L!==null&&(e&&L.alternate!==null&&C.delete(L.key===null?S:L.key),d=i(L,d,S),b===null?_=L:b.sibling=L,b=L);return e&&C.forEach(function(M){return t(p,M)}),fe&&nn(p,S),_}function j(p,d,v,x){if(typeof v=="object"&&v!==null&&v.type===Nn&&v.key===null&&(v=v.props.children),typeof v=="object"&&v!==null){switch(v.$$typeof){case Gr:e:{for(var _=v.key,b=d;b!==null;){if(b.key===_){if(_=v.type,_===Nn){if(b.tag===7){n(p,b.sibling),d=l(b,v.props.children),d.return=p,p=d;break e}}else if(b.elementType===_||typeof _=="object"&&_!==null&&_.$$typeof===$t&&Lo(_)===b.type){n(p,b.sibling),d=l(b,v.props),d.ref=ir(p,b,v),d.return=p,p=d;break e}n(p,b);break}else t(p,b);b=b.sibling}v.type===Nn?(d=cn(v.props.children,p.mode,x,v.key),d.return=p,p=d):(x=El(v.type,v.key,v.props,null,p.mode,x),x.ref=ir(p,d,v),x.return=p,p=x)}return a(p);case Sn:e:{for(b=v.key;d!==null;){if(d.key===b)if(d.tag===4&&d.stateNode.containerInfo===v.containerInfo&&d.stateNode.implementation===v.implementation){n(p,d.sibling),d=l(d,v.children||[]),d.return=p,p=d;break e}else{n(p,d);break}else t(p,d);d=d.sibling}d=Ys(v,p.mode,x),d.return=p,p=d}return a(p);case $t:return b=v._init,j(p,d,b(v._payload),x)}if(fr(v))return w(p,d,v,x);if(tr(v))return k(p,d,v,x);ul(p,v)}return typeof v=="string"&&v!==""||typeof v=="number"?(v=""+v,d!==null&&d.tag===6?(n(p,d.sibling),d=l(d,v),d.return=p,p=d):(n(p,d),d=qs(v,p.mode,x),d.return=p,p=d),a(p)):n(p,d)}return j}var Vn=Ec(!0),Tc=Ec(!1),Ul=Gt(null),Hl=null,zn=null,xa=null;function wa(){xa=zn=Hl=null}function ka(e){var t=Ul.current;ue(Ul),e._currentValue=t}function Li(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 Mn(e,t){Hl=e,xa=zn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Qe=!0),e.firstContext=null)}function ut(e){var t=e._currentValue;if(xa!==e)if(e={context:e,memoizedValue:t,next:null},zn===null){if(Hl===null)throw Error(z(308));zn=e,Hl.dependencies={lanes:0,firstContext:e}}else zn=zn.next=e;return t}var sn=null;function ja(e){sn===null?sn=[e]:sn.push(e)}function Pc(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,ja(t)):(n.next=l.next,l.next=n),t.interleaved=n,Lt(e,r)}function Lt(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 It=!1;function Sa(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function bc(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 Tt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Vt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Z&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Lt(e,n)}return l=r.interleaved,l===null?(t.next=t,ja(r)):(t.next=l.next,l.next=t),r.interleaved=t,Lt(e,n)}function kl(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,oa(e,n)}}function zo(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 a={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=a:i=i.next=a,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 Wl(e,t,n,r){var l=e.updateQueue;It=!1;var i=l.firstBaseUpdate,a=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var o=u,c=o.next;o.next=null,a===null?i=c:a.next=c,a=o;var g=e.alternate;g!==null&&(g=g.updateQueue,u=g.lastBaseUpdate,u!==a&&(u===null?g.firstBaseUpdate=c:u.next=c,g.lastBaseUpdate=o))}if(i!==null){var h=l.baseState;a=0,g=c=o=null,u=i;do{var m=u.lane,y=u.eventTime;if((r&m)===m){g!==null&&(g=g.next={eventTime:y,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var w=e,k=u;switch(m=t,y=n,k.tag){case 1:if(w=k.payload,typeof w=="function"){h=w.call(y,h,m);break e}h=w;break e;case 3:w.flags=w.flags&-65537|128;case 0:if(w=k.payload,m=typeof w=="function"?w.call(y,h,m):w,m==null)break e;h=me({},h,m);break e;case 2:It=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[u]:m.push(u))}else y={eventTime:y,lane:m,tag:u.tag,payload:u.payload,callback:u.callback,next:null},g===null?(c=g=y,o=h):g=g.next=y,a|=m;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;m=u,u=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(g===null&&(o=h),l.baseState=o,l.firstBaseUpdate=c,l.lastBaseUpdate=g,t=l.shared.interleaved,t!==null){l=t;do a|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);hn|=a,e.lanes=a,e.memoizedState=h}}function Ro(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Vs.transition;Vs.transition={};try{e(!1),t()}finally{se=n,Vs.transition=r}}function Kc(){return ct().memoizedState}function Mp(e,t,n){var r=Kt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Xc(e))Jc(t,n);else if(n=Pc(e,t,n,r),n!==null){var l=Ue();vt(n,e,r,l),qc(n,t,r)}}function Ap(e,t,n){var r=Kt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Xc(e))Jc(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var a=t.lastRenderedState,u=i(a,n);if(l.hasEagerState=!0,l.eagerState=u,yt(u,a)){var o=t.interleaved;o===null?(l.next=l,ja(t)):(l.next=o.next,o.next=l),t.interleaved=l;return}}catch{}finally{}n=Pc(e,t,l,r),n!==null&&(l=Ue(),vt(n,e,r,l),qc(n,t,r))}}function Xc(e){var t=e.alternate;return e===he||t!==null&&t===he}function Jc(e,t){kr=Ql=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function qc(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,oa(e,n)}}var Kl={readContext:ut,useCallback:$e,useContext:$e,useEffect:$e,useImperativeHandle:$e,useInsertionEffect:$e,useLayoutEffect:$e,useMemo:$e,useReducer:$e,useRef:$e,useState:$e,useDebugValue:$e,useDeferredValue:$e,useTransition:$e,useMutableSource:$e,useSyncExternalStore:$e,useId:$e,unstable_isNewReconciler:!1},Bp={readContext:ut,useCallback:function(e,t){return wt().memoizedState=[e,t===void 0?null:t],e},useContext:ut,useEffect:$o,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Sl(4194308,4,Uc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Sl(4194308,4,e,t)},useInsertionEffect:function(e,t){return Sl(4,2,e,t)},useMemo:function(e,t){var n=wt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=wt();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=Mp.bind(null,he,e),[r.memoizedState,e]},useRef:function(e){var t=wt();return e={current:e},t.memoizedState=e},useState:Do,useDebugValue:La,useDeferredValue:function(e){return wt().memoizedState=e},useTransition:function(){var e=Do(!1),t=e[0];return e=Fp.bind(null,e[1]),wt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=he,l=wt();if(fe){if(n===void 0)throw Error(z(407));n=n()}else{if(n=t(),Pe===null)throw Error(z(349));pn&30||Dc(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,$o(Ic.bind(null,r,i,e),[e]),r.flags|=2048,Ar(9,$c.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=wt(),t=Pe.identifierPrefix;if(fe){var n=Et,r=Ct;n=(r&~(1<<32-gt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Fr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=a.createElement(n,{is:r.is}):(e=a.createElement(n),n==="select"&&(a=e,r.multiple?a.multiple=!0:r.size&&(a.size=r.size))):e=a.createElementNS(e,n),e[kt]=t,e[$r]=r,id(e,t,!1,!1),t.stateNode=e;e:{switch(a=pi(n,r),n){case"dialog":oe("cancel",e),oe("close",e),l=r;break;case"iframe":case"object":case"embed":oe("load",e),l=r;break;case"video":case"audio":for(l=0;lXn&&(t.flags|=128,r=!0,ar(i,!1),t.lanes=4194304)}else{if(!r)if(e=Vl(a),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ar(i,!0),i.tail===null&&i.tailMode==="hidden"&&!a.alternate&&!fe)return Ie(t),null}else 2*xe()-i.renderingStartTime>Xn&&n!==1073741824&&(t.flags|=128,r=!0,ar(i,!1),t.lanes=4194304);i.isBackwards?(a.sibling=t.child,t.child=a):(n=i.last,n!==null?n.sibling=a:t.child=a,i.last=a)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=xe(),t.sibling=null,n=pe.current,ae(pe,r?n&1|2:n&1),t):(Ie(t),null);case 22:case 23:return Oa(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ye&1073741824&&(Ie(t),t.subtreeFlags&6&&(t.flags|=8192)):Ie(t),null;case 24:return null;case 25:return null}throw Error(z(156,t.tag))}function Jp(e,t){switch(va(t),t.tag){case 1:return Xe(t.type)&&Fl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Qn(),ue(Ke),ue(Fe),Ca(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return _a(t),null;case 13:if(ue(pe),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(z(340));Wn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ue(pe),null;case 4:return Qn(),null;case 10:return ka(t.type._context),null;case 22:case 23:return Oa(),null;case 24:return null;default:return null}}var dl=!1,Oe=!1,qp=typeof WeakSet=="function"?WeakSet:Set,O=null;function Rn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){ye(e,t,r)}else n.current=null}function Ai(e,t,n){try{n()}catch(r){ye(e,t,r)}}var Qo=!1;function Yp(e,t){if(Si=Dl,e=pc(),ma(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 a=0,u=-1,o=-1,c=0,g=0,h=e,m=null;t:for(;;){for(var y;h!==n||l!==0&&h.nodeType!==3||(u=a+l),h!==i||r!==0&&h.nodeType!==3||(o=a+r),h.nodeType===3&&(a+=h.nodeValue.length),(y=h.firstChild)!==null;)m=h,h=y;for(;;){if(h===e)break t;if(m===n&&++c===l&&(u=a),m===i&&++g===r&&(o=a),(y=h.nextSibling)!==null)break;h=m,m=h.parentNode}h=y}n=u===-1||o===-1?null:{start:u,end:o}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ni={focusedElem:e,selectionRange:n},Dl=!1,O=t;O!==null;)if(t=O,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,O=e;else for(;O!==null;){t=O;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var k=w.memoizedProps,j=w.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?k:pt(t.type,k),j);p.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var v=t.stateNode.containerInfo;v.nodeType===1?v.textContent="":v.nodeType===9&&v.documentElement&&v.removeChild(v.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(z(163))}}catch(x){ye(t,t.return,x)}if(e=t.sibling,e!==null){e.return=t.return,O=e;break}O=t.return}return w=Qo,Qo=!1,w}function jr(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&&Ai(t,n,i)}l=l.next}while(l!==r)}}function cs(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 ud(e){var t=e.alternate;t!==null&&(e.alternate=null,ud(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[kt],delete t[$r],delete t[Ei],delete t[Rp],delete t[Dp])),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 cd(e){return e.tag===5||e.tag===3||e.tag===4}function Ko(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||cd(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 Ui(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=Ol));else if(r!==4&&(e=e.child,e!==null))for(Ui(e,t,n),e=e.sibling;e!==null;)Ui(e,t,n),e=e.sibling}function Hi(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(Hi(e,t,n),e=e.sibling;e!==null;)Hi(e,t,n),e=e.sibling}var be=null,ht=!1;function Dt(e,t,n){for(n=n.child;n!==null;)dd(e,t,n),n=n.sibling}function dd(e,t,n){if(jt&&typeof jt.onCommitFiberUnmount=="function")try{jt.onCommitFiberUnmount(ns,n)}catch{}switch(n.tag){case 5:Oe||Rn(n,t);case 6:var r=be,l=ht;be=null,Dt(e,t,n),be=r,ht=l,be!==null&&(ht?(e=be,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):be.removeChild(n.stateNode));break;case 18:be!==null&&(ht?(e=be,n=n.stateNode,e.nodeType===8?Us(e.parentNode,n):e.nodeType===1&&Us(e,n),br(e)):Us(be,n.stateNode));break;case 4:r=be,l=ht,be=n.stateNode.containerInfo,ht=!0,Dt(e,t,n),be=r,ht=l;break;case 0:case 11:case 14:case 15:if(!Oe&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,a=i.destroy;i=i.tag,a!==void 0&&(i&2||i&4)&&Ai(n,t,a),l=l.next}while(l!==r)}Dt(e,t,n);break;case 1:if(!Oe&&(Rn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){ye(n,t,u)}Dt(e,t,n);break;case 21:Dt(e,t,n);break;case 22:n.mode&1?(Oe=(r=Oe)||n.memoizedState!==null,Dt(e,t,n),Oe=r):Dt(e,t,n);break;default:Dt(e,t,n)}}function Xo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new qp),t.forEach(function(r){var l=ih.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function ft(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=a),r&=~i}if(r=l,r=xe()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Zp(r/1960))-r,10e?16:e,At===null)var r=!1;else{if(e=At,At=null,ql=0,Z&6)throw Error(z(331));var l=Z;for(Z|=4,O=e.current;O!==null;){var i=O,a=i.child;if(O.flags&16){var u=i.deletions;if(u!==null){for(var o=0;oxe()-$a?un(e,0):Da|=n),Je(e,t)}function xd(e,t){t===0&&(e.mode&1?(t=nl,nl<<=1,!(nl&130023424)&&(nl=4194304)):t=1);var n=Ue();e=Lt(e,t),e!==null&&(Hr(e,t,n),Je(e,n))}function sh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),xd(e,n)}function ih(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(z(314))}r!==null&&r.delete(t),xd(e,n)}var wd;wd=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ke.current)Qe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return Qe=!1,Kp(e,t,n);Qe=!!(e.flags&131072)}else Qe=!1,fe&&t.flags&1048576&&Nc(t,Bl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Nl(e,t),e=t.pendingProps;var l=Hn(t,Fe.current);Mn(t,n),l=Ta(null,t,r,e,l,n);var i=Pa();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,Xe(r)?(i=!0,Ml(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Sa(t),l.updater=us,t.stateNode=l,l._reactInternals=t,Ri(t,r,e,n),t=Ii(null,t,r,!0,i,n)):(t.tag=0,fe&&i&&ga(t),Be(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Nl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=oh(r),e=pt(r,e),l){case 0:t=$i(null,t,r,e,n);break e;case 1:t=Ho(null,t,r,e,n);break e;case 11:t=Bo(null,t,r,e,n);break e;case 14:t=Uo(null,t,r,pt(r.type,e),n);break e}throw Error(z(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:pt(r,l),$i(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:pt(r,l),Ho(e,t,r,l,n);case 3:e:{if(rd(t),e===null)throw Error(z(387));r=t.pendingProps,i=t.memoizedState,l=i.element,bc(e,t),Wl(t,r,null,n);var a=t.memoizedState;if(r=a.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:a.cache,pendingSuspenseBoundaries:a.pendingSuspenseBoundaries,transitions:a.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=Kn(Error(z(423)),t),t=Wo(e,t,r,n,l);break e}else if(r!==l){l=Kn(Error(z(424)),t),t=Wo(e,t,r,n,l);break e}else for(Ge=Wt(t.stateNode.containerInfo.firstChild),Ze=t,fe=!0,mt=null,n=Tc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Wn(),r===l){t=zt(e,t,n);break e}Be(e,t,r,n)}t=t.child}return t;case 5:return Lc(t),e===null&&bi(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,a=l.children,_i(r,l)?a=null:i!==null&&_i(r,i)&&(t.flags|=32),nd(e,t),Be(e,t,a,n),t.child;case 6:return e===null&&bi(t),null;case 13:return ld(e,t,n);case 4:return Na(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Vn(t,null,r,n):Be(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:pt(r,l),Bo(e,t,r,l,n);case 7:return Be(e,t,t.pendingProps,n),t.child;case 8:return Be(e,t,t.pendingProps.children,n),t.child;case 12:return Be(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,a=l.value,ae(Ul,r._currentValue),r._currentValue=a,i!==null)if(yt(i.value,a)){if(i.children===l.children&&!Ke.current){t=zt(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){a=i.child;for(var o=u.firstContext;o!==null;){if(o.context===r){if(i.tag===1){o=Tt(-1,n&-n),o.tag=2;var c=i.updateQueue;if(c!==null){c=c.shared;var g=c.pending;g===null?o.next=o:(o.next=g.next,g.next=o),c.pending=o}}i.lanes|=n,o=i.alternate,o!==null&&(o.lanes|=n),Li(i.return,n,t),u.lanes|=n;break}o=o.next}}else if(i.tag===10)a=i.type===t.type?null:i.child;else if(i.tag===18){if(a=i.return,a===null)throw Error(z(341));a.lanes|=n,u=a.alternate,u!==null&&(u.lanes|=n),Li(a,n,t),a=i.sibling}else a=i.child;if(a!==null)a.return=i;else for(a=i;a!==null;){if(a===t){a=null;break}if(i=a.sibling,i!==null){i.return=a.return,a=i;break}a=a.return}i=a}Be(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Mn(t,n),l=ut(l),r=r(l),t.flags|=1,Be(e,t,r,n),t.child;case 14:return r=t.type,l=pt(r,t.pendingProps),l=pt(r.type,l),Uo(e,t,r,l,n);case 15:return ed(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:pt(r,l),Nl(e,t),t.tag=1,Xe(r)?(e=!0,Ml(t)):e=!1,Mn(t,n),Yc(t,r,l),Ri(t,r,l,n),Ii(null,t,r,!0,e,n);case 19:return sd(e,t,n);case 22:return td(e,t,n)}throw Error(z(156,t.tag))};function kd(e,t){return Xu(e,t)}function ah(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 at(e,t,n,r){return new ah(e,t,n,r)}function Ma(e){return e=e.prototype,!(!e||!e.isReactComponent)}function oh(e){if(typeof e=="function")return Ma(e)?1:0;if(e!=null){if(e=e.$$typeof,e===la)return 11;if(e===sa)return 14}return 2}function Xt(e,t){var n=e.alternate;return n===null?(n=at(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 El(e,t,n,r,l,i){var a=2;if(r=e,typeof e=="function")Ma(e)&&(a=1);else if(typeof e=="string")a=5;else e:switch(e){case Nn:return cn(n.children,l,i,t);case ra:a=8,l|=8;break;case ri:return e=at(12,n,t,l|2),e.elementType=ri,e.lanes=i,e;case li:return e=at(13,n,t,l),e.elementType=li,e.lanes=i,e;case si:return e=at(19,n,t,l),e.elementType=si,e.lanes=i,e;case Lu:return fs(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Pu:a=10;break e;case bu:a=9;break e;case la:a=11;break e;case sa:a=14;break e;case $t:a=16,r=null;break e}throw Error(z(130,e==null?e:typeof e,""))}return t=at(a,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function cn(e,t,n,r){return e=at(7,e,r,t),e.lanes=n,e}function fs(e,t,n,r){return e=at(22,e,r,t),e.elementType=Lu,e.lanes=n,e.stateNode={isHidden:!1},e}function qs(e,t,n){return e=at(6,e,null,t),e.lanes=n,e}function Ys(e,t,n){return t=at(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function uh(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=Ls(0),this.expirationTimes=Ls(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ls(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Aa(e,t,n,r,l,i,a,u,o){return e=new uh(e,t,n,u,o),t===1?(t=1,i===!0&&(t|=8)):t=0,i=at(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Sa(i),e}function ch(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(_d)}catch(e){console.error(e)}}_d(),_u.exports=tt;var mh=_u.exports,Cd,nu=mh;Cd=nu.createRoot,nu.hydrateRoot;async function Ee(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function gh(){return Ee("/api/archives")}async function vh(e){return Ee(`/api/archives/${e}/entries`)}async function yh(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),Ee(`/api/archives/${e}/entries/search?${r}`)}async function Xi(e,t){return Ee(`/api/archives/${e}/entries/${t}`)}function xh(e,t,n){return Promise.all(n.map(r=>Ee(`/api/archives/${e}/entries/${t}/artifacts/${r}`)))}async function wh(e){if(!e||e.length===0)return{};const t=await fetch("/api/util/resolve-tco",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});return t.ok?t.json():{}}async function kh(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:n??null})});if(!r.ok)throw new Error(await r.text())}async function hl(e,t){return Ee(`/api/archives/${e}/entries/${t}/tags`)}async function ru(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 jh(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 lu(e,t){const n=await fetch(`/api/archives/${e}/entries/${t}`,{method:"DELETE"});if(!n.ok)throw new Error(`Delete failed (${n.status})`)}async function Sh(e,t,n){const r=await fetch(`/api/archives/${e}/tags/${t}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n})});if(!r.ok)throw new Error(await r.text());return r.json()}async function Nh(e,t){const n=await fetch(`/api/archives/${e}/tags/${t}`,{method:"DELETE"});if(!n.ok)throw new Error(await n.text())}async function _h(e,t){const n=await fetch(`/api/archives/${e}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok)throw new Error(await n.text());return n.json()}async function Ch(e,t,n){const r=await fetch(`/api/archives/${e}/tags/${t}/move`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({parent_uid:n??null})});if(!r.ok)throw new Error(await r.text());return r.json()}async function Gs(e){return Ee(`/api/archives/${e}/runs`)}async function ml(e){return Ee(`/api/archives/${e}/tags`)}async function Eh(e,t,n=null,r=null){const l={locator:t};n&&n!=="best"&&(l.quality=n),r&&(typeof r.ublock_enabled=="boolean"&&(l.ublock_enabled=r.ublock_enabled),typeof r.reader_mode=="boolean"&&(l.reader_mode=r.reader_mode),typeof r.cookie_ext_enabled=="boolean"&&(l.cookie_ext_enabled=r.cookie_ext_enabled),typeof r.modal_closer_enabled=="boolean"&&(l.modal_closer_enabled=r.modal_closer_enabled),typeof r.via_freedium=="boolean"&&(l.via_freedium=r.via_freedium));const i=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!i.ok){const a=await i.json().catch(()=>({}));throw new Error(a.error||`HTTP ${i.status}`)}return i.json()}async function Th(e,t){return Ee(`/api/archives/${e}/captures/probe?locator=${encodeURIComponent(t)}`)}async function Ed(e,t){return Ee(`/api/archives/${e}/capture_jobs/${t}`)}async function Ph(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function bh(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 Lh(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 zh(){await fetch("/api/auth/logout",{method:"POST"})}async function Rh(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function Dh(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 $h(e){const t=await fetch("/api/auth/me",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok)throw new Error(await t.text())}async function Ih(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 Oh(){return Ee("/api/auth/tokens")}async function Fh(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 Mh(e){const t=await fetch(`/api/auth/tokens/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(await t.text())}async function Wa(){return Ee("/api/admin/instance-settings")}async function Tl(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 Ah(){return Ee("/api/admin/users")}async function Bh(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 Uh(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 Hh(){return Ee("/api/admin/roles")}async function Wh(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 Td(e){return Ee(`/api/archives/${e}/collections`)}async function Vh(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 Qh(e,t){return Ee(`/api/archives/${e}/collections/${t}`)}async function Pd(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 Kh(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 Xh(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 Jh(e,t){return Ee(`/api/archives/${e}/entries/${t}/collections`)}async function su(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 qh(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)}}async function Yh(e){return Ee(`/api/archives/${e}/blob-cleanup`)}async function Gh(e){const t=await fetch(`/api/archives/${e}/blob-cleanup`,{method:"DELETE"});if(!t.ok){const n=await t.json().catch(()=>({}));throw new Error(n.error??t.statusText)}return t.json()}async function Zh(e,t){const n=await fetch(`/api/archives/${e}/entries/${t}/rearchive`,{method:"POST"});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.message||`rearchive failed: ${n.status}`)}return n.json()}async function em(){return Ee("/api/admin/cookie-rules")}async function tm(e,t,n){const r=await fetch("/api/admin/cookie-rules",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url_pattern:e||null,pattern_kind:t,cookies_json:n})});if(!r.ok){const l=await r.json().catch(()=>({}));throw new Error(l.message||`HTTP ${r.status}`)}return r.json()}async function nm(e,t){const n=await fetch(`/api/admin/cookie-rules/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.message||`HTTP ${n.status}`)}}async function rm(e){const t=await fetch(`/api/admin/cookie-rules/${e}`,{method:"DELETE"});if(!t.ok){const n=await t.json().catch(()=>({}));throw new Error(n.message||`HTTP ${t.status}`)}}const lm=window.fetch;window.fetch=async(...e)=>{var n;const t=await lm(...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 sm({onLogin:e}){const[t,n]=f.useState(""),[r,l]=f.useState(""),[i,a]=f.useState(null),[u,o]=f.useState(!1);async function c(g){g.preventDefault(),a(null),o(!0);try{const h=await Lh(t,r);e(h)}catch(h){a(h.message)}finally{o(!1)}}return s.jsx("div",{className:"login-page",children:s.jsxs("div",{className:"login-card",children:[s.jsx("h1",{className:"login-brand",children:"Archivr"}),s.jsx("p",{className:"login-tagline",children:"Sign in to your archive"}),s.jsxs("form",{onSubmit:c,children:[s.jsxs("div",{className:"login-field",children:[s.jsx("label",{className:"login-label",htmlFor:"login-username",children:"Username"}),s.jsx("input",{className:"login-input",id:"login-username",type:"text",value:t,onChange:g=>n(g.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),s.jsxs("div",{className:"login-field",children:[s.jsx("label",{className:"login-label",htmlFor:"login-password",children:"Password"}),s.jsx("input",{className:"login-input",id:"login-password",type:"password",value:r,onChange:g=>l(g.target.value),required:!0,autoComplete:"current-password"})]}),i&&s.jsx("p",{className:"login-error",children:i}),s.jsx("button",{className:"login-submit",type:"submit",disabled:u,children:u?"Signing in…":"Sign in"})]})]})})}function im({onComplete:e}){const[t,n]=f.useState(""),[r,l]=f.useState(""),[i,a]=f.useState(""),[u,o]=f.useState(null),[c,g]=f.useState(!1);async function h(m){if(m.preventDefault(),r!==i){o("Passwords do not match");return}if(r.length<8){o("Password must be at least 8 characters");return}o(null),g(!0);try{await bh(t,r),e()}catch(y){o(y.message)}finally{g(!1)}}return s.jsx("div",{className:"setup-page",children:s.jsxs("div",{className:"setup-card",children:[s.jsx("h1",{className:"setup-brand",children:"Archivr"}),s.jsx("p",{className:"setup-tagline",children:"Create your owner account to get started."}),s.jsxs("form",{onSubmit:h,children:[s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-username",children:"Username"}),s.jsx("input",{className:"setup-input",id:"setup-username",type:"text",value:t,onChange:m=>n(m.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-password",children:"Password"}),s.jsx("input",{className:"setup-input",id:"setup-password",type:"password",value:r,onChange:m=>l(m.target.value),required:!0,autoComplete:"new-password"})]}),s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-confirm",children:"Confirm password"}),s.jsx("input",{className:"setup-input",id:"setup-confirm",type:"password",value:i,onChange:m=>a(m.target.value),required:!0,autoComplete:"new-password"})]}),u&&s.jsx("p",{className:"setup-error",children:u}),s.jsx("button",{className:"setup-submit",type:"submit",disabled:c,children:c?"Creating account…":"Create account"})]})]})})}function am({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:a,setCurrentUser:u}=f.useContext(vs)??{},[o,c]=f.useState(!1);async function g(){c(!0),await zh(),u(null),window.location.reload()}return s.jsxs("header",{className:"topbar",children:[s.jsx("div",{className:"brand",children:"Archivr"}),s.jsx("div",{className:"switcher",children:s.jsx("select",{"aria-label":"Select archive",value:t??"",onChange:h=>n(h.target.value),children:e.map(h=>s.jsx("option",{value:h.id,children:h.label},h.id))})}),s.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","tags","collections","runs","admin","settings"].map(h=>s.jsx("button",{className:`nav-link${r===h?" is-active":""}`,onClick:()=>l(h),children:h.charAt(0).toUpperCase()+h.slice(1)},h))}),s.jsx("button",{className:"capture-button",onClick:i,children:"Capture"}),a&&s.jsxs("div",{className:"user-menu",children:[s.jsx("span",{className:"user-name",children:a.display_name||a.username}),s.jsx("button",{className:"logout-btn",onClick:g,disabled:o,children:o?"Logging out…":"Log out"})]})]})}let Ji=1;function bd(e){const t=e.trim(),n=t.toLowerCase();for(const r of["yt:","youtube:"])if(n.startsWith(r)){const l=n.slice(r.length);return l.startsWith("video/")||l.startsWith("short/")||l.startsWith("shorts/")}if(n.startsWith("ytm:"))return!n.slice(4).startsWith("playlist/");for(const r of["x:","twitter:","tweet:"])if(n.startsWith(r))return n.slice(r.length).startsWith("media:");if(n.startsWith("spotify:"))return!1;if(n.startsWith("instagram:")||n.startsWith("facebook:")||n.startsWith("tiktok:")||n.startsWith("reddit:")||n.startsWith("snapchat:"))return!0;if(n.startsWith("http://")||n.startsWith("https://")){if(/^https?:\/\/music\.youtube\.com\/watch/.test(n)||/^https?:\/\/(?:www\.)?(?:youtu\.be\/[0-9A-Za-z_-]+|youtube\.com\/watch\?v=[0-9A-Za-z_-]+|youtube\.com\/shorts\/[0-9A-Za-z_-]+)/.test(t)||n.startsWith("https://x.com/")||n.startsWith("http://x.com/")||/^https?:\/\/(?:www\.)?instagram\.com\//.test(n)||/^https?:\/\/(?:www\.)?facebook\.com\//.test(n)||n.startsWith("https://fb.watch/")||n.startsWith("http://fb.watch/")||/^https?:\/\/(?:www\.)?tiktok\.com\//.test(n)||/^https?:\/\/(?:www\.)?reddit\.com\//.test(n)||n.startsWith("https://redd.it/")||n.startsWith("http://redd.it/")||/^https?:\/\/(?:www\.)?snapchat\.com\//.test(n))return!0;if(n.startsWith("https://open.spotify.com/")||n.startsWith("http://open.spotify.com/"))return!1}return!1}function ur(e=""){return{id:Ji++,locator:e,quality:"best",probeState:"idle",probeQualities:null,probeHasAudio:!1,status:"idle",error:null,jobUid:null,archiveId:null}}function om({open:e,archiveId:t,onClose:n,onCaptured:r,onToast:l,onJobStarted:i,onJobSettled:a,activeJobs:u=[]}){const o=f.useRef(null),c=f.useRef(!0),g=f.useRef(new Map),h=f.useRef(new Map),m=f.useRef(new Map),y=f.useRef(t);f.useEffect(()=>{y.current=t},[t]);const w=f.useRef(r),k=f.useRef(l);f.useEffect(()=>{w.current=r},[r]),f.useEffect(()=>{k.current=l},[l]);const j=f.useRef(i),p=f.useRef(a);f.useEffect(()=>{j.current=i},[i]),f.useEffect(()=>{p.current=a},[a]);const[d,v]=f.useState(()=>{try{const E=JSON.parse(sessionStorage.getItem("captureItems")||"null");if(Array.isArray(E)&&E.length>0){const U=E.filter(K=>!K.status||K.status==="idle");if(U.length>0)return U.forEach(K=>{K.id>=Ji&&(Ji=K.id+1)}),U}}catch{}return[ur()]});f.useEffect(()=>{sessionStorage.setItem("captureItems",JSON.stringify(d))},[d]);const[x,_]=f.useState(!1),[b,C]=f.useState(null),[S,D]=f.useState(null),[L,H]=f.useState(!0),[M,W]=f.useState(!0),[Y,ce]=f.useState(!0);f.useEffect(()=>{Wa().then(E=>{D(E),H(E.cookie_ext_enabled??!0),W(E.modal_closer_enabled??!0)}).catch(()=>D({}))},[]);const ie=b!==null?b:(S==null?void 0:S.ublock_enabled)??!0,[ee,R]=f.useState(!1);f.useEffect(()=>{["captureDialogLocator","captureDialogError","captureDialogBusy","captureDialogJobStatus","captureDialogJobUid"].forEach(E=>sessionStorage.removeItem(E)),u.forEach(E=>{E.jobUid&&!g.current.has(E.jobUid)&&T(E.id,E.jobUid,E.locator,E.archiveId,null)})},[]),f.useEffect(()=>{const E=o.current;if(!E)return;const U=()=>{h.current.forEach(K=>clearTimeout(K)),h.current.clear(),n()};return E.addEventListener("close",U),()=>E.removeEventListener("close",U)},[n]),f.useEffect(()=>{const E=o.current;E&&(e?(c.current||v([ur()]),c.current=!1,E.open||E.showModal()):(h.current.forEach(U=>clearTimeout(U)),h.current.clear(),E.open&&E.close()))},[e]),f.useEffect(()=>()=>{g.current.forEach(E=>clearInterval(E)),h.current.forEach(E=>clearTimeout(E))},[]);function T(E,U,K,V,X=null){if(g.current.has(U))return;const P=setInterval(async()=>{var B,re,le,rt;try{const Me=await Ed(V,U);if(Me.status==="completed"){clearInterval(g.current.get(U)),g.current.delete(U),await Promise.resolve((B=w.current)==null?void 0:B.call(w)),(re=p.current)==null||re.call(p,E);try{const ve=Me.notes_json?JSON.parse(Me.notes_json):null;if(ve!=null&&ve.ublock_skipped||ve!=null&&ve.cookie_ext_skipped){const qe=ve.ublock_skipped&&ve.cookie_ext_skipped?"Captured without ad-blocking or cookie-consent extension. Check ARCHIVR_UBLOCK_EXT and ARCHIVR_COOKIE_EXT in your server config.":ve.ublock_skipped?"Captured without ad-blocking. ARCHIVR_UBLOCK=true but ARCHIVR_UBLOCK_EXT is not set or the path is invalid.":"Captured without cookie-consent extension. ARCHIVR_COOKIE_EXT is not set or the path is invalid.";k.current(qe,K,"warning"),F(X,"warning",K)}else X||k.current(null,K,"success"),F(X,"archived")}catch{X||k.current(null,K,"success"),F(X,"archived")}}else if(Me.status==="failed"){clearInterval(g.current.get(U)),g.current.delete(U),(le=p.current)==null||le.call(p,E);const ve=Me.error_text||"Capture failed.";k.current(ve,K),F(X,"failed",K)}}catch(Me){clearInterval(g.current.get(U)),g.current.delete(U),(rt=p.current)==null||rt.call(p,E);const ve=Me.message||"Network error";k.current(ve,K),F(X,"failed",K)}},500);g.current.set(U,P)}function F(E,U,K=null){if(!E)return;const V=m.current.get(E);if(!V||(U==="archived"||U==="warning"?(V.archived++,U==="warning"&&(V.warnings++,K&&V.warningLocators.push(K))):(V.failed++,K&&V.failedLocators.push(K)),V.archived+V.failed0?`${X} archived (${P} with warnings)`:`${X} archived`;rt=B>0?`${qe}, ${B} failed`:qe}const Me=X===0?"error":B>0||P>0?"warning":"success",ve=[];re.length>0&&ve.push(`Failed: -${re.map(qe=>` ${qe}`).join(` -`)}`),le.length>0&&ve.push(`With warnings: -${le.map(qe=>` ${qe}`).join(` -`)}`);const en=ve.length>0?ve.join(` -`):null;k.current(en,null,Me,rt)}async function q(E,U,K){var B,re;const V=y.current,X=((B=crypto.randomUUID)==null?void 0:B.call(crypto))??`job-${Date.now()}-${Math.random()}`,P={ublock_enabled:ie,reader_mode:ee,cookie_ext_enabled:L,modal_closer_enabled:M,via_freedium:Y};try{const le=await Eh(V,E,U,P);(re=j.current)==null||re.call(j,{id:X,jobUid:le.job_uid,locator:E,archiveId:V}),T(X,le.job_uid,E,V,K)}catch(le){const rt=le.message||"Submission failed.";k.current(rt,E),F(K,"failed",E)}}function A(){var V,X;const E=d.filter(P=>P.locator.trim());if(E.length===0)return;const U=E.length>1?((V=crypto.randomUUID)==null?void 0:V.call(crypto))??`batch-${Date.now()}`:null;U&&m.current.set(U,{total:E.length,archived:0,warnings:0,failed:0,failedLocators:[],warningLocators:[]});const K=E.map(P=>P.quality||"best");v([ur()]),(X=o.current)==null||X.close(),E.forEach((P,B)=>q(P.locator.trim(),K[B],U))}function ge(){v(E=>[...E,ur()])}function de(E){clearTimeout(h.current.get(E)),h.current.delete(E),v(U=>{const K=U.filter(V=>V.id!==E);return K.length===0?[ur()]:K})}function Re(E,U){if(clearTimeout(h.current.get(E)),v(V=>V.map(X=>X.id===E?{...X,locator:U,probeState:"idle",probeQualities:null,probeHasAudio:!1,quality:"best"}:X)),!bd(U))return;const K=setTimeout(async()=>{h.current.delete(E),v(V=>V.map(X=>X.id===E?{...X,probeState:"probing"}:X));try{const V=await Th(y.current,U.trim());v(X=>X.map(P=>{if(P.id!==E||P.locator!==U)return P;const B=V.qualities??[],re=V.has_audio??!1,le=B.length===0&&re?"audio":"best";return{...P,probeState:"done",probeQualities:B,probeHasAudio:re,quality:le}}))}catch{v(V=>V.map(X=>X.id===E?{...X,probeState:"idle",probeQualities:null}:X))}},600);h.current.set(E,K)}function we(E,U){v(K=>K.map(V=>V.id===E?{...V,quality:U}:V))}const je=d.filter(E=>E.locator.trim()).length;return s.jsx("dialog",{ref:o,className:"capture-dialog",children:s.jsxs("div",{className:"capture-dialog-inner",children:[s.jsxs("div",{className:"capture-dialog-header",children:[s.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),s.jsx("button",{type:"button",className:"capture-dialog-close",onClick:()=>{var E;return(E=o.current)==null?void 0:E.close()},"aria-label":"Close",children:s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"3",y1:"3",x2:"13",y2:"13"}),s.jsx("line",{x1:"13",y1:"3",x2:"3",y2:"13"})]})})]}),s.jsx("div",{className:"capture-rows",children:d.map((E,U)=>s.jsx(um,{item:E,autoFocus:U===d.length-1,onLocatorChange:K=>Re(E.id,K),onQualityChange:K=>we(E.id,K),onRemove:()=>de(E.id),onSubmit:A},E.id))}),s.jsxs("button",{type:"button",className:"capture-add-row",onClick:ge,children:[s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"8",y1:"2",x2:"8",y2:"14"}),s.jsx("line",{x1:"2",y1:"8",x2:"14",y2:"8"})]}),"Add another"]}),s.jsxs("div",{className:"capture-advanced",children:[s.jsxs("button",{type:"button",className:"capture-advanced-toggle",onClick:()=>_(E=>!E),"aria-expanded":x,children:[s.jsx("svg",{className:`capture-chevron${x?" capture-chevron--open":""}`,viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:s.jsx("polyline",{points:"4 6 8 10 12 6"})}),"Advanced options"]}),x&&s.jsxs("div",{className:"capture-advanced-panel",children:[s.jsxs("label",{className:"capture-ext-row",children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"uBlock Origin Lite"}),s.jsx("span",{className:"capture-ext-desc",children:"Block ads during this capture"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":ie,className:`ext-toggle ext-toggle--sm${ie?" ext-toggle--on":""}`,onClick:()=>C(E=>E===null?!ie:!E),"aria-label":"Toggle uBlock for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Block cookie banners"}),s.jsx("span",{className:"capture-ext-desc",children:"Dismiss cookie consent banners during this capture"}),!(S!=null&&S.cookie_ext_available)&&s.jsxs("span",{className:"capture-ext-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_COOKIE_EXT"})]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":L,className:`ext-toggle ext-toggle--sm${L?" ext-toggle--on":""}`,onClick:()=>H(E=>!E),"aria-label":"Toggle cookie banner blocking for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Reader mode"}),s.jsx("span",{className:"capture-ext-desc",children:"Distil to article text via Readability (off by default)"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":ee,className:`ext-toggle ext-toggle--sm${ee?" ext-toggle--on":""}`,onClick:()=>R(E=>!E),"aria-label":"Toggle reader mode for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Close modals and dialogs"}),s.jsx("span",{className:"capture-ext-desc",children:"Auto-dismiss cookie banners and overlays during this capture"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":M,className:`ext-toggle ext-toggle--sm${M?" ext-toggle--on":""}`,onClick:()=>W(E=>!E),"aria-label":"Toggle modal closer for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Freedium mirror"}),s.jsx("span",{className:"capture-ext-desc",children:"Route paywalled articles through a Freedium mirror (Medium, NYT, WaPo, etc.)"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":Y,className:`ext-toggle ext-toggle--sm${Y?" ext-toggle--on":""}`,onClick:()=>ce(E=>!E),"aria-label":"Toggle Freedium mirror for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]})]})]}),s.jsxs("div",{className:"capture-actions",children:[s.jsx("button",{type:"button",className:"capture-submit",onClick:A,disabled:je===0,children:je>1?`Archive ${je}`:"Archive"}),s.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var E;return(E=o.current)==null?void 0:E.close()},children:"Cancel"})]})]})})}function um({item:e,autoFocus:t,onLocatorChange:n,onQualityChange:r,onRemove:l,onSubmit:i}){const a=f.useRef(null);f.useEffect(()=>{var o;t&&((o=a.current)==null||o.focus())},[t]);const u=(()=>{if(!bd(e.locator))return null;if(e.probeState==="probing")return s.jsx("span",{className:"capture-quality-probing","aria-label":"Checking available qualities",children:"…"});if(e.probeState==="done"){const o=e.probeQualities??[],c=e.probeHasAudio??!1;return o.length===0&&!c?s.jsx("span",{className:"capture-quality-hint",children:"No media detected"}):o.length===0&&c?s.jsx("select",{className:"capture-quality",value:"audio",onChange:g=>r(g.target.value),"aria-label":"Video quality",children:s.jsx("option",{value:"audio",children:"Audio only"})}):s.jsxs("select",{className:"capture-quality",value:e.quality||"best",onChange:g=>r(g.target.value),"aria-label":"Video quality",children:[s.jsx("option",{value:"best",children:"Best quality"}),o.map(g=>s.jsx("option",{value:g,children:g},g)),c&&s.jsx("option",{value:"audio",children:"Audio only"})]})}return null})();return s.jsxs("div",{className:"capture-row",children:[s.jsxs("div",{className:"capture-row-main",children:[s.jsx("input",{ref:a,className:"capture-input",type:"text",placeholder:"https://… · yt:ID · ytm:ID · tweet:ID · x:ID",value:e.locator,onChange:o=>n(o.target.value),onKeyDown:o=>{o.key==="Enter"&&i()},autoComplete:"off",spellCheck:!1}),u,s.jsx("button",{type:"button",className:"capture-row-action capture-row-remove",onClick:l,"aria-label":"Remove",children:s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"3",y1:"3",x2:"13",y2:"13"}),s.jsx("line",{x1:"13",y1:"3",x2:"3",y2:"13"})]})})]}),e.error&&s.jsx("p",{className:"capture-row-error",children:e.error})]})}function cm(){return s.jsxs("div",{className:"skeleton-row",children:[s.jsx("div",{className:"col-check","aria-hidden":"true"}),s.jsx("div",{className:"col-added",children:s.jsx("span",{className:"skeleton-cell",style:{width:108,height:13}})}),s.jsxs("div",{className:"col-title",style:{gap:"0.42em",display:"flex",alignItems:"center"},children:[s.jsx("span",{className:"skeleton-cell",style:{width:14,height:14,borderRadius:"50%",flexShrink:0}}),s.jsx("span",{className:"skeleton-cell",style:{width:"58%",height:13}})]}),s.jsx("div",{className:"col-type",children:s.jsx("span",{className:"skeleton-cell",style:{width:58,height:20,borderRadius:99}})}),s.jsx("div",{className:"col-size",children:s.jsx("span",{className:"skeleton-cell",style:{width:44,height:12}})}),s.jsx("div",{className:"col-url",children:s.jsx("span",{className:"skeleton-cell",style:{width:"65%",height:12}})})]})}function Zl(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&r").replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'")}function on(e){return dm(e)??""}function Ld(e){if(!e)return"";const t=new Date(e);if(isNaN(t))return e;const n=r=>String(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const iu={youtube:'',youtube_music:'',spotify:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function Va(e){return iu[e]??iu.other}function zd(e){return e.split("/").map(t=>t?t.split("-").map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join(" "):"").join("/")}function fm({entry:e,archiveId:t,isSelected:n,isMultiSelected:r,onRowClick:l}){const[i,a]=f.useState(!1),o=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!i?s.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>a(!0),style:{objectFit:"contain"}}):s.jsx("span",{dangerouslySetInnerHTML:{__html:Va(e.source_kind)}}),c=n||r;function g(h){h.stopPropagation(),l(e,{ctrlKey:!0,metaKey:!1,shiftKey:!1,preventDefault(){}})}return s.jsxs("div",{className:[n&&"is-selected",r&&"is-multi-selected"].filter(Boolean).join(" ")||void 0,tabIndex:0,"data-entry-uid":e.entry_uid,onMouseDown:h=>{h.shiftKey&&h.preventDefault()},onClick:h=>l(e,h),onKeyDown:h=>{h.key==="Enter"&&l(e,h)},children:[s.jsx("div",{className:"col-check",children:s.jsx("button",{type:"button",className:`row-checkbox${c?" is-checked":""}`,"aria-pressed":c,"aria-label":c?"Deselect entry":"Select entry",onClick:g,onKeyDown:h=>h.stopPropagation()})}),s.jsx("div",{className:"col-added",children:Ld(e.archived_at)}),s.jsxs("div",{className:"col-title",children:[s.jsx("span",{className:"source-icon",children:o}),s.jsx("span",{className:"entry-title",children:on(e.title)||on(e.entry_uid)})]}),s.jsx("div",{className:"col-type",children:s.jsx("span",{className:"type-pill",children:on(e.entity_kind)})}),s.jsxs("div",{className:"col-size",children:[s.jsx("span",{className:"size-total",children:Zl(e.total_artifact_bytes)}),e.cached_bytes>0&&e.total_artifact_bytes>0&&s.jsxs("span",{className:"size-cached-pct",title:`${Zl(e.cached_bytes)} already on disk from an earlier entry`,children:[Math.round(e.cached_bytes/e.total_artifact_bytes*100),"% cached"]})]}),s.jsx("div",{className:"url-cell col-url",children:on(e.original_url)})]})}function pm({entries:e,selectedUids:t,onRowClick:n,archiveId:r,pendingCaptures:l=[]}){return s.jsx("section",{id:"archive-view",className:"view is-active",children:s.jsxs("div",{className:"entry-table",children:[s.jsxs("div",{className:"entry-header-row",children:[s.jsx("div",{className:"col-check","aria-hidden":"true"}),s.jsx("div",{className:"col-added",children:"Added"}),s.jsx("div",{className:"col-title",children:"Title"}),s.jsx("div",{className:"col-type",children:"Type"}),s.jsx("div",{className:"col-size",children:"Size"}),s.jsx("div",{className:"col-url",children:"Original URL"})]}),s.jsxs("div",{id:"entries-body",children:[l.filter(i=>i.archiveId===r).reverse().map(i=>s.jsx(cm,{},i.id)),e.map(i=>s.jsx(fm,{entry:i,archiveId:r,isSelected:t.size===1&&t.has(i.entry_uid),isMultiSelected:t.size>=2&&t.has(i.entry_uid),onRowClick:n},i.entry_uid))]})]})})}function hm(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 mm({status:e}){const t=e==="completed"?"run-status--completed":e==="failed"?"run-status--failed":e==="in_progress"?"run-status--in-progress":"",n=e?e.replace(/_/g," "):"—";return s.jsx("span",{className:`run-status ${t}`,children:n})}function gm({runs:e}){const[t,n]=f.useState(null);function r(l){n(i=>i===l?null:l)}return s.jsx("section",{id:"runs-view",className:"view is-active",children:s.jsxs("table",{className:"entry-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Started"}),s.jsx("th",{children:"Status"}),s.jsx("th",{children:"Requested"}),s.jsx("th",{children:"Completed"}),s.jsx("th",{children:"Failed"})]})}),s.jsx("tbody",{children:e.length===0?s.jsx("tr",{children:s.jsx("td",{colSpan:5,style:{color:"var(--muted)",padding:"24px 16px",textAlign:"center"},children:"No runs yet."})}):e.map(l=>{const i=l.status==="failed"&&l.error_summary,a=t===l.run_uid;return[s.jsxs("tr",{className:i?"run-row run-row--failed":"run-row",onClick:i?()=>r(l.run_uid):void 0,title:i?a?"Click to hide error":"Click to view error":void 0,children:[s.jsx("td",{children:hm(l.started_at)}),s.jsxs("td",{children:[s.jsx(mm,{status:l.status}),i&&s.jsx("span",{className:"run-expand-hint","aria-hidden":"true",children:a?"▴":"▾"})]}),s.jsx("td",{children:l.requested_count??"—"}),s.jsx("td",{children:l.completed_count??"—"}),s.jsx("td",{children:l.failed_count??"—"})]},l.run_uid),i&&a&&s.jsx("tr",{className:"run-error-row",children:s.jsx("td",{colSpan:5,children:s.jsx("pre",{className:"run-error-detail",children:l.error_summary})})},`${l.run_uid}-detail`)]})})]})})}const vm=4;function ym({archives:e}){const{currentUser:t}=f.useContext(vs)??{},n=t&&(t.role_bits&vm)!==0,[r,l]=f.useState("users"),[i,a]=f.useState([]),[u,o]=f.useState([]),[c,g]=f.useState(!1),[h,m]=f.useState(null),[y,w]=f.useState(""),[k,j]=f.useState(""),[p,d]=f.useState(""),[v,x]=f.useState(null),[_,b]=f.useState(!1),[C,S]=f.useState(""),[D,L]=f.useState(""),[H,M]=f.useState(null),[W,Y]=f.useState(!1),ce=f.useCallback(async()=>{if(n){g(!0),m(null);try{const[T,F]=await Promise.all([Ah(),Hh()]);a(T),o(F)}catch(T){m(T.message)}finally{g(!1)}}},[n]);f.useEffect(()=>{ce()},[ce]);async function ie(T){const F=T.status==="active"?"disabled":"active";try{await Uh(T.user_uid,F),a(q=>q.map(A=>A.user_uid===T.user_uid?{...A,status:F}:A))}catch(q){m(q.message)}}async function ee(T){if(T.preventDefault(),!y.trim()||!k){x("Username and password required");return}b(!0),x(null);try{await Bh(y.trim(),k,p.trim()||void 0),w(""),j(""),d(""),await ce()}catch(F){x(F.message)}finally{b(!1)}}async function R(T){if(T.preventDefault(),!C.trim()||!D.trim()){M("Slug and name required");return}Y(!0),M(null);try{await Wh(C.trim(),D.trim()),S(""),L(""),await ce()}catch(F){M(F.message)}finally{Y(!1)}}return n?s.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[s.jsx("h1",{children:"Admin"}),s.jsxs("div",{className:"view-tabs",children:[s.jsx("button",{className:`view-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),s.jsx("button",{className:`view-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),s.jsx("button",{className:`view-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),h&&s.jsx("div",{className:"form-msg form-msg--err",children:h}),r==="users"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Users"}),c?s.jsx("p",{className:"muted",children:"Loading…"}):s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Username"}),s.jsx("th",{children:"Email"}),s.jsx("th",{children:"Roles"}),s.jsx("th",{children:"Status"}),s.jsx("th",{children:"Actions"})]})}),s.jsx("tbody",{children:i.map(T=>s.jsxs("tr",{className:T.status==="disabled"?"admin-row-disabled":"",children:[s.jsx("td",{children:T.username}),s.jsx("td",{className:"muted",children:T.email||"—"}),s.jsx("td",{children:T.role_slugs.join(", ")||"—"}),s.jsx("td",{children:s.jsx("span",{className:`status-badge status-${T.status}`,children:T.status})}),s.jsx("td",{children:s.jsx("button",{className:"admin-action-btn",onClick:()=>ie(T),children:T.status==="active"?"Ban":"Unban"})})]},T.user_uid))})]}),s.jsx("h3",{children:"Create User"}),s.jsxs("form",{className:"admin-form",onSubmit:ee,children:[s.jsx("input",{className:"admin-input",placeholder:"Username",value:y,onChange:T=>w(T.target.value),required:!0}),s.jsx("input",{className:"admin-input",type:"password",placeholder:"Password (min 8 chars)",value:k,onChange:T=>j(T.target.value),required:!0}),s.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:p,onChange:T=>d(T.target.value)}),v&&s.jsx("div",{className:"form-msg form-msg--err",children:v}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:_,children:_?"Creating…":"Create User"})]})]}),r==="roles"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Roles"}),c?s.jsx("p",{className:"muted",children:"Loading…"}):s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Slug"}),s.jsx("th",{children:"Name"}),s.jsx("th",{children:"Level"}),s.jsx("th",{children:"Bit"}),s.jsx("th",{children:"Built-in"})]})}),s.jsx("tbody",{children:u.map(T=>s.jsxs("tr",{children:[s.jsx("td",{children:s.jsx("code",{children:T.slug})}),s.jsx("td",{children:T.name}),s.jsx("td",{children:T.level}),s.jsx("td",{children:T.bit_position}),s.jsx("td",{children:T.is_builtin?"✓":""})]},T.role_uid))})]}),s.jsx("h3",{children:"Create Custom Role"}),s.jsxs("form",{className:"admin-form",onSubmit:R,children:[s.jsx("input",{className:"admin-input",placeholder:"Slug (e.g. moderator)",value:C,onChange:T=>S(T.target.value),required:!0}),s.jsx("input",{className:"admin-input",placeholder:"Display Name (e.g. Moderator)",value:D,onChange:T=>L(T.target.value),required:!0}),H&&s.jsx("div",{className:"form-msg form-msg--err",children:H}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:W,children:W?"Creating…":"Create Role"})]})]}),r==="archives"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Mounted Archives"}),s.jsx("div",{className:"admin-list",children:e.map(T=>s.jsxs("div",{className:"admin-archive",children:[s.jsx("strong",{children:T.label}),s.jsx("div",{className:"muted",children:T.archive_path})]},T.id))})]})]}):s.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[s.jsx("h1",{children:"Admin"}),s.jsx("p",{className:"muted",children:"You need admin privileges to access this panel."}),s.jsx("h2",{children:"Mounted Archives"}),s.jsx("div",{className:"admin-list",children:e.map(T=>s.jsxs("div",{className:"admin-archive",children:[s.jsx("strong",{children:T.label}),s.jsx("div",{className:"muted",children:T.archive_path})]},T.id))})]})}function Rd({node:e,onPick:t}){var n;return s.jsxs("li",{children:[s.jsx("button",{className:"tag-picker-node-btn",title:e.tag.full_path,onClick:()=>t(e.tag),children:e.tag.slug}),((n=e.children)==null?void 0:n.length)>0&&s.jsx("div",{className:"tag-children",children:s.jsx("ul",{className:"tag-tree-list",children:e.children.map(r=>s.jsx(Rd,{node:r,onPick:t},r.tag.tag_uid))})})]})}function au({title:e,tagNodes:t,excludeUid:n,onPick:r,onCancel:l}){f.useEffect(()=>{function u(o){o.key==="Escape"&&(o.preventDefault(),o.stopPropagation(),l())}return document.addEventListener("keydown",u),()=>document.removeEventListener("keydown",u)},[l]);function i(u){return u.filter(o=>o.tag.tag_uid!==n).map(o=>({...o,children:i(o.children)}))}const a=n?i(t):t;return s.jsx("div",{className:"tag-picker-backdrop",onClick:u=>{u.target===u.currentTarget&&l()},children:s.jsxs("div",{className:"tag-picker-modal",role:"dialog","aria-modal":"true",children:[s.jsxs("div",{className:"tag-picker-header",children:[s.jsx("span",{className:"tag-picker-title",children:e}),s.jsx("button",{className:"tag-picker-close",onClick:l,title:"Cancel","aria-label":"Cancel",children:"×"})]}),s.jsxs("div",{className:"tag-picker-body",children:[s.jsx("button",{className:"tag-picker-root-btn",onClick:()=>r(null),title:"Place at root level (no parent)",children:"↑ Root tag (no parent)"}),a.length>0?s.jsx("ul",{className:"tag-tree-list tag-picker-tree",children:a.map(u=>s.jsx(Rd,{node:u,onPick:r},u.tag.tag_uid))}):s.jsx("p",{className:"tag-picker-empty",children:"No other tags available."})]})]})})}function Dd({parentPath:e,archiveId:t,onDone:n,onCancel:r}){const[l,i]=f.useState(""),a=f.useRef(!1);async function u(){const o=l.trim();if(!o){r();return}const c=e?`${e}/${o}`:`/${o}`;try{await _h(t,c),n()}catch(g){alert(g.message||"Create failed"),r()}}return s.jsx("input",{className:"tag-rename-input",autoFocus:!0,placeholder:"tag name",value:l,onChange:o=>i(o.target.value),onKeyDown:o=>{o.key==="Enter"&&o.currentTarget.blur(),o.key==="Escape"&&(a.current=!0,o.currentTarget.blur())},onBlur:()=>{a.current?(a.current=!1,r()):u()}})}function $d({node:e,archiveId:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:u,humanizeTags:o,moveSelectMode:c,onMoveSourceSelect:g,pendingCreateParentUid:h,onCreateDone:m,onCreateCancel:y}){var H;const w=n===e.tag.full_path,[k,j]=f.useState(!1),[p,d]=f.useState(""),v=f.useRef(!1);function x(){if(k)return;if(c){g(e);return}const M=w?null:e.tag.full_path;r(M),l("archive")}function _(M){M.stopPropagation(),!c&&(d(e.tag.slug),j(!0))}async function b(){const M=p.trim();if(!M||M===e.tag.slug){j(!1);return}try{const W=await Sh(t,e.tag.tag_uid,M);i(e.tag.full_path,W.full_path),u()}catch{}finally{j(!1)}}async function C(M){var Y;M.stopPropagation();const W=((Y=e.children)==null?void 0:Y.length)>0?`Delete tag "${e.tag.full_path}" and all its child tags? This cannot be undone.`:`Delete tag "${e.tag.full_path}"? This cannot be undone.`;if(window.confirm(W))try{await Nh(t,e.tag.tag_uid),a(e.tag.full_path),u()}catch{}}const S={archiveId:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:u,humanizeTags:o,moveSelectMode:c,onMoveSourceSelect:g,pendingCreateParentUid:h,onCreateDone:m,onCreateCancel:y},D=h===e.tag.tag_uid,L=((H=e.children)==null?void 0:H.length)>0;return s.jsxs("li",{children:[s.jsxs("div",{className:`tag-node-row${c?" tag-node-row--move-select":""}`,children:[k?s.jsx("input",{className:"tag-rename-input",autoFocus:!0,value:p,onChange:M=>d(M.target.value),onKeyDown:M=>{M.key==="Enter"&&M.currentTarget.blur(),M.key==="Escape"&&(v.current=!0,M.currentTarget.blur())},onBlur:()=>{v.current?(v.current=!1,j(!1)):b()}}):s.jsxs("button",{className:`tag-node-btn${w?" is-active":""}${c?" tag-node-btn--move-select":""}`,title:c?`Select "${e.tag.full_path}" to move`:e.tag.full_path,onClick:x,onDoubleClick:c?void 0:_,children:[s.jsx("span",{className:"tag-node-label",children:o?e.tag.name:e.tag.slug}),s.jsx("span",{className:"tag-node-count",children:e.children.length===0?`(${e.entry_count})`:`(${e.entry_count}) (${e.subtree_count} Total)`}),!c&&s.jsx("svg",{className:"edit-icon",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",title:"Rename tag",onClick:M=>{M.stopPropagation(),_(M)},children:s.jsx("path",{d:"M11.5 2.5a1.5 1.5 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"})})]}),!k&&!c&&s.jsx("button",{className:"remove tag-node-delete",title:`Delete "${e.tag.full_path}"`,onClick:C,"aria-label":`Delete "${e.tag.full_path}"`,children:"×"})]}),(L||D)&&s.jsx("div",{className:"tag-children",children:s.jsxs("ul",{className:"tag-tree-list",children:[e.children.map(M=>s.jsx($d,{node:M,...S},M.tag.tag_uid)),D&&s.jsx("li",{children:s.jsx(Dd,{parentPath:e.tag.full_path,archiveId:t,onDone:m,onCancel:y})})]})})]})}function xm({archiveId:e,tagNodes:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:u,humanizeTags:o}){const[c,g]=f.useState(null),[h,m]=f.useState(null);function y(){g("select-source"),m(null)}function w(){g(null),m(null)}f.useEffect(()=>{if(c!=="select-source")return;function W(Y){Y.key==="Escape"&&(Y.preventDefault(),Y.stopPropagation(),w())}return document.addEventListener("keydown",W),()=>document.removeEventListener("keydown",W)},[c]);function k(W){m(W),g("select-dest")}async function j(W){const Y=h.tag.full_path,ce=h.tag.tag_uid,ie=(W==null?void 0:W.tag_uid)??null;w();try{const ee=await Ch(e,ce,ie);i(Y,ee.full_path),u()}catch(ee){alert(ee.message||"Move failed")}}const[p,d]=f.useState(null),[v,x]=f.useState(void 0);function _(){d("select-parent"),x(void 0)}function b(){d(null),x(void 0)}function C(W){x(W??null),d("input")}function S(){d(null),x(void 0),u()}const D=c==="select-source",L=p==="input"?v?v.tag_uid:"__root__":void 0,H={archiveId:e,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:u,humanizeTags:o,moveSelectMode:D,onMoveSourceSelect:k,pendingCreateParentUid:L,onCreateDone:S,onCreateCancel:b},M=L==="__root__";return s.jsxs("section",{id:"tags-view",className:"view is-active",children:[s.jsxs("div",{className:"tag-tree",children:[s.jsx("div",{className:"tag-tree-header",children:D?s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"tag-tree-title tag-tree-title--move",children:"Select a tag to move"}),s.jsx("button",{className:"tag-tree-action-btn tag-tree-action-btn--cancel",onClick:w,children:"Cancel"})]}):s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"tag-tree-title",children:"Tags"}),n&&s.jsxs("span",{className:"tag-tree-active",title:n,children:["Filtering: ",n]}),s.jsxs("div",{className:"tag-tree-actions",children:[s.jsx("button",{className:"tag-tree-action-btn",onClick:_,title:"Create a new tag",disabled:!!p||!!c,children:"+ New"}),s.jsx("button",{className:"tag-tree-action-btn",onClick:y,title:"Move a tag to a different parent",disabled:!!p||!!c||t.length===0,children:"Move"})]})]})}),t.length===0&&!M?s.jsx("p",{className:"muted",style:{padding:"8px 0"},children:"No tags yet."}):s.jsxs("ul",{className:"tag-tree-list",children:[t.map(W=>s.jsx($d,{node:W,...H},W.tag.tag_uid)),M&&s.jsx("li",{children:s.jsx(Dd,{parentPath:null,archiveId:e,onDone:S,onCancel:b})})]})]}),p==="select-parent"&&s.jsx(au,{title:"Create tag under…",tagNodes:t,excludeUid:null,onPick:C,onCancel:b}),c==="select-dest"&&h&&s.jsx(au,{title:`Move "${h.tag.slug}" under…`,tagNodes:t,excludeUid:h.tag.tag_uid,onPick:j,onCancel:w})]})}const mr=[{value:0,label:"Private"},{value:2,label:"Users only"},{value:3,label:"Public"}],wm=e=>{var t;return((t=mr.find(n=>n.value===e))==null?void 0:t.label)??String(e)};function km({archiveId:e}){const[t,n]=f.useState([]),[r,l]=f.useState(!1),[i,a]=f.useState(null),[u,o]=f.useState(null),[c,g]=f.useState(null),[h,m]=f.useState(!1),[y,w]=f.useState(null),[k,j]=f.useState(""),[p,d]=f.useState(""),[v,x]=f.useState(2),[_,b]=f.useState(!1),[C,S]=f.useState(null),[D,L]=f.useState(""),[H,M]=f.useState(2),[W,Y]=f.useState(!1),[ce,ie]=f.useState(null),[ee,R]=f.useState(!1),[T,F]=f.useState(""),q=f.useRef(null),A=t.find(P=>P.collection_uid===u)??null,ge=(A==null?void 0:A.slug)==="_default_",de=f.useCallback(async()=>{if(e){l(!0),a(null);try{const P=await Td(e);n(P)}catch(P){a(P.message)}finally{l(!1)}}},[e]),Re=f.useCallback(async P=>{if(!P){g(null);return}m(!0),w(null);try{const B=await Qh(e,P);g(B)}catch(B){w(B.message)}finally{m(!1)}},[e]);f.useEffect(()=>{de()},[de]),f.useEffect(()=>{Re(u)},[u,Re]),f.useEffect(()=>{ee&&q.current&&q.current.focus()},[ee]);async function we(P){P.preventDefault();const B=k.trim(),re=p.trim();if(!(!B||!re)){b(!0),S(null);try{const le=await Vh(e,B,re,v);j(""),d(""),x(2),await de(),o(le.collection_uid)}catch(le){S(le.message)}finally{b(!1)}}}async function je(){const P=T.trim();if(!P||!A){R(!1);return}try{await su(e,A.collection_uid,{name:P}),await de(),g(B=>B&&{...B,name:P})}catch(B){a(B.message)}finally{R(!1)}}async function E(P){if(A)try{await su(e,A.collection_uid,{default_visibility_bits:P}),await de(),g(B=>B&&{...B,default_visibility_bits:P})}catch(B){a(B.message)}}async function U(){if(A&&window.confirm(`Delete collection "${A.name}"? Entries will not be deleted.`))try{await qh(e,A.collection_uid),o(null),g(null),await de()}catch(P){a(P.message)}}async function K(P){P.preventDefault();const B=D.trim();if(!(!B||!A)){Y(!0),ie(null);try{await Pd(e,A.collection_uid,B,H),L(""),await Re(A.collection_uid)}catch(re){ie(re.message)}finally{Y(!1)}}}async function V(P){if(A)try{await Kh(e,A.collection_uid,P),await Re(A.collection_uid)}catch(B){w(B.message)}}async function X(P,B){if(A)try{await Xh(e,A.collection_uid,P,B),g(re=>re&&{...re,entries:re.entries.map(le=>le.entry_uid===P?{...le,collection_visibility_bits:B}:le)})}catch(re){w(re.message)}}return e?s.jsxs("div",{className:"collections-view",children:[s.jsx("h2",{className:"collections-heading",children:"Collections"}),r&&s.jsx("div",{className:"muted",children:"Loading…"}),i&&s.jsxs("div",{className:"collections-error",children:[i," ",s.jsx("button",{onClick:()=>a(null),className:"coll-dismiss",children:"×"})]}),s.jsxs("div",{className:"collections-layout",children:[s.jsxs("div",{className:"collections-sidebar",children:[t.map(P=>s.jsxs("button",{className:`coll-sidebar-row${u===P.collection_uid?" is-active":""}`,onClick:()=>o(P.collection_uid),children:[s.jsx("span",{className:"coll-row-name",children:P.name}),s.jsx("span",{className:"coll-row-meta",children:wm(P.default_visibility_bits)})]},P.collection_uid)),t.length===0&&!r&&s.jsx("div",{className:"muted",style:{padding:"8px 12px"},children:"No collections yet."})]}),A?s.jsxs("div",{className:"coll-detail",children:[s.jsxs("div",{className:"coll-detail-header",children:[ee?s.jsx("input",{ref:q,className:"coll-rename-input",value:T,onChange:P=>F(P.target.value),onBlur:je,onKeyDown:P=>{P.key==="Enter"&&je(),P.key==="Escape"&&R(!1)}}):s.jsxs("h3",{className:`coll-detail-name${ge?"":" coll-detail-name--editable"}`,title:ge?void 0:"Click to rename",onClick:()=>{ge||(F(A.name),R(!0))},children:[(c==null?void 0:c.name)??A.name,!ge&&s.jsx("span",{className:"coll-edit-hint",children:" ✎"})]}),!ge&&s.jsx("button",{className:"coll-delete-btn",onClick:U,title:"Delete collection",children:"Delete"})]}),s.jsxs("div",{className:"coll-detail-vis",children:[s.jsx("span",{className:"coll-vis-label",children:"Default visibility"}),s.jsx("select",{className:"coll-vis-select",value:(c==null?void 0:c.default_visibility_bits)??A.default_visibility_bits,onChange:P=>E(Number(P.target.value)),disabled:ge,children:mr.map(P=>s.jsx("option",{value:P.value,children:P.label},P.value))})]}),s.jsxs("div",{className:"coll-entries-section",children:[s.jsx("div",{className:"coll-section-heading",children:"Entries"}),h&&s.jsx("div",{className:"muted",children:"Loading…"}),y&&s.jsx("div",{className:"collections-error",children:y}),!h&&c&&(c.entries.length===0?s.jsx("div",{className:"muted",children:"No entries in this collection."}):s.jsx("ul",{className:"coll-entries-list",children:c.entries.map(P=>s.jsxs("li",{className:"coll-entry-row",children:[s.jsxs("div",{className:"coll-entry-info",children:[s.jsx("span",{className:"coll-entry-title",children:P.title||P.entry_uid}),s.jsx("span",{className:"coll-entry-kind muted",children:P.source_kind})]}),s.jsxs("div",{className:"coll-entry-actions",children:[s.jsx("select",{className:"coll-entry-vis-select",value:P.collection_visibility_bits,onChange:B=>X(P.entry_uid,Number(B.target.value)),children:mr.map(B=>s.jsx("option",{value:B.value,children:B.label},B.value))}),!ge&&s.jsx("button",{className:"coll-entry-remove",onClick:()=>V(P.entry_uid),title:"Remove from collection",children:"×"})]})]},P.entry_uid))}))]}),!ge&&s.jsxs("form",{className:"coll-add-entry-form",onSubmit:K,children:[s.jsx("div",{className:"coll-section-heading",children:"Add entry"}),s.jsxs("div",{className:"coll-add-entry-row",children:[s.jsx("input",{className:"coll-add-entry-input",type:"text",value:D,onChange:P=>L(P.target.value),placeholder:"entry_uid",required:!0}),s.jsx("select",{className:"coll-vis-select",value:H,onChange:P=>M(Number(P.target.value)),children:mr.map(P=>s.jsx("option",{value:P.value,children:P.label},P.value))}),s.jsx("button",{className:"coll-add-btn",type:"submit",disabled:W,children:W?"…":"Add"})]}),ce&&s.jsx("div",{className:"collections-error",style:{marginTop:4},children:ce})]})]}):s.jsx("div",{className:"coll-detail coll-detail--empty",children:s.jsx("div",{className:"muted",children:"Select a collection to view details."})})]}),s.jsxs("details",{className:"coll-create-details",children:[s.jsx("summary",{children:"+ Create collection"}),s.jsxs("form",{className:"coll-create-form",onSubmit:we,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-name",children:"Name"}),s.jsx("input",{className:"capture-input",id:"coll-name",type:"text",value:k,onChange:P=>{j(P.target.value),p||d(P.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-slug",children:"Slug"}),s.jsx("input",{className:"capture-input",id:"coll-slug",type:"text",value:p,onChange:P=>d(P.target.value),placeholder:"my-collection",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-vis",children:"Default visibility"}),s.jsx("select",{className:"capture-input",id:"coll-vis",style:{height:42},value:v,onChange:P=>x(Number(P.target.value)),children:mr.map(P=>s.jsx("option",{value:P.value,children:P.label},P.value))})]}),C&&s.jsx("div",{className:"collections-error",children:C}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:_,children:_?"Creating…":"Create collection"})]})]})]}):s.jsx("div",{className:"view-placeholder",children:"Select an archive."})}const jm=4;function Sm({tab:e,onTabChange:t,archiveId:n}){const{currentUser:r,setCurrentUser:l}=f.useContext(vs)??{},i=r&&(r.role_bits&jm)!==0,a=["profile","tokens",...i?["instance","cookies","extensions","storage"]:[]],u={profile:"Profile",tokens:"API Tokens",instance:"Instance",cookies:"Cookies",extensions:"Extensions",storage:"Storage"};return s.jsxs("section",{className:"admin-view",children:[s.jsx("h1",{children:"Settings"}),s.jsx("div",{className:"view-tabs",children:a.map(o=>s.jsx("button",{className:`view-tab${e===o?" is-active":""}`,onClick:()=>t(o),children:u[o]},o))}),e==="profile"&&s.jsx(Nm,{currentUser:r,setCurrentUser:l}),e==="tokens"&&s.jsx(_m,{}),e==="instance"&&i&&s.jsx(Cm,{}),e==="cookies"&&i&&s.jsx(Tm,{}),e==="extensions"&&i&&s.jsx(Pm,{}),e==="storage"&&i&&s.jsx(Em,{archiveId:n})]})}function Nm({currentUser:e,setCurrentUser:t}){const[n,r]=f.useState((e==null?void 0:e.display_name)??""),[l,i]=f.useState(!1),[a,u]=f.useState(null),[o,c]=f.useState(""),[g,h]=f.useState(""),[m,y]=f.useState(""),[w,k]=f.useState(!1),[j,p]=f.useState(null);async function d(x){x.preventDefault(),i(!0),u(null);try{await Dh(n),t(_=>({..._,display_name:n||null})),u({ok:!0,text:"Saved."})}catch(_){u({ok:!1,text:_.message})}finally{i(!1)}}async function v(x){if(x.preventDefault(),g!==m){p({ok:!1,text:"Passwords do not match."});return}k(!0),p(null);try{await Ih(o,g),c(""),h(""),y(""),p({ok:!0,text:"Password changed."})}catch(_){p({ok:!1,text:_.message})}finally{k(!1)}}return s.jsxs("div",{style:{maxWidth:440},children:[s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Display Name"}),s.jsxs("form",{onSubmit:d,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"display-name",children:"Name shown in the UI"}),s.jsx("input",{className:"field-input",id:"display-name",placeholder:(e==null?void 0:e.username)??"",value:n,onChange:x=>r(x.target.value)})]}),a&&s.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:l,children:l?"Saving…":"Save"})]})]}),s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Display Preferences"}),s.jsxs("label",{className:"checkbox-row",children:[s.jsx("input",{type:"checkbox",checked:(e==null?void 0:e.humanize_slugs)??!1,onChange:async x=>{const _=x.target.checked;try{await $h({humanize_slugs:_}),t(b=>({...b,humanize_slugs:_}))}catch{}}}),s.jsx("span",{className:"form-label",style:{margin:0},children:"Humanize tag display"})]}),s.jsx("p",{className:"muted",style:{fontSize:13,margin:"4px 0 0"},children:'When on, tag paths show as "X / Articles" instead of "x/articles".'})]}),s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Change Password"}),s.jsxs("form",{onSubmit:v,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"cur-pw",children:"Current password"}),s.jsx("input",{className:"field-input",id:"cur-pw",type:"password",value:o,onChange:x=>c(x.target.value),required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"new-pw",children:"New password"}),s.jsx("input",{className:"field-input",id:"new-pw",type:"password",value:g,onChange:x=>h(x.target.value),required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"confirm-pw",children:"Confirm new password"}),s.jsx("input",{className:"field-input",id:"confirm-pw",type:"password",value:m,onChange:x=>y(x.target.value),required:!0})]}),j&&s.jsx("div",{className:`form-msg form-msg--${j.ok?"ok":"err"}`,children:j.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:w,children:w?"Changing…":"Change Password"})]})]})]})}function _m(){const[e,t]=f.useState([]),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,u]=f.useState(""),[o,c]=f.useState(!1),[g,h]=f.useState(null),m=f.useCallback(async()=>{r(!0),i(null);try{t(await Oh())}catch(k){i(k.message)}finally{r(!1)}},[]);f.useEffect(()=>{m()},[m]);async function y(k){if(k.preventDefault(),!!a.trim()){c(!0);try{const j=await Fh(a.trim());h(j),u(""),m()}catch(j){i(j.message)}finally{c(!1)}}}async function w(k){try{await Mh(k),t(j=>j.filter(p=>p.token_uid!==k))}catch(j){i(j.message)}}return s.jsx("div",{style:{maxWidth:600},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"API Tokens"}),g&&s.jsxs("div",{className:"token-banner",children:[s.jsx("strong",{children:"Token created."})," Copy it now — it won't be shown again.",s.jsx("code",{children:g.raw_token}),s.jsx("button",{className:"token-dismiss",onClick:()=>h(null),children:"Dismiss"})]}),s.jsxs("form",{className:"token-create-row",onSubmit:y,children:[s.jsx("input",{className:"field-input field-input--flex",placeholder:"Token name",value:a,onChange:k=>u(k.target.value),required:!0}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:o,children:o?"Creating…":"Create token"})]}),l&&s.jsx("div",{className:"form-msg form-msg--err",children:l}),n?s.jsx("div",{className:"muted",children:"Loading…"}):s.jsxs("div",{children:[e.length===0&&s.jsx("div",{className:"muted",children:"No tokens yet."}),e.map(k=>s.jsxs("div",{className:"token-row",children:[s.jsxs("div",{className:"token-row-info",children:[s.jsx("strong",{children:k.name}),s.jsxs("div",{className:"muted",children:["Created ",k.created_at.slice(0,10),k.last_used_at&&` · Last used ${k.last_used_at.slice(0,10)}`]})]}),s.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"4px 10px"},onClick:()=>w(k.token_uid),children:"Revoke"})]},k.token_uid))]})]})})}function Cm(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,u]=f.useState(!1),[o,c]=f.useState(null);f.useEffect(()=>{(async()=>{try{t(await Wa())}catch(h){i(h.message)}finally{r(!1)}})()},[]);async function g(h){h.preventDefault(),u(!0),c(null);try{await Tl(e),c({ok:!0,text:"Saved."})}catch(m){c({ok:!1,text:m.message})}finally{u(!1)}}return n?s.jsx("div",{className:"muted",children:"Loading…"}):l?s.jsx("div",{className:"form-msg form-msg--err",children:l}):e?s.jsx("div",{style:{maxWidth:440},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Instance Settings"}),s.jsxs("form",{onSubmit:g,children:[[["public_index_enabled","Public index (unauthenticated browsing)"],["public_entry_content_enabled","Public entry content"],["open_registration_enabled","Open registration"]].map(([h,m])=>s.jsxs("label",{className:"checkbox-row",children:[s.jsx("input",{type:"checkbox",checked:!!e[h],onChange:y=>t(w=>({...w,[h]:y.target.checked}))}),m]},h)),s.jsxs("div",{className:"form-field",style:{marginTop:4},children:[s.jsx("label",{className:"form-label",children:"Default entry visibility"}),s.jsxs("select",{className:"field-input",value:e.default_entry_visibility,onChange:h=>t(m=>({...m,default_entry_visibility:Number(h.target.value)})),children:[s.jsx("option",{value:0,children:"Private"}),s.jsx("option",{value:2,children:"Unlisted"}),s.jsx("option",{value:3,children:"Public"})]})]}),o&&s.jsx("div",{className:`form-msg form-msg--${o.ok?"ok":"err"}`,children:o.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:a,children:a?"Saving…":"Save Settings"})]})]})}):null}function Zs(e){if(e===0)return"0 B";const t=["B","KB","MB","GB","TB"],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/Math.pow(1024,n)).toFixed(n===0?0:1)} ${t[n]}`}function Em({archiveId:e}){const[t,n]=f.useState("idle"),[r,l]=f.useState(null),[i,a]=f.useState(null),[u,o]=f.useState(null);function c(){n("idle"),l(null),a(null),o(null)}async function g(){n("scanning"),o(null),l(null);try{const y=await Yh(e);l(y),n("scanned")}catch(y){o(y.message),n("error")}}async function h(){n("deleting"),o(null);try{const y=await Gh(e);a(y),n("done")}catch(y){o(y.message),n("error")}}const m=r&&r.deletable_files===0&&r.orphaned_blob_rows===0;return s.jsx("div",{style:{maxWidth:440},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Orphan Cleanup"}),s.jsxs("p",{className:"muted",style:{marginBottom:16},children:["Scan for blob files and database records that are no longer referenced by any archive entry and safely delete them."," ",s.jsx("strong",{children:"Cleanup is blocked while captures are running."})]}),!e&&s.jsx("div",{className:"muted",children:"No archive selected."}),e&&t==="idle"&&s.jsx("button",{className:"btn-ghost",onClick:g,children:"Scan for orphaned blobs"}),t==="scanning"&&s.jsx("div",{className:"muted",children:"Scanning…"}),t==="scanned"&&r&&m&&s.jsxs(s.Fragment,{children:[s.jsx("div",{className:"form-msg form-msg--ok",children:"Archive is clean — nothing to remove."}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Done"})]}),t==="scanned"&&r&&!m&&s.jsxs("div",{children:[s.jsxs("div",{style:{marginBottom:14,lineHeight:1.6},children:["Found ",s.jsx("strong",{children:r.deletable_files})," unreferenced file",r.deletable_files!==1?"s":""," ","and ",s.jsx("strong",{children:r.orphaned_blob_rows})," orphaned DB record",r.orphaned_blob_rows!==1?"s":""," ","— ",s.jsx("strong",{children:Zs(r.total_bytes)})," recoverable."]}),s.jsxs("div",{style:{display:"flex",gap:8},children:[s.jsxs("button",{className:"btn-danger",onClick:h,children:["Delete (",Zs(r.total_bytes),")"]}),s.jsx("button",{className:"btn-ghost",onClick:c,children:"Cancel"})]})]}),t==="deleting"&&s.jsx("div",{className:"muted",children:"Deleting…"}),t==="done"&&i&&s.jsxs("div",{children:[s.jsxs("div",{className:"form-msg form-msg--ok",children:["Freed ",s.jsx("strong",{children:Zs(i.freed_bytes)})," ","— removed ",i.deleted_files," file",i.deleted_files!==1?"s":""," ","and ",i.deleted_blob_rows," DB record",i.deleted_blob_rows!==1?"s":"","."]}),i.errors&&i.errors.length>0&&s.jsxs("div",{className:"form-msg form-msg--err",style:{marginTop:6},children:[i.errors.length," file",i.errors.length!==1?"s":""," could not be deleted."]}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Scan again"})]}),t==="error"&&s.jsxs("div",{children:[s.jsx("div",{className:"form-msg form-msg--err",children:u}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Try again"})]})]})})}function Tm(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,u]=f.useState("global"),[o,c]=f.useState(""),[g,h]=f.useState("{}"),[m,y]=f.useState(null),[w,k]=f.useState(!1),[j,p]=f.useState({});f.useEffect(()=>{d()},[]);async function d(){r(!0),i(null);try{t(await em())}catch(S){i(S.message)}finally{r(!1)}}async function v(S){S.preventDefault();try{JSON.parse(g)}catch{y({ok:!1,text:'cookies must be valid JSON, e.g. {"session": "abc"}'});return}k(!0),y(null);try{await tm(a==="global"?null:o.trim(),a,g),c(""),h("{}"),u("global"),y({ok:!0,text:"Rule added."}),await d()}catch(D){y({ok:!1,text:D.message})}finally{k(!1)}}async function x(S){try{await rm(S),await d()}catch(D){i(D.message)}}function _(S){p(D=>({...D,[S.rule_uid]:{cookiesInput:S.cookies_json,saving:!1,msg:null}}))}function b(S){p(D=>{const L={...D};return delete L[S],L})}async function C(S){const D=j[S.rule_uid];try{JSON.parse(D.cookiesInput)}catch{p(L=>({...L,[S.rule_uid]:{...L[S.rule_uid],msg:{ok:!1,text:"Invalid JSON"}}}));return}p(L=>({...L,[S.rule_uid]:{...L[S.rule_uid],saving:!0,msg:null}}));try{await nm(S.rule_uid,{cookies_json:D.cookiesInput}),b(S.rule_uid),await d()}catch(L){p(H=>({...H,[S.rule_uid]:{...H[S.rule_uid],saving:!1,msg:{ok:!1,text:L.message}}}))}}return n?s.jsx("div",{className:"muted",children:"Loading…"}):l?s.jsx("div",{className:"form-msg form-msg--err",children:l}):s.jsx("div",{style:{maxWidth:560},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Cookie Rules"}),s.jsx("p",{className:"muted",style:{marginBottom:12},children:"Cookies are injected into every capture network request (yt-dlp, HTTP downloads, web-page snapshots). Global rules apply to all URLs; wildcard and regex rules apply only to matching URLs."}),e&&e.length>0?s.jsxs("table",{className:"data-table",style:{width:"100%",marginBottom:16},children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Pattern"}),s.jsx("th",{children:"Cookies"}),s.jsx("th",{style:{width:100},children:"Actions"})]})}),s.jsx("tbody",{children:e.map(S=>{const D=j[S.rule_uid],L=S.url_pattern?s.jsxs(s.Fragment,{children:[s.jsxs("span",{className:"muted",children:[S.pattern_kind,":"]})," ",s.jsx("code",{children:S.url_pattern})]}):s.jsx("span",{className:"muted",children:"global (all URLs)"});return s.jsxs("tr",{children:[s.jsx("td",{children:L}),s.jsx("td",{children:D?s.jsxs(s.Fragment,{children:[s.jsx("textarea",{className:"field-input",style:{fontFamily:"monospace",fontSize:12,width:"100%",minHeight:60},value:D.cookiesInput,onChange:H=>p(M=>({...M,[S.rule_uid]:{...M[S.rule_uid],cookiesInput:H.target.value}}))}),D.msg&&s.jsx("div",{className:`form-msg form-msg--${D.msg.ok?"ok":"err"}`,children:D.msg.text}),s.jsxs("div",{style:{display:"flex",gap:8,marginTop:4},children:[s.jsx("button",{className:"btn-primary",style:{fontSize:12,padding:"2px 8px"},disabled:D.saving,onClick:()=>C(S),children:D.saving?"Saving…":"Save"}),s.jsx("button",{className:"btn-secondary",style:{fontSize:12,padding:"2px 8px"},onClick:()=>b(S.rule_uid),children:"Cancel"})]})]}):s.jsx("code",{style:{fontSize:12,wordBreak:"break-all"},children:S.cookies_json})}),s.jsx("td",{children:!D&&s.jsxs("div",{style:{display:"flex",gap:6},children:[s.jsx("button",{className:"btn-secondary",style:{fontSize:12,padding:"2px 8px"},onClick:()=>_(S),children:"Edit"}),s.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"2px 8px"},onClick:()=>x(S.rule_uid),children:"Del"})]})})]},S.rule_uid)})})]}):s.jsx("p",{className:"muted",style:{marginBottom:16},children:"No cookie rules defined."}),s.jsx("h3",{style:{marginBottom:8},children:"Add Rule"}),s.jsxs("form",{onSubmit:v,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:"Pattern type"}),s.jsxs("select",{className:"field-input",value:a,onChange:S=>u(S.target.value),children:[s.jsx("option",{value:"global",children:"Global (all URLs)"}),s.jsx("option",{value:"wildcard",children:"Wildcard (e.g. *.youtube.com)"}),s.jsx("option",{value:"regex",children:"Regex (matched against full URL)"})]})]}),a!=="global"&&s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:a==="wildcard"?"URL/hostname pattern":"Regex pattern"}),s.jsx("input",{className:"field-input",type:"text",value:o,onChange:S=>c(S.target.value),placeholder:a==="wildcard"?"*.youtube.com or https://example.com/*":".*\\.youtube\\.com.*",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:"Cookies (JSON object)"}),s.jsx("textarea",{className:"field-input",style:{fontFamily:"monospace",fontSize:13,minHeight:70},value:g,onChange:S=>h(S.target.value),placeholder:'{"SESSION": "abc123", "token": "xyz"}',required:!0})]}),m&&s.jsx("div",{className:`form-msg form-msg--${m.ok?"ok":"err"}`,children:m.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:w,children:w?"Adding…":"Add Rule"})]})]})})}function Pm(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(!1),[a,u]=f.useState(null);f.useEffect(()=>{(async()=>{try{t(await Wa())}catch(j){u({ok:!1,text:j.message})}finally{r(!1)}})()},[]);async function o(j){i(!0),u(null);try{await Tl({ublock_enabled:j}),t(p=>({...p,ublock_enabled:j})),u({ok:!0,text:"Saved."})}catch(p){u({ok:!1,text:p.message})}finally{i(!1)}}async function c(j){i(!0),u(null);try{await Tl({cookie_ext_enabled:j}),t(p=>({...p,cookie_ext_enabled:j})),u({ok:!0,text:"Saved."})}catch(p){u({ok:!1,text:p.message})}finally{i(!1)}}async function g(j){i(!0),u(null);try{await Tl({modal_closer_enabled:j}),t(p=>({...p,modal_closer_enabled:j})),u({ok:!0,text:"Saved."})}catch(p){u({ok:!1,text:p.message})}finally{i(!1)}}if(n)return s.jsx("div",{className:"muted",children:"Loading\\u2026"});const h=(e==null?void 0:e.ublock_ext_available)??!1,m=(e==null?void 0:e.ublock_enabled)??!0,y=(e==null?void 0:e.cookie_ext_available)??!1,w=(e==null?void 0:e.cookie_ext_enabled)??!0,k=(e==null?void 0:e.modal_closer_enabled)??!0;return s.jsx("div",{children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Extensions"}),s.jsx("p",{className:"form-hint",style:{marginBottom:20},children:"Extensions run inside the browser during WebPage captures and can block ads, accept cookie banners, and more. Changes take effect on the next capture."}),s.jsxs("div",{className:"ext-grid",children:[s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"uBlock Origin Lite"}),s.jsx("span",{className:"ext-card-desc",children:"Blocks ads, trackers, and other page clutter during archiving via Chrome’s declarativeNetRequest API (Manifest V3)."}),!h&&s.jsxs("span",{className:"ext-card-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_UBLOCK_EXT"})," to the unpacked extension directory to enable."]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":m,className:`ext-toggle${m?" ext-toggle--on":""}`,onClick:()=>o(!m),disabled:l,"aria-label":"Toggle uBlock Origin Lite",children:s.jsx("span",{className:"ext-toggle-knob"})})]})}),s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"I Still Don’t Care About Cookies"}),s.jsx("span",{className:"ext-card-desc",children:"Dismiss cookie consent banners during archiving."}),!y&&s.jsxs("span",{className:"ext-card-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_COOKIE_EXT"})," to the unpacked extension directory to enable."]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":w,className:`ext-toggle${w?" ext-toggle--on":""}`,onClick:()=>c(!w),disabled:l,"aria-label":"Toggle I Still Don't Care About Cookies",children:s.jsx("span",{className:"ext-toggle-knob"})})]})}),s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"Modal & Dialog Closer"}),s.jsx("span",{className:"ext-card-desc",children:"Auto-dismiss cookie banners, consent overlays, and other modal dialogs before a WebPage capture is taken. Implemented as an injected browser script; no external extension required."})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":k,className:`ext-toggle${k?" ext-toggle--on":""}`,onClick:()=>g(!k),disabled:l,"aria-label":"Toggle Modal and Dialog Closer",children:s.jsx("span",{className:"ext-toggle-knob"})})]})})]}),a&&s.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text})]})})}const ou={0:"Private",1:"Public",2:"Users only",3:"Public"},bm=()=>s.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:s.jsx("path",{d:"M7 17 17 7M9 7h8v8"})});function Lm({archiveId:e,selectedEntry:t,selectedUids:n,selectedEntries:r,detail:l,onTagFilterSet:i,tagNodes:a,onTagsRefresh:u,onEntryTitleChange:o,onEntryDeleted:c,onBulkDeleted:g,humanizeTags:h,onDetailRefresh:m,onOpenPreview:y,onPlay:w}){var Jr;const[k,j]=f.useState([]),[p,d]=f.useState(""),[v,x]=f.useState([]),[_,b]=f.useState(""),C=f.useRef(0),S=f.useRef(!1),[D,L]=f.useState(!1),[H,M]=f.useState(""),[W,Y]=f.useState("idle"),[ce,ie]=f.useState(""),ee=f.useRef(null),[R,T]=f.useState(!1);f.useEffect(()=>{T(!1)},[(Jr=l==null?void 0:l.summary)==null?void 0:Jr.entry_uid]);const F=(n==null?void 0:n.size)>=2,[q,A]=f.useState(""),[ge,de]=f.useState("idle"),[Re,we]=f.useState(""),[je,E]=f.useState([]),[U,K]=f.useState(""),[V,X]=f.useState("idle"),[P,B]=f.useState(""),[re,le]=f.useState("idle");f.useEffect(()=>{const $=++C.current;if(ee.current&&(clearInterval(ee.current),ee.current=null),Y("idle"),ie(""),!t||!e){j([]),x([]);return}L(!1),M(""),S.current=!1,j([]),Promise.all([hl(e,t.entry_uid),Jh(e,t.entry_uid)]).then(([te,Ae])=>{$===C.current&&(j(te),x(Ae))}).catch(()=>{})},[t,e]),f.useEffect(()=>()=>{clearInterval(ee.current)},[]),f.useEffect(()=>{if(!F||!e){E([]);return}Td(e).then(E).catch(()=>E([]))},[F,e]),f.useEffect(()=>{A(""),de("idle"),we(""),K(""),X("idle"),B(""),le("idle")},[n]);async function rt(){const $=n.size;if(!window.confirm(`Delete ${$} entr${$===1?"y":"ies"}? This cannot be undone.`))return;le("running");const te=new Set;for(const Ae of n)try{await lu(e,Ae),te.add(Ae)}catch{}le("idle"),g==null||g(te)}async function Me(){const $=q.trim();if($){de("running"),we("");try{for(const te of n)await ru(e,te,$);A(""),de("done"),u==null||u(),setTimeout(()=>de("idle"),1800)}catch(te){we(te.message),de("error")}}}async function ve(){if(!U)return;X("running"),B("");const $=[];for(const te of n)try{await Pd(e,U,te)}catch{$.push(te)}$.length>0?(B(`Failed for ${$.length} entr${$.length===1?"y":"ies"}.`),X("error")):(X("done"),setTimeout(()=>X("idle"),1800))}async function en(){const $=H.trim()||null;try{await kh(e,t.entry_uid,$),o==null||o(t.entry_uid,$)}catch{}finally{L(!1)}}async function qe(){const $=p.trim();if(!(!$||!t))try{await ru(e,t.entry_uid,$),d(""),b("");const te=await hl(e,t.entry_uid);j(te),u()}catch(te){b(te.message)}}async function ys($){try{await jh(e,t.entry_uid,$);const te=await hl(e,t.entry_uid);j(te),u()}catch{}}async function xs(){if(!(!t||!e)&&window.confirm("Delete this entry? This cannot be undone."))try{await lu(e,t.entry_uid),c==null||c(t.entry_uid)}catch{}}async function ws(){if(!t||!e||W==="running")return;const $=C.current,te=t.entry_uid;Y("running"),ie("");try{const{job_uid:Ae}=await Zh(e,te);if(C.current!==$)return;ee.current=setInterval(async()=>{try{const dt=await Ed(e,Ae);if(dt.status==="completed"){if(clearInterval(ee.current),ee.current=null,C.current!==$)return;Y("done");const wn=await hl(e,te);if(C.current!==$)return;j(wn),m==null||m()}else if(dt.status==="failed"){if(clearInterval(ee.current),ee.current=null,C.current!==$)return;Y("error"),ie(dt.error_text||"Re-archive failed.")}}catch{if(clearInterval(ee.current),ee.current=null,C.current!==$)return;Y("error"),ie("Network error while polling.")}},500)}catch(Ae){if(C.current!==$)return;Y("error"),ie(Ae.message||"Failed to start re-archive.")}}const ks=l?[["Added",Ld(l.summary.archived_at)],["Source",l.summary.source_kind],["Type",l.summary.entity_kind],["Visibility",ou[l.summary.visibility]??l.summary.visibility],["Root",l.structured_root_relpath]]:[],js=new Set(["mp3","ogg","m4a","opus","wav","flac","aac"]),Ss=new Set(["mp4","webm","mov","mkv","avi","m4v","ogv","pdf","html","htm","jpg","jpeg","png","gif","webp","avif","svg","bmp"]),yn=l?l.artifacts.findIndex($=>$.artifact_role==="primary_media"):-1,xn=yn>=0?l.artifacts[yn]:null,Kr=xn?xn.relpath.split(".").pop().toLowerCase():"",Xr=xn&&js.has(Kr),Ns=yn>=0&&t?`/api/archives/${e}/entries/${t.entry_uid}/artifacts/${yn}`:null,_s=l&&!Xr&&(l.summary.entity_kind==="tweet"||l.summary.entity_kind==="tweet_thread"||xn&&Ss.has(Kr));return s.jsxs("aside",{className:"context-rail",children:[s.jsx("div",{className:"rail-eyebrow",children:"Context"}),F?s.jsxs("div",{className:"bulk-panel",children:[s.jsxs("p",{className:"bulk-count",children:[s.jsx("span",{className:"bulk-count-num",children:n.size})," entries selected"]}),s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Assign tag"}),Re&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:Re}),s.jsxs("div",{className:"tag-input-wrap",children:[s.jsx("span",{className:"hash",children:"/"}),s.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:q,onChange:$=>A($.target.value),onKeyDown:$=>{$.key==="Enter"&&Me()}}),s.jsx("button",{className:"tag-add-btn",onClick:Me,disabled:ge==="running"||!q.trim(),children:ge==="running"?"…":ge==="done"?"✓":"Add"})]})]}),je.length>0&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Add to collection"}),s.jsxs("div",{className:"bulk-coll-row",children:[s.jsxs("select",{className:"bulk-coll-select",value:U,onChange:$=>K($.target.value),children:[s.jsx("option",{value:"",children:"Pick a collection…"}),je.map($=>s.jsx("option",{value:$.collection_uid,children:$.name},$.collection_uid))]}),s.jsx("button",{className:"tag-add-btn",onClick:ve,disabled:!U||V==="running",children:V==="running"?"…":V==="done"?"✓":V==="error"?"!":"Add"})]}),P&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"6px 0 0"},children:P})]}),s.jsx("div",{className:"rail-delete-zone",children:s.jsx("button",{className:"rail-delete-btn",onClick:rt,disabled:re==="running",children:re==="running"?"Deleting…":`Delete ${n.size} entr${n.size===1?"y":"ies"}`})})]}):t?l?s.jsxs(s.Fragment,{children:[D?s.jsx("input",{className:"rail-title-input",autoFocus:!0,value:H,onChange:$=>M($.target.value),onKeyDown:$=>{$.key==="Enter"&&$.currentTarget.blur(),$.key==="Escape"&&(S.current=!0,$.currentTarget.blur())},onBlur:()=>{S.current?L(!1):en(),S.current=!1}}):s.jsxs("h2",{className:"rail-title rail-title--editable",title:"Click to rename",onClick:()=>{M(l.summary.title??""),L(!0)},children:[on(l.summary.title)||on(l.summary.entry_uid),s.jsx("svg",{className:"edit-icon",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:s.jsx("path",{d:"M11.5 2.5a1.5 1.5 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"})})]}),l.summary.original_url&&s.jsxs("a",{className:"url-tile",href:l.summary.original_url,target:"_blank",rel:"noopener noreferrer",children:[s.jsx("span",{className:"ico",dangerouslySetInnerHTML:{__html:Va(l.summary.source_kind)}}),s.jsx("span",{className:"u-text",children:l.summary.original_url}),s.jsx("span",{className:"ext",children:s.jsx(bm,{})})]}),Xr&&w&&s.jsx("button",{className:"rail-preview-btn",onClick:()=>w(Ns,t),children:"▶ Play"}),_s&&y&&s.jsx("button",{className:"rail-preview-btn",onClick:y,children:"Preview"}),s.jsx("div",{className:"meta-list",children:ks.filter(([,$])=>$!=null&&$!=="").map(([$,te])=>s.jsxs("div",{className:"meta-item",children:[s.jsx("span",{className:"meta-k",children:$}),s.jsx("span",{className:`meta-v${$==="Root"?" mono":""}`,children:on(te)})]},$))}),l.artifacts.length>0&&(()=>{const $=l.artifacts.map((Se,Gn)=>({...Se,_idx:Gn})),te=$.filter(Se=>Se.artifact_role==="font"),Ae=$.filter(Se=>Se.artifact_role!=="font"),dt=te.reduce((Se,Gn)=>Se+(Gn.byte_size||0),0),wn=l.summary.entry_uid,qr=Se=>s.jsx("li",{children:s.jsxs("a",{href:`/api/archives/${e}/entries/${wn}/artifacts/${Se._idx}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[s.jsx("span",{className:"artifact-name",children:Se.artifact_role==="font"?Se.relpath.split("/").pop():Se.artifact_role.replace(/_/g," ")}),s.jsx("span",{className:"artifact-size",children:Se.byte_size!=null?Zl(Se.byte_size):"—"})]})},Se._idx);return s.jsxs("div",{className:"rail-section",children:[s.jsxs("div",{className:"rail-section-heading",children:["Artifacts ",s.jsx("span",{className:"num",children:l.artifacts.length})]}),s.jsxs("ul",{className:"artifact-list",children:[Ae.map(qr),te.length>0&&s.jsxs("li",{className:"artifact-group",children:[s.jsxs("button",{type:"button",className:"artifact-group-header artifact-link","aria-expanded":R,onClick:()=>T(Se=>!Se),children:[s.jsxs("span",{className:"artifact-name",children:[s.jsx("span",{"aria-hidden":"true",className:`artifact-group-chevron${R?" open":""}`,children:"›"}),` fonts (${te.length})`]}),s.jsx("span",{className:"artifact-size",children:Zl(dt)})]}),R&&s.jsx("ul",{className:"artifact-list artifact-group-body",children:te.map(qr)})]})]})]})})()]}):s.jsx("p",{className:"tags-empty",children:"Loading\\u2026"}):s.jsx("p",{className:"tags-empty",children:"Select an entry."}),t&&!F&&s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Tags"}),k.length===0?s.jsx("p",{className:"tags-empty",children:"No tags yet."}):s.jsx("div",{className:"tags-wrap",children:k.map($=>s.jsxs("span",{className:"tag-pill",title:$.full_path,children:[h?zd($.full_path):$.full_path,s.jsx("button",{className:"remove",title:`Remove tag ${$.full_path}`,onClick:()=>ys($.tag_uid),children:"×"})]},$.tag_uid))}),_&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:_}),s.jsxs("div",{className:"tag-input-wrap",children:[s.jsx("span",{className:"hash",children:"/"}),s.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:p,onChange:$=>d($.target.value),onKeyDown:$=>{$.key==="Enter"&&qe()}}),s.jsx("button",{className:"tag-add-btn",onClick:qe,children:"Add"})]})]}),v.length>0&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Collections"}),v.map($=>s.jsxs("div",{className:"coll-row",children:[s.jsx("span",{className:"coll-name",children:$.collection_uid}),s.jsx("span",{className:"vis-badge",children:ou[$.visibility_bits]??`bits:${$.visibility_bits}`})]},$.collection_uid))]}),l&&(l.summary.entity_kind==="tweet"||l.summary.entity_kind==="tweet_thread")&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Actions"}),s.jsx("button",{className:"rail-rearchive-btn",onClick:ws,disabled:W==="running",children:W==="running"?"Re-archiving…":"Re-archive"}),W==="done"&&s.jsx("p",{className:"form-msg form-msg--ok",style:{marginTop:"6px"},children:"Re-archived successfully."}),W==="error"&&s.jsx("p",{className:"form-msg form-msg--err",style:{marginTop:"6px"},children:ce})]}),s.jsx("div",{className:"rail-delete-zone",children:s.jsx("button",{className:"rail-delete-btn",onClick:xs,children:"Delete entry"})})]})]})}function zm({src:e}){const t=f.useRef(null);return f.useEffect(()=>{t.current&&t.current.load()},[e]),s.jsx("div",{className:"preview-video-wrap",style:{background:"#111",display:"flex",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",minHeight:"240px"},children:e?s.jsxs("video",{ref:t,controls:!0,autoPlay:!1,style:{width:"100%",maxHeight:"100%",display:"block"},children:[s.jsx("source",{src:e}),"Your browser does not support the video element."]}):s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No video available"})})}function uu({src:e,type:t,title:n,originalUrl:r}){const l=r||e,i=n||null;return s.jsxs("div",{className:"preview-iframe-wrap",style:{flex:1,minHeight:0,display:"flex",flexDirection:"column"},children:[s.jsxs("div",{className:"preview-iframe-toolbar",style:{display:"flex",flexDirection:"column",gap:"2px",padding:"6px 12px",borderBottom:"1px solid var(--line-soft)",background:"var(--paper-2)",flexShrink:0},children:[i&&s.jsx("span",{style:{fontSize:"0.85rem",fontWeight:600,color:"var(--ink)",fontFamily:"var(--sans)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:i}),s.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[s.jsx("span",{style:{flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontSize:"0.78rem",color:"var(--muted)",fontFamily:"var(--sans)"},children:l}),s.jsx("a",{href:e,target:"_blank",rel:"noreferrer noopener",style:{fontSize:"0.78rem",color:"var(--accent)",textDecoration:"none",whiteSpace:"nowrap",fontFamily:"var(--sans)",flexShrink:0},children:t==="pdf"?"Open PDF ↗":"Open in new tab ↗"})]})]}),s.jsx("iframe",{src:e,sandbox:"allow-same-origin allow-popups",allow:"autoplay 'none'",referrerPolicy:"no-referrer",style:{flex:1,border:"none",width:"100%",minHeight:0},title:i||(t==="pdf"?"PDF preview":"Page preview")})]})}function Rm({src:e,alt:t}){return s.jsx("div",{className:"preview-image-wrap",style:{display:"flex",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",background:"var(--paper-2)",overflow:"hidden"},children:s.jsx("a",{href:e,target:"_blank",rel:"noreferrer noopener",style:{display:"contents"},children:s.jsx("img",{src:e,alt:t||"",style:{objectFit:"contain",maxHeight:"100%",maxWidth:"100%",display:"block",cursor:"pointer"}})})})}function Id(e){return e?new Intl.DateTimeFormat("en-US",{month:"short",day:"numeric",year:"numeric"}).format(new Date(e*1e3)):""}function cu(e){return!e||!e.includes("&")?e:e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'")}const J={card:{background:"var(--paper)",border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",maxWidth:"560px",margin:"0 auto",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},threadOuter:{border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",maxWidth:"560px",margin:"0 auto",background:"var(--paper)",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},tweetRow:{display:"flex",gap:"10px",padding:"10px 12px"},tweetRowThread:{display:"flex",gap:"10px",padding:"10px 12px 0"},leftCol:{display:"flex",flexDirection:"column",alignItems:"center",flexShrink:0,width:"36px"},rightCol:{flex:1,minWidth:0,paddingBottom:"8px"},avatar:{width:"36px",height:"36px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},avatarPh:{width:"36px",height:"36px",borderRadius:"50%",background:"var(--line)",flexShrink:0},threadLine:{flex:1,width:"2px",background:"var(--line-soft, var(--line))",margin:"4px 0",minHeight:"12px",borderRadius:"1px"},authorRow:{display:"flex",alignItems:"baseline",gap:"4px",flexWrap:"wrap",marginBottom:"4px",lineHeight:"1.3"},authorName:{fontWeight:"700",fontSize:"14px",color:"var(--ink)"},authorHandle:{fontSize:"13px",color:"var(--muted)"},datePart:{fontSize:"13px",color:"var(--muted)"},tweetText:{fontSize:"14px",lineHeight:"1.5",color:"var(--ink)",whiteSpace:"pre-line",marginBottom:"8px",wordBreak:"break-word"},stats:{display:"flex",gap:"12px",fontSize:"13px",color:"var(--muted)"},link:{color:"var(--accent)",textDecoration:"none"},loading:{padding:"24px 16px",textAlign:"center",color:"var(--muted)",fontSize:"14px"},error:{padding:"24px 16px",textAlign:"center",color:"var(--alert)",fontSize:"14px"},mediaGrid:{marginBottom:"8px",borderRadius:"10px",overflow:"hidden",border:"1px solid var(--line)"},mediaImg:{display:"block",width:"100%",objectFit:"cover",maxHeight:"260px"},mediaVideo:{display:"block",width:"100%",maxHeight:"260px",background:"#000"},article:{maxWidth:"560px",margin:"0 auto",border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",background:"var(--paper)",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},aCover:{width:"100%",display:"block",objectFit:"cover",maxHeight:"280px"},aMeta:{padding:"10px 14px 0"},aTweetTitle:{fontSize:"20px",fontWeight:"800",letterSpacing:"-0.3px",color:"var(--ink)",lineHeight:"1.3",marginBottom:"8px"},aAuthorRow:{display:"flex",alignItems:"center",gap:"8px",marginBottom:"8px"},aAvatar:{width:"36px",height:"36px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},aAvatarPh:{width:"36px",height:"36px",borderRadius:"50%",background:"var(--line)",flexShrink:0},aAuthorName:{fontSize:"14px",fontWeight:"700",color:"var(--ink)",lineHeight:"1.3"},aAuthorSub:{fontSize:"13px",color:"var(--muted)",lineHeight:"1.3"},aDivider:{border:"none",borderTop:"1px solid var(--line)",margin:"0"},aBody:{padding:"4px 14px 16px"},bH1:{fontSize:"22px",fontWeight:"800",letterSpacing:"-0.4px",color:"var(--ink)",lineHeight:"1.25",margin:"16px 0 6px"},bH2:{fontSize:"18px",fontWeight:"700",letterSpacing:"-0.2px",color:"var(--ink)",lineHeight:"1.3",margin:"14px 0 4px"},bP:{fontSize:"15px",color:"var(--ink)",lineHeight:"1.65",marginBottom:"12px",marginTop:"0"},bSpacer:{height:"4px",display:"block"},bQuote:{borderLeft:"3px solid var(--line)",padding:"2px 12px",margin:"12px 0",color:"var(--muted)",fontSize:"15px",lineHeight:"1.6"},bHr:{border:"none",borderTop:"1px solid var(--line)",margin:"14px 0"},bImg:{width:"100%",display:"block",borderRadius:"8px",margin:"12px 0"},bUl:{margin:"8px 0 12px",paddingLeft:"24px"},bOl:{margin:"8px 0 12px",paddingLeft:"24px"},bLi:{fontSize:"15px",color:"var(--ink)",lineHeight:"1.6",marginBottom:"4px"},bTweet:{display:"flex",alignItems:"center",gap:"10px",border:"1px solid var(--line)",borderRadius:"10px",padding:"10px 14px",margin:"12px 0",color:"var(--muted)",fontSize:"14px",textDecoration:"none"},bMdPre:{borderRadius:"8px",margin:"10px 0",overflow:"auto",background:"var(--paper-3, var(--field))",padding:"12px 14px"},bMdCode:{fontFamily:"ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, monospace",fontSize:"12px",lineHeight:"1.6",color:"var(--ink)",background:"transparent",display:"block"},iCode:{fontFamily:"ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace",fontSize:"0.875em",background:"var(--paper-3, var(--field))",padding:"1px 5px",borderRadius:"4px",color:"var(--ink)"},qtBadge:{fontSize:"11px",color:"var(--muted)",display:"inline-flex",alignItems:"center",gap:"2px",marginLeft:"4px",letterSpacing:"0.03em",flexShrink:0},lightboxBackdrop:{position:"fixed",inset:0,zIndex:500,background:"rgba(0,0,0,0.92)",display:"flex",alignItems:"center",justifyContent:"center",padding:"20px"},lightboxContent:{display:"flex",flexDirection:"column",alignItems:"center",gap:"10px",maxWidth:"90vw",maxHeight:"90vh"},lightboxToolbar:{display:"flex",alignItems:"center",gap:"10px",alignSelf:"flex-end"},lightboxImg:{maxWidth:"88vw",maxHeight:"80vh",objectFit:"contain",borderRadius:"6px",display:"block"},lightboxNav:{display:"flex",gap:"12px",alignItems:"center"},lightboxNavBtn:{background:"rgba(255,255,255,0.18)",border:"none",color:"#fff",fontSize:"20px",cursor:"pointer",borderRadius:"50%",width:"36px",height:"36px",display:"flex",alignItems:"center",justifyContent:"center",lineHeight:1,flexShrink:0},lightboxBtn:{background:"rgba(255,255,255,0.18)",border:"none",color:"#fff",fontSize:"16px",cursor:"pointer",borderRadius:"50%",width:"30px",height:"30px",display:"flex",alignItems:"center",justifyContent:"center",lineHeight:1,flexShrink:0},lightboxLink:{color:"rgba(255,255,255,0.7)",textDecoration:"none",fontSize:"14px"},lightboxCounter:{color:"rgba(255,255,255,0.6)",fontSize:"13px"}},Ne={bg:"#000000",border:"#2f3336",text:"#e7e9ea",dim:"#71767b",accent:"#1d9bf0",codeBg:"#16181c"},ei={article:{background:Ne.bg,color:Ne.text,minHeight:"100%",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'},articleInner:{maxWidth:"598px",margin:"0 auto",paddingBottom:"80px"},aCover:{width:"100%",display:"block",objectFit:"cover",maxHeight:"420px"},aMeta:{padding:"20px 16px 0"},aTweetTitle:{fontSize:"34px",fontWeight:"800",letterSpacing:"-0.5px",color:Ne.text,lineHeight:"44px",marginBottom:"16px"},aAuthorRow:{display:"flex",alignItems:"center",gap:"12px",marginBottom:"16px"},aAvatar:{width:"40px",height:"40px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},aAvatarPh:{width:"40px",height:"40px",borderRadius:"50%",background:Ne.border,flexShrink:0},aAuthorName:{fontSize:"15px",fontWeight:"700",color:Ne.text,lineHeight:"1.3"},aAuthorSub:{fontSize:"15px",color:Ne.dim,lineHeight:"1.3"},aDivider:{border:"none",borderTop:`1px solid ${Ne.border}`,margin:"0"},aBody:{padding:"4px 16px 0"},bH1:{fontSize:"26px",fontWeight:"800",letterSpacing:"-0.4px",color:Ne.text,lineHeight:"36px",margin:"24px 0 10px"},bH2:{fontSize:"26px",fontWeight:"800",letterSpacing:"-0.4px",color:Ne.text,lineHeight:"36px",margin:"24px 0 10px"},bP:{fontSize:"17px",color:Ne.text,lineHeight:"1.5",marginBottom:"16px",marginTop:"0"},bSpacer:{height:"6px",display:"block"},bQuote:{borderLeft:`3px solid ${Ne.border}`,padding:"2px 14px",margin:"14px 0",color:Ne.dim,fontSize:"17px",lineHeight:"1.5"},bHr:{border:"none",borderTop:`1px solid ${Ne.border}`,margin:"28px 0"},bImg:{width:"100%",display:"block",borderRadius:"12px",margin:"16px 0"},bUl:{margin:"10px 0 16px",paddingLeft:"28px"},bOl:{margin:"10px 0 16px",paddingLeft:"28px"},bLi:{fontSize:"17px",color:Ne.text,lineHeight:"1.5",marginBottom:"6px"},bTweet:{display:"flex",alignItems:"center",gap:"12px",border:`1px solid ${Ne.border}`,borderRadius:"12px",padding:"14px 16px",margin:"14px 0",color:Ne.dim,fontSize:"15px",textDecoration:"none"},bMdPre:{borderRadius:"12px",margin:"12px 0",overflow:"auto",background:"#1e2029"},bMdCode:{fontFamily:"ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, monospace",fontSize:"13px",lineHeight:"1.65",padding:"18px 20px",display:"block",color:Ne.text,background:"transparent"},iCode:{fontFamily:"ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace",fontSize:"0.875em",background:Ne.codeBg,padding:"2px 6px",borderRadius:"4px",color:"#e6edf3"},link:{color:Ne.accent,textDecoration:"none"}};function Dm(e,t,n){const r={};return n&&n.forEach((l,i)=>{l.relpath&&(r[l.relpath]=`/api/archives/${e}/entries/${t}/artifacts/${i}`)}),r}function Bn(e,t,n){return e&&n[e]?n[e]:t||null}function Od(){return s.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true",style:{flexShrink:0,color:"var(--ink)"},children:s.jsx("path",{d:"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.748l7.73-8.835L1.254 2.25H8.08l4.259 5.63L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"})})}function Qa(e,t,...n){var r;if(e.fromIndex!=null&&e.toIndex!=null)return{s:e.fromIndex,e:e.toIndex};if(((r=e.indices)==null?void 0:r.length)===2)return{s:e.indices[0],e:e.indices[1]};if(t)for(const l of n){if(!l)continue;const i=t.indexOf(l);if(i!==-1)return{s:i,e:i+l.length}}return null}function Fd(e,t){const n=e.expanded_url||e.url||e.text||"",r=e.display_url||e.expanded_url||e.url||e.text||"",l=Qa(e,t,e.url,e.text,e.display_url,e.expanded_url);return!l||!n?null:{...l,kind:"url",href:n,display:r}}function Md(e,t){const n=e.screen_name||e.name||e.text||"",r=n?`@${n}`:null,l=Qa(e,t,r);return!l||!n?null:{...l,kind:"mention",screen_name:n}}const du=/https?:\/\/[^\s<>"'\])]+/g,$m=/[.,;:!?)()]+$/;function es(e,t){const n=[];let r=0,l;for(du.lastIndex=0;(l=du.exec(e))!==null;){l.index>r&&n.push(e.slice(r,l.index));let i=l[0].replace($m,"");n.push(s.jsx("a",{href:i,target:"_blank",rel:"noopener noreferrer",style:t,children:i},l.index));const a=l[0].slice(i.length);a&&n.push(a),r=l.index+l[0].length}return r===0?e:(rFd(u,e)).filter(Boolean),...(t.user_mentions||[]).map(u=>Md(u,e)).filter(Boolean)];if(r.length===0&&n.length===0)return es(cu(e),J.link);const l=new Set([0,e.length]);for(const u of r)u.s>=0&&u.s<=e.length&&l.add(u.s),u.e>=0&&u.e<=e.length&&l.add(u.e);for(const[u,o]of n)u>=0&&u<=e.length&&l.add(u),o>=0&&o<=e.length&&l.add(o);const i=(u,o)=>n.some(([c,g])=>c<=u&&g>=o),a=[...l].sort((u,o)=>u-o);return a.slice(0,-1).map((u,o)=>{const c=a[o+1];if(i(u,c))return null;const g=e.slice(u,c),h=r.filter(w=>w.s<=u&&w.e>=c),m=h.find(w=>w.kind==="url");if(m)return s.jsx("a",{href:m.href,target:"_blank",rel:"noopener noreferrer",style:J.link,children:m.display||g},o);const y=h.find(w=>w.kind==="mention");return y?s.jsx("a",{href:`https://x.com/${y.screen_name}`,target:"_blank",rel:"noopener noreferrer",style:J.link,children:g},o):s.jsx("span",{children:es(cu(g),J.link)},o)}).filter(u=>u!==null)}function Om(e,t,n,r,l=J){if(!e)return null;t=t||[],n=n||[],r=r||[];const i=[];for(const o of t)o.length>0&&i.push({s:o.offset,e:o.offset+o.length,kind:"style",style:o.style});for(const o of n){const c=Fd(o,e);c&&i.push(c)}for(const o of r){const c=Md(o,e);c&&i.push(c)}if(i.length===0)return es(e,l.link);const a=new Set([0,e.length]);for(const o of i)o.s>=0&&o.s<=e.length&&a.add(o.s),o.e>=0&&o.e<=e.length&&a.add(o.e);const u=[...a].sort((o,c)=>o-c);return u.slice(0,-1).map((o,c)=>{const g=u[c+1],h=i.filter(j=>j.s<=o&&j.e>=g),m=e.slice(o,g);let y;if(m.includes(` -`)){const j=m.split(` -`);y=j.flatMap((p,d)=>dj.kind==="style"&&j.style==="Code")&&(y=s.jsx("code",{style:l.iCode,children:y})),h.some(j=>j.kind==="style"&&j.style==="Bold")&&(y=s.jsx("strong",{children:y})),h.some(j=>j.kind==="style"&&j.style==="Italic")&&(y=s.jsx("em",{children:y})),h.some(j=>j.kind==="style"&&j.style==="Underline")&&(y=s.jsx("u",{children:y})),h.some(j=>j.kind==="style"&&j.style==="Strikethrough")&&(y=s.jsx("s",{children:y}));const w=h.find(j=>j.kind==="url");if(w){const j=/^https?:\/\/t\.co\//i.test(y);y=s.jsx("a",{href:w.href,target:"_blank",rel:"noopener noreferrer",style:l.link,children:j?w.display:y})}const k=h.find(j=>j.kind==="mention");return k&&(y=s.jsx("a",{href:`https://x.com/${k.screen_name}`,target:"_blank",rel:"noopener noreferrer",style:l.link,children:y})),s.jsx("span",{children:typeof y=="string"?es(y,l.link):y},c)})}function Fm(e,t,n){const r=(n==null?void 0:n.st)||J,l=e.resolved_entities||[];return l.length===0?null:l.map((i,a)=>{var u;switch(i.type){case"divider":return s.jsx("hr",{style:r.bHr},a);case"media":{const o=Bn(i.local_path,i.url,t);return o?s.jsx("a",{href:o,target:"_blank",rel:"noopener noreferrer",style:{display:"block",cursor:"zoom-in"},onClick:c=>{var g;!c.metaKey&&!c.ctrlKey&&(c.preventDefault(),(g=n==null?void 0:n.onImgClick)==null||g.call(n,o))},children:s.jsx("img",{src:o,style:r.bImg,loading:"lazy",alt:""})},a):null}case"tweet":return i.tweet_id?s.jsxs("a",{href:`https://x.com/i/status/${i.tweet_id}`,target:"_blank",rel:"noopener noreferrer",style:r.bTweet,children:[s.jsx(Od,{}),"View post on X"]},a):null;case"link":return i.url?s.jsx("p",{style:r.bP,children:s.jsx("a",{href:i.url,target:"_blank",rel:"noopener noreferrer",style:r.link,children:i.url})},a):null;case"markdown":{const o=i.markdown??((u=i.data)==null?void 0:u.markdown)??"";return s.jsx("pre",{style:r.bMdPre,children:s.jsx("code",{style:r.bMdCode,children:o})},a)}case"emoji":return i.url?s.jsx("img",{src:i.url,alt:"",style:{height:"1.2em",verticalAlign:"middle",margin:"0 1px"}},a):null;default:return null}})}function ti(e,t,n,r){const l=(r==null?void 0:r.st)||J,i=e.type||"",a=e.text||"",u=e.inline_style_ranges||[],o=e.data||{},c=Om(a,u,o.urls||[],o.mentions||[],l);switch(i){case"header-one":return s.jsx("h1",{style:l.bH1,children:c},t);case"header-two":{const g=a.match(/(?:x\.com|twitter\.com)\/i\/status\/(\d+)/);return g?s.jsxs("a",{href:`https://x.com/i/status/${g[1]}`,target:"_blank",rel:"noopener noreferrer",style:l.bTweet,children:[s.jsx(Od,{}),"View post on X"]},t):s.jsx("h2",{style:l.bH2,children:c},t)}case"unstyled":return a.trim()?s.jsx("p",{style:l.bP,children:c},t):s.jsx("span",{style:l.bSpacer},t);case"blockquote":return s.jsx("blockquote",{style:l.bQuote,children:c},t);case"unordered-list-item":case"ordered-list-item":return s.jsx("li",{style:l.bLi,children:c},t);case"atomic":return s.jsx("span",{children:Fm(e,n,r)},t);default:return a?s.jsx("p",{style:l.bP,children:c},t):null}}function Mm(e,t,n){const r=(n==null?void 0:n.st)||J,l=[];let i=0;for(;i{r&&(l==null||l(!0))},[r,l]);const u=r?ei:J,o=e.cover_media||{},c=e.author||t||{},g=e.first_published_at_secs?Id(e.first_published_at_secs):"",m=[c.screen_name?`@${c.screen_name}`:"",g].filter(Boolean).join(" · "),y=Bn(o.local_path,o.url,n),w=Bn(c.avatar_local_path,c.avatar_url,n),k=s.jsxs(s.Fragment,{children:[i&&s.jsx(Ad,{items:[{src:i,alt:""}],startIndex:0,onClose:()=>a(null)}),y&&s.jsx("a",{href:y,target:"_blank",rel:"noopener noreferrer",style:{display:"block",cursor:"zoom-in"},onClick:j=>{!j.metaKey&&!j.ctrlKey&&(j.preventDefault(),a(y))},children:s.jsx("img",{src:y,style:u.aCover,alt:"Article cover"})}),s.jsxs("div",{style:u.aMeta,children:[e.title&&s.jsx("div",{style:u.aTweetTitle,children:e.title}),s.jsxs("div",{style:u.aAuthorRow,children:[w?s.jsx("img",{src:w,style:u.aAvatar,alt:c.name||""}):s.jsx("div",{style:u.aAvatarPh}),s.jsxs("div",{children:[s.jsx("div",{style:u.aAuthorName,children:c.name||c.screen_name||"Unknown"}),m&&s.jsx("div",{style:u.aAuthorSub,children:m})]})]})]}),s.jsx("hr",{style:u.aDivider}),s.jsx("div",{style:u.aBody,children:Mm(e.blocks||[],n,{onImgClick:a,st:u})})]});return r?s.jsx("div",{style:ei.article,children:s.jsx("div",{style:ei.articleInner,children:k})}):s.jsx("div",{style:J.article,children:k})}function Bm({photos:e,onOpen:t}){const n=e.length;if(n===0)return null;if(n===1)return s.jsx("a",{href:e[0].src,target:"_blank",rel:"noopener noreferrer",style:{display:"block"},onClick:l=>{!l.metaKey&&!l.ctrlKey&&(l.preventDefault(),t(0))},children:s.jsx("img",{src:e[0].src,alt:e[0].alt||"",style:J.mediaImg,loading:"lazy"})});const r=n<=2?"180px":"140px";return s.jsx("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gridTemplateRows:n===2?r:`${r} ${r}`,gap:"2px"},children:e.map((l,i)=>s.jsx("a",{href:l.src,target:"_blank",rel:"noopener noreferrer",style:{display:"block",overflow:"hidden",gridRow:n===3&&i===0?"span 2":void 0},onClick:a=>{!a.metaKey&&!a.ctrlKey&&(a.preventDefault(),t(i))},children:s.jsx("img",{src:l.src,alt:l.alt||"",loading:"lazy",style:{width:"100%",height:"100%",objectFit:"cover",display:"block"}})},i))})}function Ad({items:e,startIndex:t,onClose:n}){const[r,l]=f.useState(t);f.useEffect(()=>{const a=u=>{(u.key==="Escape"||u.key==="ArrowLeft"||u.key==="ArrowRight")&&(u.stopPropagation(),u.preventDefault()),u.key==="Escape"&&n(),u.key==="ArrowRight"&&l(o=>Math.min(o+1,e.length-1)),u.key==="ArrowLeft"&&l(o=>Math.max(o-1,0))};return document.addEventListener("keydown",a),()=>document.removeEventListener("keydown",a)},[n,e.length]);const i=e[r];return s.jsx("div",{style:J.lightboxBackdrop,onClick:n,children:s.jsxs("div",{style:J.lightboxContent,onClick:a=>a.stopPropagation(),children:[s.jsxs("div",{style:J.lightboxToolbar,children:[e.length>1&&s.jsxs("span",{style:J.lightboxCounter,children:[r+1," / ",e.length]}),s.jsx("a",{href:i.src,target:"_blank",rel:"noopener noreferrer",style:J.lightboxLink,title:"Open in new tab",children:"↗"}),s.jsx("button",{style:J.lightboxBtn,onClick:n,"aria-label":"Close",children:"×"})]}),s.jsx("img",{src:i.src,alt:i.alt||"",style:J.lightboxImg}),e.length>1&&s.jsxs("div",{style:J.lightboxNav,children:[s.jsx("button",{style:J.lightboxNavBtn,onClick:()=>l(a=>Math.max(a-1,0)),disabled:r===0,children:"‹"}),s.jsx("button",{style:J.lightboxNavBtn,onClick:()=>l(a=>Math.min(a+1,e.length-1)),disabled:r===e.length-1,children:"›"})]})]})})}function fu({tweet:e,isInThread:t,isLast:n,artifactMap:r}){var d,v;const[l,i]=f.useState(null),a=e.author||{},u=e.created_at_secs?Id(e.created_at_secs):"",o=e.entities||{},c=Bn(a.avatar_local_path,a.avatar_url,r),g=t&&!n,h=e.is_quote_status===!0,m=t?J.tweetRowThread:J.tweetRow,y=e.full_text||"",w=((v=(d=e.extended_entities)==null?void 0:d.media)!=null&&v.length?e.extended_entities.media:o.media)||[],k=[],j=[];for(const x of w)if(x.type==="photo"){const _=Bn(x.local_path,x.media_url_https,r);_&&k.push({kind:"photo",src:_,alt:x.alt_text||""})}else if(x.type==="video"||x.type==="animated_gif"){const _=x.local_path&&r[x.local_path]||(()=>{var C,S;return(S=(((C=x.video_info)==null?void 0:C.variants)||[]).filter(D=>D.content_type==="video/mp4").sort((D,L)=>(L.bitrate||0)-(D.bitrate||0))[0])==null?void 0:S.url})();_&&j.push({kind:x.type==="animated_gif"?"gif":"video",src:_})}const p=[];for(const x of w){if(!x.url||!(x.type==="photo"?k.some(C=>C.src===Bn(x.local_path,x.media_url_https,r)):j.length>0))continue;const b=Qa(x,y,x.url);b&&p.push([b.s,b.e])}return s.jsxs(s.Fragment,{children:[l!==null&&s.jsx(Ad,{items:k,startIndex:l,onClose:()=>i(null)}),s.jsxs("div",{style:m,children:[s.jsxs("div",{style:J.leftCol,children:[c?s.jsx("img",{src:c,style:J.avatar,alt:a.name||""}):s.jsx("div",{style:J.avatarPh}),g&&s.jsx("div",{style:J.threadLine})]}),s.jsxs("div",{style:J.rightCol,children:[s.jsxs("div",{style:J.authorRow,children:[s.jsx("span",{style:J.authorName,children:a.name||a.screen_name||"Unknown"}),a.screen_name&&s.jsxs("span",{style:J.authorHandle,children:["@",a.screen_name]}),u&&s.jsxs("span",{style:J.datePart,children:["· ",u]}),h&&s.jsx("span",{style:J.qtBadge,title:"Quote tweet",children:"↻ QT"})]}),s.jsx("div",{style:J.tweetText,children:Im(y,o,p)}),k.length>0&&s.jsx("div",{style:J.mediaGrid,children:s.jsx(Bm,{photos:k,onOpen:x=>i(x)})}),j.map((x,_)=>s.jsx("div",{style:J.mediaGrid,children:s.jsx("video",{src:x.src,style:J.mediaVideo,controls:!0,loop:x.kind==="gif",muted:x.kind==="gif",autoPlay:x.kind==="gif"})},_)),(e.retweet_count>0||e.favorite_count>0)&&s.jsxs("div",{style:J.stats,children:[e.favorite_count>0&&s.jsxs("span",{children:["❤️ ",e.favorite_count.toLocaleString()]}),e.retweet_count>0&&s.jsxs("span",{children:["🔁 ",e.retweet_count.toLocaleString()]})]})]})]})]})}function Um({archiveId:e,entryUid:t,artifacts:n,entityKind:r,fullPage:l,onXArticle:i}){const[a,u]=f.useState(!0),[o,c]=f.useState(null),[g,h]=f.useState([]);if(f.useEffect(()=>{if(u(!0),c(null),h([]),!n||!e||!t){u(!1);return}const w=n.map((j,p)=>({...j,index:p})).filter(j=>j.artifact_role==="raw_tweet_json");if(w.length===0){c("No tweet data found."),u(!1);return}let k=!1;return xh(e,t,w.map(j=>j.index)).then(async j=>{if(k)return;const p=/https?:\/\/t\.co\/[A-Za-z0-9]+/g,d=C=>{var D;const S=C.full_text||"";return(((D=C.entities)==null?void 0:D.urls)||[]).map(L=>{var H;if(L.fromIndex!=null&&L.toIndex!=null)return[L.fromIndex,L.toIndex];if(((H=L.indices)==null?void 0:H.length)===2)return[L.indices[0],L.indices[1]];if(L.url){const M=S.indexOf(L.url);if(M!==-1)return[M,M+L.url.length]}return null}).filter(Boolean)},v=(C,S,D)=>C.some(([L,H])=>L<=S&&H>=D),x=new Set;for(const C of j){const S=C.full_text||"",D=d(C);let L;for(p.lastIndex=0;(L=p.exec(S))!==null;)v(D,L.index,L.index+L[0].length)||x.add(L[0])}const _=x.size>0?await wh([...x]).catch(()=>({})):{},b=j.map(C=>{var M;const S=C.full_text||"",D=d(C),L=[];let H;for(p.lastIndex=0;(H=p.exec(S))!==null;){const W=H[0],Y=_[W];Y&&Y!==W&&!v(D,H.index,H.index+W.length)&&L.push({url:W,expanded_url:Y,display_url:(()=>{try{const ce=new URL(Y);return ce.hostname+(ce.pathname.length>1?"/…":"")}catch{return Y}})(),fromIndex:H.index,toIndex:H.index+W.length})}return L.length===0?C:{...C,entities:{...C.entities||{},urls:[...((M=C.entities)==null?void 0:M.urls)||[],...L]}}});k||h(b)}).catch(j=>{k||c(j.message||"Failed to load tweet.")}).finally(()=>{k||u(!1)}),()=>{k=!0}},[e,t,n]),a)return s.jsx("div",{style:J.loading,children:"Loading…"});if(o)return s.jsxs("div",{style:J.error,children:["Error: ",o]});if(g.length===0)return null;const m=Dm(e,t,n);if(r==="tweet_thread")return s.jsx("div",{style:J.threadOuter,children:g.map((w,k)=>s.jsx(fu,{tweet:w,isInThread:!0,isLast:k===g.length-1,artifactMap:m},w.id||k))});const y=g[0];return y.is_article&&y.article?s.jsx(Am,{article:y.article,tweetAuthor:y.author,artifactMap:m,fullPage:l,onXArticle:i}):s.jsx("div",{style:J.card,children:s.jsx(fu,{tweet:y,isInThread:!1,isLast:!0,artifactMap:m})})}const Hm=new Set(["mp4","webm","mov","mkv","avi","m4v","ogv"]),Wm=new Set(["mp3","ogg","m4a","opus","wav","flac","aac"]),Vm=new Set(["jpg","jpeg","png","gif","webp","avif","svg","bmp"]);function Bd({archiveId:e,entry:t,detail:n,fullPage:r,onXArticle:l}){if(!t)return s.jsx("div",{className:"preview-panel preview-panel--empty",children:s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"Select an entry to preview"})});if(!n)return s.jsx("div",{className:"preview-panel preview-panel--loading",children:s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"Loading…"})});const{summary:i,artifacts:a}=n,u=i.entry_uid,o=i.entity_kind;if(o==="tweet"||o==="tweet_thread"){const y=s.jsx(Um,{archiveId:e,entryUid:u,artifacts:a,entityKind:o,fullPage:r,onXArticle:l});return r?y:s.jsx("div",{className:"preview-tweet-wrap",children:y})}const c=a.findIndex(y=>y.artifact_role==="primary_media");if(c===-1)return s.jsxs("div",{className:"preview-panel preview-panel--no-preview",children:[s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No preview available"}),a.length>0&&s.jsx("ul",{style:{marginTop:"12px",paddingLeft:"20px",fontSize:"0.8rem",color:"var(--muted-2)",fontFamily:"var(--sans)"},children:a.map((y,w)=>s.jsxs("li",{children:[y.artifact_role,": ",y.relpath]},w))})]});const g=a[c],h=`/api/archives/${e}/entries/${u}/artifacts/${c}`,m=g.relpath.split(".").pop().toLowerCase();return Hm.has(m)?s.jsx("div",{className:"preview-panel",children:s.jsx(zm,{src:h})}):Wm.has(m)?s.jsxs("div",{className:"preview-panel preview-panel--audio",style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:"12px",padding:"24px",fontFamily:"var(--sans)"},children:[s.jsx("span",{style:{fontSize:"2rem"},children:"🎵"}),s.jsx("span",{style:{color:"var(--ink)",fontSize:"0.95rem",fontWeight:600},children:i.title||u}),s.jsx("audio",{src:h,controls:!0,style:{marginTop:"8px",width:"100%",maxWidth:"400px"}})]}):m==="pdf"?s.jsx("div",{className:"preview-panel",style:{flex:1,minHeight:0},children:s.jsx(uu,{src:h,type:"pdf",title:i.title,originalUrl:i.original_url})}):m==="html"||m==="htm"?s.jsx("div",{className:"preview-panel",style:{flex:1,minHeight:0},children:s.jsx(uu,{src:h,type:"page",title:i.title,originalUrl:i.original_url})}):Vm.has(m)?s.jsx("div",{className:"preview-panel",style:{height:"100%"},children:s.jsx(Rm,{src:h,alt:i.title||"Image"})}):s.jsxs("div",{className:"preview-panel preview-panel--no-preview",children:[s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No preview available"}),s.jsx("ul",{style:{marginTop:"12px",paddingLeft:"20px",fontSize:"0.8rem",color:"var(--muted-2)",fontFamily:"var(--sans)"},children:a.map((y,w)=>s.jsxs("li",{children:[y.artifact_role,": ",y.relpath]},w))})]})}function Qm({archiveId:e,entry:t,detail:n,onClose:r}){var l,i;return f.useEffect(()=>{const a=u=>{u.key==="Escape"&&r()};return window.addEventListener("keydown",a),()=>window.removeEventListener("keydown",a)},[r]),s.jsx("div",{className:"preview-modal-backdrop",onClick:r,children:s.jsxs("div",{className:`preview-modal${((l=n==null?void 0:n.summary)==null?void 0:l.entity_kind)==="tweet"||((i=n==null?void 0:n.summary)==null?void 0:i.entity_kind)==="tweet_thread"?"":" preview-modal--full"}`,onClick:a=>a.stopPropagation(),children:[s.jsxs("div",{className:"preview-modal-header",children:[s.jsx("span",{className:"preview-modal-title",children:(t==null?void 0:t.title)||(t==null?void 0:t.entry_uid)||"Preview"}),s.jsx("a",{className:"preview-modal-newtab",href:`/preview/${e}/${t==null?void 0:t.entry_uid}`,target:"_blank",rel:"noopener noreferrer",title:"Open in new tab",children:"↗"}),s.jsx("button",{className:"preview-modal-close",onClick:r,"aria-label":"Close preview",children:"×"})]}),s.jsx("div",{className:"preview-modal-body",children:s.jsx(Bd,{archiveId:e,entry:t,detail:n})})]})})}function pu(e){if(!isFinite(e)||isNaN(e))return"--:--";const t=Math.floor(e/60),n=Math.floor(e%60);return`${t}:${String(n).padStart(2,"0")}`}function Km({entry:e,src:t,archiveId:n,onClose:r}){const l=f.useRef(null),[i,a]=f.useState(!1),[u,o]=f.useState(0),[c,g]=f.useState(NaN),[h,m]=f.useState(1);f.useEffect(()=>{!l.current||!t||(l.current.load(),o(0),g(NaN),a(!1))},[t]),f.useEffect(()=>{l.current&&(l.current.volume=h)},[h]);function y(){const d=l.current;d&&(i?d.pause():d.play().catch(()=>{}))}function w(d){const v=l.current;if(!v||!isFinite(c))return;const x=Number(d.target.value);v.currentTime=x,o(x)}function k(d){m(Number(d.target.value))}const j=(e==null?void 0:e.title)||(e==null?void 0:e.entry_uid)||"Unknown",p=(e==null?void 0:e.source_kind)||"other";return s.jsxs(s.Fragment,{children:[s.jsx("audio",{ref:l,src:t||void 0,preload:"auto",onPlay:()=>a(!0),onPause:()=>a(!1),onEnded:()=>a(!1),onTimeUpdate:()=>{var d;return o(((d=l.current)==null?void 0:d.currentTime)??0)},onLoadedMetadata:()=>{var d;return g(((d=l.current)==null?void 0:d.duration)??NaN)},onDurationChange:()=>{var d;return g(((d=l.current)==null?void 0:d.duration)??NaN)},style:{display:"none"}}),s.jsxs("div",{className:"audio-bar",style:{position:"fixed",bottom:0,left:0,right:0,zIndex:100,background:"var(--paper-3)",borderTop:"1px solid var(--line)",display:"flex",alignItems:"center",gap:"16px",padding:"0 16px",height:"56px",fontFamily:"var(--sans)"},children:[s.jsxs("div",{className:"audio-bar-info",style:{display:"flex",alignItems:"center",gap:"8px",minWidth:0,flex:"0 1 220px",overflow:"hidden"},children:[s.jsx("span",{className:"source-icon",style:{flexShrink:0,width:"18px",height:"18px",display:"flex",alignItems:"center"},dangerouslySetInnerHTML:{__html:Va(p)}}),s.jsx("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontSize:"0.85rem",color:"var(--ink)"},title:j,children:j})]}),s.jsxs("div",{className:"audio-bar-controls",style:{display:"flex",alignItems:"center",gap:"10px",flex:"1 1 0",minWidth:0},children:[s.jsx("button",{onClick:y,"aria-label":i?"Pause":"Play",style:{background:"none",border:"none",cursor:"pointer",padding:"4px",color:"var(--ink)",flexShrink:0,fontSize:"1.2rem",lineHeight:1},children:i?"⏸":"▶"}),s.jsx("span",{style:{fontSize:"0.75rem",color:"var(--muted)",whiteSpace:"nowrap",flexShrink:0},children:pu(u)}),s.jsx("input",{type:"range",min:0,max:isFinite(c)?c:0,step:.1,value:isFinite(u)?u:0,onChange:w,"aria-label":"Seek",style:{flex:1,minWidth:0,accentColor:"var(--accent)"}}),s.jsx("span",{style:{fontSize:"0.75rem",color:"var(--muted)",whiteSpace:"nowrap",flexShrink:0},children:pu(c)})]}),s.jsxs("div",{className:"audio-bar-right",style:{display:"flex",alignItems:"center",gap:"8px",flex:"0 1 160px"},children:[s.jsx("span",{style:{fontSize:"0.85rem",color:"var(--muted)",flexShrink:0},children:"🔊"}),s.jsx("input",{type:"range",min:0,max:1,step:.01,value:h,onChange:k,"aria-label":"Volume",style:{width:"80px",accentColor:"var(--accent)"}}),s.jsx("button",{onClick:r,"aria-label":"Close audio player",style:{background:"none",border:"none",cursor:"pointer",padding:"4px",color:"var(--muted)",fontSize:"1rem",lineHeight:1,marginLeft:"4px"},children:"✕"})]})]})]})}function Xm({archiveId:e,entryUid:t}){var m,y;const[n,r]=f.useState(null),[l,i]=f.useState(!0),[a,u]=f.useState(null),[o,c]=f.useState(!1);f.useEffect(()=>{if(!o)return;const w=document.documentElement,k=document.body,j=w.style.colorScheme,p=k.style.background;return w.style.colorScheme="dark",k.style.background="#000",()=>{w.style.colorScheme=j,k.style.background=p}},[o]),f.useEffect(()=>{c(!1),Xi(e,t).then(w=>{r(w),i(!1)}).catch(w=>{u((w==null?void 0:w.message)||"Failed to load entry"),i(!1)})},[e,t]);const g=((m=n==null?void 0:n.summary)==null?void 0:m.title)||t,h=(y=n==null?void 0:n.summary)==null?void 0:y.original_url;return s.jsxs("div",{style:{minHeight:"100vh",display:"flex",flexDirection:"column",background:o?"#000":"var(--paper)",fontFamily:"var(--sans)"},children:[s.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px",padding:o?"8px 12px":"10px 16px",borderBottom:o?"none":"1px solid var(--line)",flexShrink:0,position:o?"sticky":"relative",top:0,zIndex:20,background:o?"rgba(0,0,0,0.82)":"var(--paper-2)",backdropFilter:o?"blur(12px)":"none",WebkitBackdropFilter:o?"blur(12px)":"none"},children:[s.jsx("a",{href:"/",style:{color:o?"#1d9bf0":"var(--accent)",textDecoration:"none",fontSize:"13px",flexShrink:0},children:"← Archive"}),s.jsx("span",{style:{flex:1,fontSize:"14px",fontWeight:600,color:o?"#e7e9ea":"var(--ink)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:g}),h&&s.jsx("a",{href:h,target:"_blank",rel:"noopener noreferrer",style:{color:o?"#71767b":"var(--muted)",textDecoration:"none",fontSize:"13px",flexShrink:0},children:"Original ↗"})]}),s.jsxs("div",{style:o?{flex:1}:{flex:1,minHeight:0,overflow:"auto",display:"flex",flexDirection:"column"},children:[l&&s.jsx("div",{style:{padding:"48px",textAlign:"center",color:"var(--muted)",fontSize:"14px"},children:"Loading…"}),a&&s.jsx("div",{style:{padding:"48px",textAlign:"center",color:"var(--alert)",fontSize:"14px"},children:a}),n&&s.jsx(Bd,{archiveId:e,entry:n.summary,detail:n,fullPage:!0,onXArticle:c})]})]})}const Jm=7e3;function qm({toasts:e,onDismiss:t,onIgnoreUblock:n}){return e.length?s.jsx("div",{className:"toast-stack",role:"log","aria-live":"polite","aria-label":"Notifications",children:e.map(r=>s.jsx(Gm,{toast:r,onDismiss:t,onIgnoreUblock:n},r.id))}):null}function Ym(e){if(!e)return null;if(e.length<=52)return e;try{const{hostname:t,pathname:n,search:r}=new URL(e),l=n.split("/").filter(Boolean),i=(l[l.length-1]??"")+r,a=i?`${t}/…/${i}`:t;return a.length<=56?a:a.slice(0,53)+"…"}catch{return"…"+e.slice(-51)}}function Gm({toast:e,onDismiss:t,onIgnoreUblock:n}){const[r,l]=f.useState(!1),i=e.type==="warning",a=e.type==="success";f.useEffect(()=>{if(r)return;const o=setTimeout(()=>t(e.id),Jm);return()=>clearTimeout(o)},[r,e.id,t]);const u=Ym(e.locator);return a?s.jsx("div",{className:"toast toast--success",role:"alert","aria-atomic":"true",children:s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"✓"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Archived"}),u&&s.jsx("span",{className:"toast-locator",title:e.locator,children:u})]}),s.jsx("div",{className:"toast-btns",children:s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})})]})}):i?s.jsxs("div",{className:"toast toast--warning",role:"alert","aria-atomic":"true",children:[s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"⚠"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Archived with warnings"}),u&&s.jsx("span",{className:"toast-locator",title:e.locator,children:u})]}),s.jsxs("div",{className:"toast-btns",children:[e.text&&s.jsx("button",{type:"button",className:"toast-view-btn",onClick:()=>l(o=>!o),"aria-expanded":r,children:r?"Hide":"Details"}),e.locator&&s.jsx("button",{type:"button",className:"toast-view-btn toast-ignore-btn",onClick:()=>{n==null||n(),t(e.id)},children:"Ignore"}),s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})]})]}),r&&s.jsx("p",{className:"toast-warning-detail",children:e.text||"The page was captured but one or more browser extensions were unavailable (ad-blocking or cookie-consent). Check ARCHIVR_UBLOCK_EXT and ARCHIVR_COOKIE_EXT in your server config."})]}):s.jsxs("div",{className:"toast toast--error",role:"alert","aria-atomic":"true",children:[s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"✕"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Capture failed"}),u&&s.jsx("span",{className:"toast-locator",title:e.locator,children:u})]}),s.jsxs("div",{className:"toast-btns",children:[e.text&&s.jsx("button",{type:"button",className:"toast-view-btn",onClick:()=>l(o=>!o),children:r?"Hide":"View error"}),s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})]})]}),r&&e.text&&s.jsx("pre",{className:"toast-error-detail",children:e.text})]})}const vs=f.createContext(null),cr=(()=>{const e=window.location.pathname.match(/^\/preview\/([^/]+)\/([^/]+)/);return e?{archiveId:e[1],entryUid:e[2]}:null})(),Zm=["archive","tags","collections","runs","admin","settings"],eg=["profile","tokens","instance","cookies","extensions","storage"];function tn(){const e=window.location.pathname.split("/").filter(Boolean),t=Zm.includes(e[0])?e[0]:"archive",n=t==="settings"&&eg.includes(e[1])?e[1]:"profile",r=new URLSearchParams(window.location.search),l=r.get("q")??"",i=t==="archive"?r.get("tag")??null:null,a=t==="archive"?r.get("entry")??null:null;return{view:t,settingsTab:n,q:l,tag:i,entry:a}}function tg(e,t){return e==="archive"?"/":e==="settings"?`/settings/${t}`:`/${e}`}function ng(){const[e,t]=f.useState("loading"),[n,r]=f.useState(null);f.useEffect(()=>{(async()=>{if(await Ph()){t("setup");return}const I=await Rh();if(!I){t("login");return}r(I),t("authenticated")})()},[]),f.useEffect(()=>{const N=()=>{r(null),t("login")};return window.addEventListener("auth:expired",N),()=>window.removeEventListener("auth:expired",N)},[]),f.useEffect(()=>{const N=()=>{const{view:I,settingsTab:Q,q:ne,tag:De,entry:lt}=tn();v(I),_(Q),C(ne),p(De),h(lt),y(null),k(lt?new Set([lt]):new Set)};return window.addEventListener("popstate",N),()=>window.removeEventListener("popstate",N)},[]);const[l,i]=f.useState([]),[a,u]=f.useState(null),[o,c]=f.useState([]),[g,h]=f.useState(()=>tn().entry),[m,y]=f.useState(null),[w,k]=f.useState(()=>{const N=tn().entry;return N?new Set([N]):new Set}),[j,p]=f.useState(()=>tn().tag),[d,v]=f.useState(()=>tn().view),[x,_]=f.useState(()=>tn().settingsTab),[b,C]=f.useState(()=>tn().q),[S,D]=f.useState(""),[L,H]=f.useState(!1),[M,W]=f.useState([]),[Y,ce]=f.useState([]),[ie,ee]=f.useState(()=>sessionStorage.getItem("captureDialogOpen")==="true"),[R,T]=f.useState([]),F=f.useRef(0),[q,A]=f.useState(()=>{try{const N=JSON.parse(sessionStorage.getItem("pendingCaptures")||"[]");if(Array.isArray(N))return N}catch{}return[]});f.useEffect(()=>{sessionStorage.setItem("pendingCaptures",JSON.stringify(q))},[q]);const[ge,de]=f.useState(()=>sessionStorage.getItem("ublockWarningIgnored")==="true"),[Re,we]=f.useState(null),je=f.useRef(0),E=f.useRef(null),U=f.useRef(!1),K=f.useRef(!0),V=f.useRef(null),X=(n==null?void 0:n.humanize_slugs)??!1;f.useEffect(()=>{const N=++je.current;we(null),!(!m||!a)&&Xi(a,m.entry_uid).then(I=>{N===je.current&&we(I)}).catch(()=>{})},[m,a]),f.useEffect(()=>{sessionStorage.setItem("captureDialogOpen",ie)},[ie]);const P=f.useCallback(async(N,I,Q)=>{if(N){H(!0);try{let ne;I||Q?ne=await yh(N,I,Q):ne=await vh(N),c(ne),k(De=>{if(De.size<2)return De;const lt=new Set(ne.map(kn=>kn.entry_uid)),Zn=new Set([...De].filter(kn=>lt.has(kn)));return Zn.size===De.size?De:Zn}),D(ne.length===0?"No results":`${ne.length} result${ne.length===1?"":"s"}`)}catch{c([]),D("Search failed. Try again.")}finally{H(!1)}}},[]);f.useEffect(()=>{e==="authenticated"&&gh().then(N=>{if(i(N),N.length>0){const I=N[0].id;u(I)}})},[e]),f.useEffect(()=>{if(a){if(K.current){K.current=!1,Promise.all([Gs(a).then(W),ml(a).then(ce)]);return}p(null),y(null),h(null),k(new Set),Promise.all([P(a,"",null),Gs(a).then(W),ml(a).then(ce)])}},[a]),f.useEffect(()=>{if(a===null)return;const N=setTimeout(()=>{P(a,b,j)},300);return()=>clearTimeout(N)},[b,a]),f.useEffect(()=>{a!==null&&(j!==null&&v("archive"),P(a,b,j))},[j,a]);const B=f.useCallback(N=>{u(N)},[]),re=f.useCallback(N=>{v(N),N==="tags"&&a&&ml(a).then(ce)},[a]);f.useEffect(()=>{if(cr)return;const N=tg(d,x);window.location.pathname!==N&&history.pushState(null,"",N+window.location.search)},[d,x]);const le=f.useCallback(N=>{h(N?N.entry_uid:null),y(N)},[]),rt=f.useCallback((N,I)=>{if(I.shiftKey&&V.current!==null){I.preventDefault();const Q=o.findIndex(er=>er.entry_uid===V.current),ne=o.findIndex(er=>er.entry_uid===N.entry_uid);if(Q===-1||ne===-1){V.current=N.entry_uid,k(new Set([N.entry_uid])),le(N);return}const De=Math.min(Q,ne),lt=Math.max(Q,ne),Zn=o.slice(De,lt+1),kn=new Set(Zn.map(er=>er.entry_uid));k(kn),kn.size===1&&le(Zn[0])}else I.ctrlKey||I.metaKey?(V.current=N.entry_uid,k(Q=>{const ne=new Set(Q);return ne.has(N.entry_uid)?ne.delete(N.entry_uid):ne.add(N.entry_uid),ne})):(V.current=N.entry_uid,k(new Set([N.entry_uid])),le(N))},[o,le]),Me=f.useCallback(N=>{p(N)},[]),ve=f.useCallback(()=>{p(null)},[]),en=f.useCallback(()=>{a&&ml(a).then(ce)},[a]),qe=f.useCallback((N,I)=>{j===N?p(I):j!=null&&j.startsWith(N+"/")&&p(I+j.slice(N.length))},[j]),ys=f.useCallback(N=>{(j===N||j!=null&&j.startsWith(N+"/"))&&p(null)},[j]),xs=f.useCallback((N,I)=>{c(Q=>Q.map(ne=>ne.entry_uid===N?{...ne,title:I}:ne)),y(Q=>Q&&Q.entry_uid===N?{...Q,title:I}:Q),we(Q=>Q&&Q.summary.entry_uid===N?{...Q,summary:{...Q.summary,title:I}}:Q)},[]),ws=f.useCallback(()=>{if(!a||!m)return;const N=++je.current;Xi(a,m.entry_uid).then(I=>{N===je.current&&we(I)}).catch(()=>{})},[a,m]),ks=f.useCallback(N=>{c(I=>I.filter(Q=>Q.entry_uid!==N)),y(I=>(I==null?void 0:I.entry_uid)===N?null:I),h(I=>I===N?null:I),k(I=>{const Q=new Set(I);return Q.delete(N),Q})},[]),js=f.useCallback(N=>{c(I=>I.filter(Q=>!N.has(Q.entry_uid))),k(new Set),y(null),h(null)},[]);f.useEffect(()=>{if(w.size>=2)h(null),y(null);else if(w.size===1){const[N]=w;h(N)}else h(null),y(null)},[w]);const Ss=f.useMemo(()=>o.filter(N=>w.has(N.entry_uid)),[o,w]);f.useEffect(()=>{if(!g||m)return;const N=o.find(I=>I.entry_uid===g);N&&y(N)},[o,g,m]),f.useEffect(()=>{if(cr)return;const N=new URLSearchParams;b&&N.set("q",b),d==="archive"&&j&&N.set("tag",j),d==="archive"&&g&&N.set("entry",g);const I=N.toString(),Q=window.location.pathname+(I?"?"+I:"");window.location.pathname+window.location.search!==Q&&history.replaceState(null,"",Q)},[b,j,g,d]),f.useEffect(()=>{const N=I=>{var Q,ne;(I.metaKey||I.ctrlKey)&&I.key==="k"&&(I.preventDefault(),d==="archive"?((Q=E.current)==null||Q.focus(),(ne=E.current)==null||ne.select()):(U.current=!0,v("archive")))};return document.addEventListener("keydown",N),()=>document.removeEventListener("keydown",N)},[d]),f.useEffect(()=>{d==="archive"&&U.current&&(U.current=!1,requestAnimationFrame(()=>{var N,I;(N=E.current)==null||N.focus(),(I=E.current)==null||I.select()}))},[d]);const yn=f.useCallback(()=>{ee(!0)},[]),xn=f.useCallback(()=>{ee(!1)},[]),Kr=f.useCallback(()=>{if(a)return Promise.all([P(a,b,j),Gs(a).then(W)])},[a,b,j,P]),Xr=f.useCallback((N,I,Q="error",ne=null)=>{if(Q==="warning"&&ge&&I)return;const De=++F.current;T(lt=>[...lt,{id:De,text:N,locator:I,type:Q,headline:ne}])},[ge]),Ns=f.useCallback(N=>{T(I=>I.filter(Q=>Q.id!==N))},[]),_s=f.useCallback(()=>{sessionStorage.setItem("ublockWarningIgnored","true"),de(!0),T(N=>N.filter(I=>!(I.type==="warning"&&I.locator)))},[]),Jr=f.useCallback(N=>{A(I=>[...I,N])},[]),$=f.useCallback(N=>{A(I=>I.filter(Q=>Q.id!==N))},[]),[te,Ae]=f.useState(null),[dt,wn]=f.useState(null),qr=f.useCallback(()=>{m&&Ae(m.entry_uid)},[m]),Se=f.useCallback(()=>Ae(null),[]),Gn=f.useCallback((N,I)=>{wn({src:N,entry:I})},[]),Ud=f.useCallback(()=>wn(null),[]);return f.useEffect(()=>{Ae(null)},[m]),f.useEffect(()=>{const N=I=>{var ne,De,lt;if(I.key!=="Escape"||ie||te)return;const Q=(ne=document.activeElement)==null?void 0:ne.tagName;Q==="INPUT"||Q==="TEXTAREA"||Q==="SELECT"||w.size>0&&(I.preventDefault(),k(new Set),(lt=(De=document.activeElement)==null?void 0:De.blur)==null||lt.call(De))};return window.addEventListener("keydown",N),()=>window.removeEventListener("keydown",N)},[ie,te,w]),f.useEffect(()=>(document.body.classList.toggle("has-audio-bar",!!dt),()=>document.body.classList.remove("has-audio-bar")),[dt]),e==="loading"?s.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?s.jsx(im,{onComplete:()=>t("login")}):e==="login"?s.jsx(sm,{onLogin:N=>{r(N),t("authenticated")}}):cr?s.jsx(Xm,{archiveId:cr.archiveId,entryUid:cr.entryUid}):s.jsx(vs.Provider,{value:{currentUser:n,setCurrentUser:r},children:s.jsxs(s.Fragment,{children:[s.jsx(am,{archives:l,archiveId:a,onArchiveChange:B,view:d,onViewChange:re,onCaptureClick:yn}),s.jsxs("main",{className:"app-shell",children:[s.jsxs("div",{className:"workspace",children:[d==="archive"&&s.jsxs("div",{className:"toolbar",children:[s.jsxs("div",{className:"search-field",children:[s.jsx("span",{className:"ico","aria-hidden":"true",children:s.jsxs("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("circle",{cx:"11",cy:"11",r:"7"}),s.jsx("line",{x1:"16.5",y1:"16.5",x2:"21",y2:"21"})]})}),s.jsx("input",{ref:E,className:"search-input",type:"search","aria-label":"Search archive","aria-busy":L,placeholder:"Search titles, URLs, types, tags…",value:b,onChange:N=>C(N.target.value)}),s.jsx("span",{className:"kbd",children:"⌘K"})]}),s.jsxs("span",{className:"result-count",children:[S&&s.jsxs(s.Fragment,{children:[s.jsx("b",{children:S.split(" ")[0]})," ",S.split(" ").slice(1).join(" ")]}),j&&s.jsxs("button",{className:"tag-filter-badge",onClick:ve,children:["× ",X?zd(j):j]})]})]}),d==="archive"&&s.jsx(pm,{entries:o,selectedUids:w,onRowClick:rt,archiveId:a,pendingCaptures:q}),d==="runs"&&s.jsx(gm,{runs:M}),d==="admin"&&s.jsx(ym,{archives:l}),d==="tags"&&s.jsx(xm,{archiveId:a,tagNodes:Y,tagFilter:j,onTagFilterSet:Me,onViewChange:re,onTagRenamed:qe,onTagDeleted:ys,onTagsRefresh:en,humanizeTags:X}),d==="collections"&&s.jsx(km,{archiveId:a}),d==="settings"&&s.jsx(Sm,{tab:x,onTabChange:_,archiveId:a})]}),s.jsx(Lm,{archiveId:a,selectedEntry:m,selectedUids:w,selectedEntries:Ss,detail:Re,onTagFilterSet:Me,tagNodes:Y,onTagsRefresh:en,onEntryTitleChange:xs,onEntryDeleted:ks,onBulkDeleted:js,humanizeTags:X,onDetailRefresh:ws,onOpenPreview:qr,onPlay:Gn})]}),te&&m&&m.entry_uid===te&&s.jsx(Qm,{archiveId:a,entry:m,detail:Re,onClose:Se}),dt&&s.jsx(Km,{entry:dt.entry,src:dt.src,archiveId:a,onClose:Ud}),s.jsx(om,{open:ie,archiveId:a,onClose:xn,onCaptured:Kr,onToast:Xr,activeJobs:q,onJobStarted:Jr,onJobSettled:$}),s.jsx(qm,{toasts:R,onDismiss:Ns,onIgnoreUblock:_s})]})})}Cd(document.getElementById("root")).render(s.jsx(f.StrictMode,{children:s.jsx(ng,{})})); diff --git a/crates/archivr-server/static/assets/index-D8ic-z4p.css b/crates/archivr-server/static/assets/index-D8ic-z4p.css new file mode 100644 index 0000000..cd248bf --- /dev/null +++ b/crates/archivr-server/static/assets/index-D8ic-z4p.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600&family=Spectral:wght@500;600&display=swap";:root{color-scheme:light;--ink: #20251f;--muted: #6b6f66;--muted-2: #8a8d83;--paper: #f5f0e7;--paper-2: #e9e1d2;--paper-3: #fffaf0;--line: #d2c6b5;--line-soft: #e5dccd;--accent: #8d3f30;--accent-2: #b78342;--link: #245f72;--top: #141d18;--field: #fffdf7;--r: 3px;--r2: 6px;--r3: 10px;--sans: "Helvetica Neue", Helvetica, Arial, sans-serif;--serif: "Spectral", Georgia, "Times New Roman", serif;--display: "Cormorant Garamond", Georgia, serif}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:14px;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}button,input,select{font:inherit}.workspace,.context-rail{scrollbar-width:thin;scrollbar-color:var(--line) transparent}.workspace::-webkit-scrollbar,.context-rail::-webkit-scrollbar{width:11px;height:11px}.workspace::-webkit-scrollbar-track,.context-rail::-webkit-scrollbar-track{background:transparent}.workspace::-webkit-scrollbar-thumb,.context-rail::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:3px solid transparent;background-clip:padding-box}.workspace::-webkit-scrollbar-thumb:hover,.context-rail::-webkit-scrollbar-thumb:hover{background:var(--muted-2);background-clip:padding-box;border:3px solid transparent}.topbar{height:58px;display:grid;grid-template-columns:auto auto 1fr auto auto;align-items:center;gap:26px;padding:0 22px;background:var(--top);color:#efe6d6;border-bottom:3px solid var(--accent)}.brand{font-family:var(--display);font-weight:600;font-size:30px;line-height:1;letter-spacing:.01em;color:#f4ead8}.switcher{position:relative;display:inline-flex;align-items:center}.switcher select{-moz-appearance:none;appearance:none;-webkit-appearance:none;border:1px solid rgba(247,238,223,.2);background:#f7eedf0f;color:#f1e8d8;font-family:var(--sans);font-weight:550;font-size:13.5px;letter-spacing:.04em;padding:8px 32px 8px 15px;cursor:pointer;border-radius:6px;transition:background .15s ease,border-color .15s ease}.switcher select:hover{background:#f7eedf1c;border-color:#f7eedf57}.switcher:after{content:"";position:absolute;right:15px;top:50%;width:6px;height:6px;margin-top:-5px;border-right:1.2px solid #b6ab98;border-bottom:1.2px solid #b6ab98;transform:rotate(45deg);pointer-events:none}.nav{display:flex;gap:22px;justify-content:flex-end;min-width:0}.nav-link{border:0;background:transparent;color:#cdc1ad;cursor:pointer;padding:6px 0;font-size:12.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;position:relative}.nav-link:hover,.nav-link.is-active{color:#f4ead8}.nav-link.is-active:after{content:"";position:absolute;left:0;right:0;bottom:-2px;height:1px;background:var(--accent-2)}.capture-button{display:inline-flex;align-items:center;gap:8px;position:relative;overflow:hidden;border:0;background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 88%,#fff) 0%,var(--accent) 55%);color:#f7eddd;padding:10px 18px;cursor:pointer;font-size:12px;font-weight:600;letter-spacing:.15em;text-transform:uppercase;border-radius:var(--r);box-shadow:inset 0 1px #fff5e629,0 1px 2px #141d184d;transition:filter .18s ease,box-shadow .18s ease}.capture-button:hover{filter:brightness(.93);box-shadow:inset 0 1px #fff5e61f,0 2px 6px #141d1857}.capture-button:before{content:"";position:absolute;top:0;bottom:0;left:0;width:45%;background:linear-gradient(100deg,transparent 0%,rgba(255,246,232,.45) 50%,transparent 100%);transform:translate(-180%) skew(-18deg);pointer-events:none}@media (prefers-reduced-motion: no-preference){.capture-button:hover:before{animation:cap-sheen .7s ease}}@keyframes cap-sheen{0%{transform:translate(-180%) skew(-18deg)}to{transform:translate(340%) skew(-18deg)}}.app-shell{height:calc(100vh - 58px);display:grid;grid-template-columns:minmax(0,1fr) 340px}.workspace{min-width:0;overflow:auto;display:flex;flex-direction:column}.view{display:none}.view.is-active{display:block}.toolbar{position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:16px;padding:12px 22px;background:color-mix(in srgb,var(--paper) 88%,white);border-bottom:1px solid var(--line-soft)}.search-field{position:relative;flex:1 1 auto;min-width:0;display:flex;align-items:center;height:42px;background:var(--field);border:1px solid var(--line);border-radius:20px;transition:border-color .15s ease,box-shadow .15s ease}.search-field:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 14%,transparent)}.search-field .ico{display:grid;place-items:center;width:46px;height:100%;color:var(--muted-2);flex-shrink:0}.search-field .ico svg{width:16px;height:16px}.search-input{flex:1;min-width:0;height:100%;border:0;background:transparent;color:var(--ink);padding:0 6px 0 0;font-size:14px;letter-spacing:.01em;outline:none}.search-input::placeholder{color:var(--muted-2)}.search-field .kbd{margin-right:12px;display:inline-flex;align-items:center;padding:3px 7px;font-size:11px;color:var(--muted);background:var(--paper-2);border:1px solid var(--line-soft);border-radius:var(--r);flex-shrink:0}.result-count{flex-shrink:0;font-size:13px;color:var(--muted);letter-spacing:.02em;white-space:nowrap}.result-count b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}.tag-filter-badge{display:inline-flex;align-items:center;gap:4px;background:var(--accent);color:#fff;border:0;border-radius:var(--r);font-size:12px;padding:2px 8px;cursor:pointer;margin-left:8px}.tag-filter-badge:hover{opacity:.85}.entry-table{width:100%;font-size:12.5px}.entry-header-row{display:flex;align-items:stretch;position:sticky;top:67px;z-index:2;background:color-mix(in srgb,var(--paper) 92%,white);border-bottom:1px solid var(--line-soft);color:var(--muted);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em}.entry-header-row>div{padding:9px 10px;flex-shrink:0;font-weight:600;display:flex;align-items:center}#entries-body>div{display:flex;align-items:center;cursor:default;border-bottom:1px solid var(--line-soft)}#entries-body>div>div{padding:7px 10px;flex-shrink:0;overflow:hidden}#entries-body>div:not(.entry-row-outer):nth-child(2n){background:#f2ede5}#entries-body>div:not(.entry-row-outer):nth-child(odd){background:var(--paper-3)}#entries-body>.entry-row-outer--light{background:var(--paper-3)}#entries-body>.entry-row-outer--dark{background:#f2ede5}#entries-body>div.is-selected{background:#eee2d2;box-shadow:inset 0 0 0 2px var(--accent)}#entries-body>div.is-multi-selected{background:#eee2d2;box-shadow:inset 0 0 0 2px var(--accent)}.col-added{width:162px;color:var(--muted)}.col-title{flex:1 1 0;min-width:0;overflow:hidden;display:flex;align-items:center;gap:.42em}.col-type{width:116px}.col-size{width:100px;display:flex;flex-direction:column;justify-content:center;gap:1px}.size-total{font-variant-numeric:tabular-nums}.size-cached-pct{font-size:10px;color:var(--muted);opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.col-url{flex:0 0 30%;min-width:0;overflow:hidden}.entry-header-row .col-added,#entries-body>div .col-added{padding-left:22px}.entry-header-row>div:last-child,#entries-body>div>div:last-child{padding-right:22px}.entry-title{color:var(--link);font-weight:700;min-width:0}.entry-header-row>div{padding:9px 10px;flex-shrink:0;font-weight:600}.source-icon{display:flex;align-items:center;justify-content:center;width:1.05em;height:1.05em;flex-shrink:0}.source-icon>*{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.source-icon svg{width:100%;height:100%}.url-cell{color:#555b55;white-space:nowrap;text-overflow:ellipsis;word-break:break-all}#entries-body .url-cell:hover{overflow:visible;white-space:normal}.type-pill{display:inline-block;padding:2px 6px;background:#d8e3df;color:#275a5f;border:1px solid #bfd0ca;border-radius:var(--r)}.col-check{display:none;width:40px;flex-shrink:0;align-items:center;justify-content:center;padding:0!important;cursor:pointer}.row-checkbox{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;border:1.5px solid var(--line);cursor:pointer;display:block;width:17px;height:17px;border-radius:4px;background:var(--paper-3);transition:border-color .15s ease,background .15s ease;position:relative;flex-shrink:0}.row-checkbox.is-checked{background:var(--accent);border-color:var(--accent)}.row-checkbox.is-checked:after{content:"";position:absolute;left:4px;top:2px;width:5px;height:8px;border:2px solid #fff;border-top:none;border-left:none;transform:rotate(45deg)}@media (pointer: coarse){.col-check{display:flex}.entry-header-row .col-added,#entries-body>div .col-added{padding-left:10px}}#runs-view .entry-table{border-collapse:collapse;table-layout:auto}#runs-view .entry-table th{position:sticky;top:0;z-index:1;text-align:left;padding:10px;background:color-mix(in srgb,var(--paper) 92%,white);color:var(--muted);border-bottom:1px solid var(--line-soft);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em}#runs-view .entry-table td{padding:10px;border-bottom:1px solid var(--line-soft);vertical-align:top}#runs-view .entry-table tr:nth-child(2n) td{background:#f2ede5}#runs-view .entry-table tr:nth-child(odd) td{background:var(--paper-3)}.context-rail{border-left:1px solid var(--line);background:var(--paper);padding:20px 20px 32px;overflow:auto}.rail-eyebrow{font-size:10.5px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted-2);margin-bottom:16px}.rail-title{display:block;font-family:var(--sans);font-weight:700;font-size:15.5px;line-height:1.4;color:var(--ink);margin:0 0 16px;word-break:break-word}.url-tile{display:flex;align-items:center;gap:9px;padding:10px 12px;background:var(--paper-3);border:1px solid var(--line);border-radius:var(--r3);margin-bottom:20px;text-decoration:none}.url-tile:hover{background:var(--field);border-color:var(--accent)}.url-tile .ico{color:var(--ink);flex-shrink:0;display:grid;place-items:center}.url-tile .ico svg{width:15px;height:15px}.url-tile .u-text{min-width:0;flex:1;font-size:12.5px;color:var(--link);font-family:ui-monospace,SF Mono,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.url-tile .ext{color:var(--muted-2);flex-shrink:0}.url-tile .ext svg{width:13px;height:13px;display:block}.meta-list{margin-bottom:24px;border-top:1px solid var(--line-soft)}.meta-item{display:grid;grid-template-columns:92px 1fr;gap:14px;align-items:baseline;padding:9px 0;border-bottom:1px solid var(--line-soft)}.meta-k{font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted-2)}.meta-v{font-size:13.5px;color:var(--ink);word-break:break-word;line-height:1.4}.meta-v.mono{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px;color:var(--muted)}.rail-section{margin-bottom:24px}.rail-section-heading{display:flex;align-items:baseline;gap:8px;font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.12em;color:var(--muted-2);margin-bottom:9px;padding-bottom:8px;border-bottom:1px solid var(--line)}.rail-section-heading .num{color:var(--muted-2);font-weight:600}.artifact-list{list-style:none;margin:0;padding:0}.artifact-link{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:7px 2px;border-bottom:1px solid var(--line-soft);text-decoration:none}.artifact-link:last-child{border-bottom:0}.artifact-name{font-size:13px;color:var(--link);min-width:0;word-break:break-word}.artifact-link:hover .artifact-name{text-decoration:underline}.artifact-size{font-size:11.5px;color:var(--muted-2);flex-shrink:0;font-variant-numeric:tabular-nums}.artifact-group{list-style:none}.artifact-group-header{width:100%;background:none;border:none;border-bottom:1px solid var(--line-soft);cursor:pointer;text-align:left;color:inherit;font:inherit}.artifact-group-header:hover .artifact-name{text-decoration:underline}.artifact-group-chevron{display:inline-block;font-style:normal;transition:transform .15s ease;transform:rotate(0);transform-origin:45% 50%;margin-right:3px}.artifact-group-chevron.open{transform:rotate(90deg)}.artifact-group-body{padding-left:14px}.tags-wrap{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 12px}.tag-pill{display:inline-flex;align-items:center;gap:7px;background:var(--paper-2);border:1px solid var(--line);border-radius:var(--r);font-size:12px;padding:4px 7px 4px 10px;color:var(--ink)}.tag-pill .remove{border:0;background:transparent;cursor:pointer;color:var(--muted-2);width:15px;height:15px;display:grid;place-items:center;font-size:13px;line-height:1}.tag-pill .remove:hover{color:var(--accent)}.tags-empty{font-size:13px;color:var(--muted);margin:0 0 11px}.tag-input-wrap{display:flex;align-items:center;gap:8px;background:var(--field);border:1px solid var(--line);border-radius:var(--r2);padding:2px 2px 2px 11px;transition:border-color .15s ease,box-shadow .15s ease}.tag-input-wrap:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent)}.tag-input-wrap .hash{color:var(--muted-2);font-size:13px}.tag-input{flex:1;min-width:0;border:0;background:transparent;color:var(--ink);font-size:13px;padding:6px 0;outline:none;font-family:ui-monospace,SF Mono,Menlo,monospace}.tag-add-btn{border:0;background:var(--ink);color:var(--paper-3);padding:6px 13px;font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r);cursor:pointer;white-space:nowrap}.tag-add-btn:hover{background:#2c332b}.coll-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 13px;background:var(--field);border:1px solid var(--line);border-radius:var(--r2);margin-bottom:6px;transition:border-color .15s ease}.coll-row:hover{border-color:var(--accent)}.coll-name{font-size:13px;color:var(--ink);font-weight:500}.vis-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);background:var(--paper-2);border:1px solid var(--line-soft);border-radius:999px;padding:3px 10px}.vis-badge:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent-2)}.bulk-panel{padding-top:2px}.bulk-count{font-size:13px;color:var(--muted);margin-bottom:22px}.bulk-count-num{font-size:22px;font-weight:700;color:var(--ink);line-height:1;margin-right:4px}.bulk-coll-row{display:flex;align-items:center;gap:8px}.bulk-coll-select{flex:1;min-width:0;height:32px;padding:0 8px;border:1px solid var(--line);border-radius:var(--r);background:var(--field);color:var(--ink);font-size:13px;cursor:pointer;outline:none;transition:border-color .15s ease}.bulk-coll-select:focus{border-color:var(--accent)}.tag-add-btn:disabled{opacity:.45;cursor:default}.rail-delete-zone{margin-top:24px;padding-top:20px;border-top:1px solid var(--line-soft)}.rail-delete-btn{width:100%;padding:8px 14px;background:none;border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);color:var(--accent);border-radius:var(--r);cursor:pointer;font-size:12.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;transition:background .15s ease,border-color .15s ease}.rail-delete-btn:hover{background:color-mix(in srgb,var(--accent) 8%,transparent);border-color:var(--accent)}.rail-rearchive-btn{width:100%;padding:6px 12px;background:var(--surface-2, #2a2a2a);color:var(--text, #e0e0e0);border:1px solid var(--border, #444);border-radius:4px;cursor:pointer;font-size:13px}.rail-rearchive-btn:hover:not(:disabled){background:var(--surface-3, #333)}.rail-rearchive-btn:disabled{opacity:.6;cursor:not-allowed}.capture-dialog{border:1px solid var(--line);background:var(--paper);padding:0;width:min(540px,calc(100vw - 32px));border-radius:var(--r3);box-shadow:0 20px 60px #141d1847}.capture-dialog::backdrop{background:#141d1873}.capture-dialog-inner{padding:28px}.capture-dialog-title{margin:0;font-size:22px;font-family:var(--display);font-weight:600}.capture-label{display:block;font-size:11px;font-weight:600;margin-bottom:6px;color:var(--muted-2);text-transform:uppercase;letter-spacing:.06em}.capture-input{width:100%;height:44px;border:1px solid var(--line);background:var(--field);color:var(--ink);padding:0 14px;font-size:15px;margin-bottom:10px;border-radius:var(--r2);outline:none;transition:border-color .15s ease,box-shadow .15s ease}.capture-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 14%,transparent)}.form-field .capture-input{margin-bottom:0}.capture-error{color:var(--accent);font-size:13px;margin-bottom:10px;min-height:18px}.capture-actions{display:flex;flex-direction:column;gap:8px;margin-top:18px}.capture-submit{border:0;background:var(--ink);color:var(--paper);padding:13px 20px;border-radius:var(--r2);cursor:pointer;font-weight:600;font-size:15px;width:100%;min-width:220px;letter-spacing:.01em}.capture-submit:hover{opacity:.85}.capture-submit:disabled{opacity:.45;cursor:default}.capture-cancel{border:0;background:none;color:var(--muted);padding:7px 18px;border-radius:var(--r);cursor:pointer;font-size:13px;text-align:center;width:100%}.capture-cancel:hover{color:var(--ink);background:var(--paper-2)}.capture-advanced{margin-top:12px}.capture-advanced-toggle{display:flex;align-items:center;gap:5px;background:none;border:0;cursor:pointer;color:var(--muted);font-size:12.5px;padding:4px 0;border-radius:var(--r)}.capture-advanced-toggle:hover{color:var(--ink)}.capture-chevron{width:14px;height:14px;flex-shrink:0;transition:transform .18s ease}.capture-chevron--open{transform:rotate(180deg)}.capture-advanced-panel{margin-top:8px;padding:10px 12px;border:1px solid var(--line);border-radius:var(--r2);background:var(--paper-2)}.capture-ext-row{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:default}.capture-ext-label{flex:1;min-width:0}.capture-ext-name{display:block;font-size:13px;font-weight:600;color:var(--ink)}.capture-ext-desc{display:block;font-size:11.5px;color:var(--muted);margin-top:1px}.capture-dialog-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.capture-dialog-close{flex-shrink:0;background:none;border:0;cursor:pointer;color:var(--muted);width:28px;height:28px;display:grid;place-items:center;border-radius:var(--r);padding:0}.capture-dialog-close svg{width:14px;height:14px}.capture-dialog-close:hover{background:var(--paper-2);color:var(--ink)}.capture-rows{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.capture-row{display:flex;flex-direction:column;gap:3px}.capture-row-main{display:flex;align-items:center;gap:8px}.capture-row-main .capture-input{margin-bottom:0}.capture-quality{flex-shrink:0;height:32px;padding:0 6px;border:1px solid var(--line);border-radius:var(--r);background:var(--paper);color:var(--ink);font-size:12px;cursor:pointer;outline:none;transition:border-color .15s}.capture-quality:hover{border-color:var(--accent-2)}.capture-quality:focus{border-color:var(--accent)}.capture-quality:disabled{opacity:.5;cursor:default}.capture-quality-probing{flex-shrink:0;font-size:12px;color:var(--muted);padding:0 4px;letter-spacing:.05em}.capture-quality-hint{flex-shrink:0;font-size:12px;color:var(--muted);font-style:italic}.capture-quality-hint--error{color:#c05000;font-style:normal}.cap-dot{flex-shrink:0;width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:700;line-height:1}.cap-dot--idle{background:var(--paper-2)}.cap-dot--running{background:#fdefd8;color:#8a4f10}.cap-dot--ok{background:#d8eddf;color:#235c35}.cap-dot--err{background:#f5ddd8;color:#8d3f30}.cap-spinner{display:block;width:10px;height:10px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:cap-spin .7s linear infinite}@keyframes cap-spin{to{transform:rotate(360deg)}}.capture-row-action{flex-shrink:0;background:none;border:0;cursor:pointer;color:var(--muted);width:28px;height:28px;display:grid;place-items:center;border-radius:var(--r);padding:0}.capture-row-action svg{width:12px;height:12px}.capture-row-action:hover{color:var(--accent);background:var(--paper-2)}.capture-row-remove:hover{color:var(--accent)}.capture-row-error{margin:0;padding-left:28px;font-size:12px;color:var(--accent);line-height:1.45}.capture-add-row{display:flex;align-items:center;gap:7px;width:100%;background:none;border:1px dashed var(--line);border-radius:var(--r);color:var(--muted);font-size:13px;cursor:pointer;padding:7px 12px;margin-top:4px;margin-bottom:18px;transition:border-color .15s,color .15s,background .15s}.capture-add-row svg{width:13px;height:13px;flex-shrink:0}.capture-add-row:hover{border-color:var(--accent-2);color:var(--ink);background:var(--paper-2)}.toast-stack{position:fixed;bottom:24px;right:24px;z-index:9000;display:flex;flex-direction:column;gap:10px;width:340px;max-width:calc(100vw - 32px);pointer-events:none}.toast{pointer-events:auto;background:var(--paper);border:1px solid var(--line);border-radius:var(--r2);box-shadow:0 6px 24px #141d182e,0 1px 4px #141d181a;overflow:hidden;animation:toast-slide-in .22s cubic-bezier(.22,.68,0,1.2)}@keyframes toast-slide-in{0%{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}.toast--error{border-left:3px solid var(--accent)}.toast--warning{border-left:3px solid #e8a000}.toast--success{border-left:3px solid #22a855}.toast-top{display:flex;align-items:flex-start;gap:10px;padding:12px 12px 12px 14px}.toast-icon{flex-shrink:0;font-size:13px;font-weight:700;color:var(--accent);margin-top:2px}.toast--success .toast-icon{color:#22a855}.toast--warning .toast-icon{color:#e8a000}.toast-body{flex:1;min-width:0}.toast-headline{display:block;font-size:13.5px;font-weight:600;color:var(--ink);line-height:1.3}.toast-locator{display:block;font-size:12px;color:var(--muted);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast-btns{display:flex;align-items:center;gap:6px;flex-shrink:0}.toast-view-btn{background:none;border:1px solid var(--line);color:var(--link);font-size:12px;cursor:pointer;padding:3px 9px;border-radius:var(--r);white-space:nowrap}.toast-view-btn:hover{background:var(--paper-2);border-color:var(--link)}.toast-dismiss{background:none;border:0;cursor:pointer;color:var(--muted);font-size:20px;line-height:1;width:24px;height:24px;display:grid;place-items:center;border-radius:var(--r);padding:0}.toast-dismiss:hover{color:var(--ink);background:var(--paper-2)}.toast-error-detail{margin:0;padding:10px 14px 12px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11.5px;line-height:1.55;color:var(--muted);background:var(--paper-2);border-top:1px solid var(--line);white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto}.toast-warning-detail{margin:0;padding:10px 14px 12px;font-size:12px;line-height:1.55;color:var(--muted);background:var(--paper-2);border-top:1px solid var(--line);white-space:pre-wrap;word-break:break-word}.toast-ignore-btn{color:var(--muted)}.ext-toggle{flex-shrink:0;position:relative;width:44px;height:24px;border-radius:12px;background:var(--line);border:0;cursor:pointer;transition:background .18s ease;padding:0}.ext-toggle--sm{width:36px;height:20px;border-radius:10px}.ext-toggle--on{background:var(--ink)}.ext-toggle:disabled{opacity:.4;cursor:default}.ext-toggle-knob{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:var(--paper);transition:transform .18s ease;display:block;box-shadow:0 1px 3px #0003}.ext-toggle--sm .ext-toggle-knob{width:14px;height:14px;top:3px;left:3px}.ext-toggle--on .ext-toggle-knob{transform:translate(20px)}.ext-toggle--sm.ext-toggle--on .ext-toggle-knob{transform:translate(16px)}.ext-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}.ext-card{border:1px solid var(--line);border-radius:var(--r2);padding:16px 18px;background:var(--paper)}.ext-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.ext-card-info{flex:1;min-width:0}.ext-card-name{display:block;font-weight:600;font-size:14px;margin-bottom:4px}.ext-card-desc{display:block;font-size:13px;color:var(--muted);line-height:1.5}.ext-card-hint{display:block;font-size:12px;color:#e8a000;margin-top:6px}.ext-card-hint code{font-size:11.5px}.form-hint{font-size:13px;color:var(--muted);line-height:1.55;margin:0}.muted{color:var(--muted)}.admin-view{padding:22px}.admin-list{display:grid;gap:10px;max-width:780px}.admin-archive{border:1px solid var(--line);background:var(--paper-3);padding:12px;border-radius:var(--r2)}.tag-tree{padding:20px 22px;max-width:320px}.tag-tree-header{display:flex;align-items:baseline;gap:10px;margin-bottom:14px;border-bottom:1px solid var(--line-soft);padding-bottom:10px}.tag-tree-title{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.07em}.tag-tree-active{font-size:12px;color:var(--muted-2);font-style:italic}.tag-tree-list{list-style:none;margin:0;padding:0}.tag-children{padding-left:16px}.tag-node-btn{border:0;background:transparent;color:var(--link);cursor:pointer;padding:3px 0;text-align:left;font-size:13px;display:block;width:100%}.tag-node-btn:hover{text-decoration:underline}.tag-node-btn.is-active{font-weight:600;color:var(--ink)}.tag-node-row{display:flex;align-items:center;gap:4px}.tag-node-row .tag-node-btn{flex:1 1 0;min-width:0;display:flex;align-items:center;gap:4px;width:auto}.tag-node-btn .edit-icon{width:.75em;height:.75em;flex-shrink:0;vertical-align:-.1em;opacity:.35}.tag-node-btn:hover .edit-icon{opacity:.65}.tag-node-delete{flex-shrink:0;border:0;background:transparent;color:var(--muted-2);cursor:pointer;padding:0 3px;font-size:14px;line-height:1;opacity:0}.tag-node-row:hover .tag-node-delete{opacity:1}.tag-node-delete:hover{color:var(--accent)}.tag-rename-input{font-size:13px;flex:1 1 0;min-width:0;padding:2px 5px;border:1px solid var(--accent);border-radius:var(--r);background:var(--field);color:var(--ink);outline:none}.tag-node-label{flex-shrink:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag-node-count{font-size:11px;color:var(--muted-2);font-weight:400;flex-shrink:0;margin-left:3px}.tag-tree-header{align-items:center}.tag-tree-actions{margin-left:auto;display:flex;gap:4px}.tag-tree-action-btn{font-size:11px;padding:2px 7px;border:1px solid var(--line);border-radius:var(--r);background:transparent;color:var(--muted);cursor:pointer;line-height:1.5;white-space:nowrap;transition:background .1s,color .1s}.tag-tree-action-btn:hover:not(:disabled){background:var(--paper-2);color:var(--ink)}.tag-tree-action-btn:disabled{opacity:.4;cursor:default}.tag-tree-action-btn--cancel{border-color:var(--accent);color:var(--accent)}.tag-tree-action-btn--cancel:hover{background:color-mix(in srgb,var(--accent) 8%,transparent)}.tag-tree-title--move{font-style:italic;color:var(--accent-2)}.tag-node-row--move-select .tag-node-btn{cursor:crosshair;color:var(--ink)}.tag-node-row--move-select .tag-node-btn:hover{background:color-mix(in srgb,var(--link) 10%,transparent);text-decoration:none;border-radius:var(--r)}.tag-picker-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#00000073;display:flex;align-items:center;justify-content:center;padding:20px}.tag-picker-modal{background:var(--paper);border-radius:var(--r3);box-shadow:0 16px 56px #0000003d;width:300px;max-width:100%;display:flex;flex-direction:column;max-height:60vh}.tag-picker-header{display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-shrink:0;gap:8px}.tag-picker-title{flex:1;font-size:13px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag-picker-close{width:26px;height:26px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tag-picker-close:hover{background:var(--paper-2);color:var(--ink)}.tag-picker-body{flex:1;min-height:0;overflow-y:auto;padding:8px 12px 12px}.tag-picker-root-btn{display:block;width:100%;text-align:left;padding:5px 8px;margin-bottom:6px;border:1px dashed var(--line);border-radius:var(--r);background:transparent;color:var(--muted);font-size:12px;cursor:pointer}.tag-picker-root-btn:hover{background:var(--paper-2);color:var(--ink);border-color:var(--muted-2)}.tag-picker-tree{border-top:1px solid var(--line-soft);padding-top:6px;margin-top:2px}.tag-picker-node-btn{border:0;background:transparent;color:var(--link);cursor:pointer;padding:3px 6px;text-align:left;font-size:13px;display:block;width:100%;border-radius:var(--r)}.tag-picker-node-btn:hover{background:var(--paper-2);text-decoration:none}.tag-picker-empty{font-size:13px;color:var(--muted-2);margin:6px 0 0}.collections-view{padding:24px}.collections-heading{margin:0 0 16px;font-size:22px;font-family:var(--display);font-weight:600}.collections-error{color:var(--accent);font-size:13px;margin-bottom:8px}.coll-dismiss{background:none;border:none;cursor:pointer;color:var(--accent);font-size:16px;line-height:1;padding:0 4px}.collections-layout{display:grid;grid-template-columns:220px 1fr;gap:0;border:1px solid var(--line);border-radius:var(--r2);min-height:340px}.collections-sidebar{border-right:1px solid var(--line);overflow-y:auto}.coll-sidebar-row{display:flex;flex-direction:column;width:100%;padding:10px 14px;border:none;border-bottom:1px solid var(--line-soft);background:none;cursor:pointer;text-align:left;gap:2px}.coll-sidebar-row:hover{background:var(--paper-2)}.coll-sidebar-row.is-active{background:var(--paper-2);border-left:3px solid var(--accent)}.coll-row-name{font-size:14px;font-weight:600;color:var(--ink)}.coll-row-meta{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.coll-detail{padding:20px 24px;display:flex;flex-direction:column;gap:16px}.coll-detail--empty{justify-content:center;align-items:center}.coll-detail-header{display:flex;align-items:center;gap:12px}.coll-detail-name{margin:0;font-size:18px;font-family:var(--display);font-weight:600;flex:1}.coll-detail-name--editable{cursor:pointer}.coll-detail-name--editable:hover{color:var(--link)}.coll-edit-hint{font-size:14px;opacity:.5;margin-left:4px}.coll-rename-input{flex:1;font-size:16px;font-family:var(--display);border:1px solid var(--line);padding:4px 8px;background:var(--field);color:var(--ink);border-radius:var(--r)}.coll-delete-btn{border:1px solid var(--accent);background:none;color:var(--accent);padding:5px 12px;font-size:13px;cursor:pointer;border-radius:var(--r)}.coll-delete-btn:hover{background:var(--accent);color:var(--paper)}.coll-detail-vis{display:flex;align-items:center;gap:10px}.coll-vis-label{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}.coll-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:4px 8px;font-size:13px;border-radius:var(--r)}.coll-section-heading{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}.coll-entries-section{flex:1}.coll-entries-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}.coll-entry-row{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid var(--line-soft)}.coll-entry-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.coll-entry-title{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.coll-entry-kind{font-size:11px;text-transform:uppercase;letter-spacing:.04em}.coll-entry-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.coll-entry-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:3px 6px;font-size:12px;border-radius:var(--r)}.coll-entry-remove{border:none;background:none;cursor:pointer;color:var(--muted);font-size:18px;line-height:1;padding:0 4px}.coll-entry-remove:hover{color:var(--accent)}.coll-add-entry-form{border-top:1px solid var(--line-soft);padding-top:12px}.coll-add-entry-row{display:flex;gap:8px;align-items:center}.coll-add-entry-input{flex:1;border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:6px 10px;font-size:13px;min-width:0;border-radius:var(--r)}.coll-add-btn{border:none;background:var(--ink);color:var(--paper);padding:6px 14px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;border-radius:var(--r)}.coll-add-btn:hover{opacity:.85}.coll-add-btn:disabled{opacity:.45;cursor:default}.auth-loading{min-height:100vh;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:15px;background:var(--paper)}.user-menu{display:flex;align-items:center;gap:14px;padding-left:6px;flex-shrink:0}.user-name{font-size:12.5px;color:var(--muted-2);letter-spacing:.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}.logout-btn{border:1px solid rgba(255,255,255,.18);background:transparent;color:#c8bfb0;font-size:12px;padding:4px 11px;border-radius:var(--r);cursor:pointer;white-space:nowrap;transition:background .15s,color .15s}.logout-btn:hover{background:#ffffff1a;color:var(--paper)}.logout-btn:disabled{opacity:.5;cursor:default}.login-page,.setup-page{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--paper)}.login-card,.setup-card{width:100%;max-width:360px;padding:40px 36px 44px;background:var(--paper-3);border:1px solid var(--line);border-radius:var(--r3);box-shadow:0 2px 20px #141d1812}.login-brand,.setup-brand{font-family:var(--display);font-size:32px;font-weight:600;color:var(--ink);letter-spacing:-.01em;margin:0 0 6px}.login-tagline,.setup-tagline{font-size:13px;color:var(--muted);margin:0 0 28px}.login-field,.setup-field{display:flex;flex-direction:column;gap:5px;margin-bottom:14px}.login-label,.setup-label{font-size:11.5px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}.login-input,.setup-input{width:100%;border:1px solid var(--line);background:var(--field);color:var(--ink);padding:9px 11px;font-size:14px;border-radius:var(--r2);outline:none;transition:border-color .15s}.login-input:focus,.setup-input:focus{border-color:var(--accent)}.login-error,.setup-error{font-size:13px;color:var(--accent);margin:4px 0 8px}.login-submit,.setup-submit{width:100%;margin-top:8px;border:none;background:var(--top);color:var(--paper);font-size:14px;font-weight:600;padding:11px 16px;border-radius:var(--r2);cursor:pointer;letter-spacing:.02em;transition:opacity .15s}.login-submit:hover,.setup-submit:hover{opacity:.85}.login-submit:disabled,.setup-submit:disabled{opacity:.45;cursor:default}.view-tabs{display:flex;gap:2px;margin-bottom:22px;border-bottom:1px solid var(--line-soft);padding-bottom:0}.view-tab{border:none;background:none;color:var(--muted);font-size:13px;font-weight:600;padding:7px 14px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;letter-spacing:.02em;transition:color .12s;text-transform:uppercase;letter-spacing:.06em;font-size:11.5px}.view-tab:hover{color:var(--ink)}.view-tab.is-active{color:var(--ink);border-bottom-color:var(--accent)}.form-section{margin-bottom:32px}.form-section h2{font-size:14px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px}.form-row{display:flex;gap:8px;margin-bottom:10px}.form-field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}.form-label{font-size:11.5px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}.field-input{border:1px solid var(--line);background:var(--field);color:var(--ink);padding:8px 10px;font-size:13.5px;border-radius:var(--r);outline:none;transition:border-color .15s;min-width:0}.field-input:focus{border-color:var(--accent)}.field-input--flex{flex:1}.form-msg{font-size:13px;margin:6px 0}.form-msg--ok{color:var(--link)}.form-msg--err{color:var(--accent)}.btn-primary{border:none;background:var(--top);color:var(--paper);font-size:13px;font-weight:600;padding:8px 16px;border-radius:var(--r);cursor:pointer;white-space:nowrap}.btn-primary:hover{opacity:.85}.btn-primary:disabled{opacity:.45;cursor:default}.btn-ghost{border:1px solid var(--line);background:none;color:var(--ink);font-size:13px;padding:7px 14px;border-radius:var(--r);cursor:pointer}.btn-ghost:hover{background:var(--paper-2)}.btn-danger{border:1px solid var(--accent);background:none;color:var(--accent);font-size:13px;padding:7px 14px;border-radius:var(--r);cursor:pointer}.btn-danger:hover{background:var(--accent);color:var(--paper)}.admin-view h1{font-family:var(--display);font-weight:600;font-size:26px;margin:0 0 20px}.admin-table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:28px}.admin-table th{text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);padding:8px 10px;border-bottom:1px solid var(--line);background:var(--paper-2)}.admin-table th:first-child{padding-left:16px}.admin-table td{padding:9px 10px;border-bottom:1px solid var(--line-soft);color:var(--ink);vertical-align:middle}.admin-table td:first-child{padding-left:16px}.admin-table tr:hover td{background:var(--paper-2)}.admin-row-disabled td{opacity:.45}.admin-section{margin-bottom:36px;max-width:860px}.admin-section h2{font-size:14px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px}.admin-section h3{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:22px 0 10px}.admin-form{display:flex;flex-direction:column;gap:8px;max-width:480px}.admin-input{border:1px solid var(--line);background:var(--field);color:var(--ink);padding:8px 10px;font-size:13.5px;border-radius:var(--r);outline:none;transition:border-color .15s}.admin-input:focus{border-color:var(--accent)}.admin-action-btn{border:1px solid var(--line);background:none;color:var(--ink);font-size:12px;padding:4px 10px;border-radius:var(--r);cursor:pointer;white-space:nowrap}.admin-action-btn:hover{border-color:var(--accent);color:var(--accent)}.status-badge{display:inline-block;padding:2px 8px;border-radius:var(--r);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.status-active{background:#d8eddf;color:#235c35;border:1px solid #b4d9be}.status-disabled{background:#ede8e0;color:var(--muted);border:1px solid var(--line)}.run-status{display:inline-block;padding:2px 8px;border-radius:var(--r);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.run-status--completed{background:#d8eddf;color:#235c35;border:1px solid #b4d9be}.run-status--failed{background:#f5ddd8;color:#8d3f30;border:1px solid #e0b8b0}.run-status--in-progress{background:#fdefd8;color:#8a4f10;border:1px solid #f0cfa0}.run-row--failed{cursor:pointer}.run-row--failed:hover td{background:#fdf0ed!important}.run-expand-hint{margin-left:6px;font-size:10px;color:var(--accent);vertical-align:middle}.run-error-row td{padding:0!important;background:var(--paper-2)!important}.run-error-detail{margin:0;padding:10px 16px 12px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11.5px;line-height:1.55;color:var(--muted);white-space:pre-wrap;word-break:break-word;max-height:220px;overflow-y:auto}.token-banner{background:#d8eddf;border:1px solid #b4d9be;padding:12px 14px;margin-bottom:16px;font-size:13px;border-radius:var(--r2)}.token-banner code{word-break:break-all;display:block;margin-top:6px;padding:6px 8px;background:var(--field);border:1px solid var(--line);border-radius:var(--r);font-size:12px;font-family:ui-monospace,SF Mono,Menlo,monospace}.token-dismiss{margin-top:8px;font-size:12px;border:1px solid var(--line);background:none;cursor:pointer;padding:3px 8px;border-radius:var(--r);color:var(--muted)}.token-dismiss:hover{background:var(--paper-2)}.token-row{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--line);background:var(--paper-3);padding:10px 12px;border-radius:var(--r);margin-bottom:6px}.token-row-info strong{font-size:14px;display:block}.token-row-info .muted{font-size:12px;margin-top:2px}.token-create-row{display:flex;gap:8px;margin-bottom:16px}.checkbox-row{display:flex;align-items:center;gap:10px;margin-bottom:12px;cursor:pointer;font-size:14px}.checkbox-row input[type=checkbox]{width:15px;height:15px;accent-color:var(--accent)}.coll-create-details{margin-top:16px}.coll-create-details summary{font-weight:600;cursor:pointer;font-size:13px;color:var(--link);list-style:none;display:flex;align-items:center;gap:6px;padding:8px 12px;border:1px solid var(--line-soft);border-radius:var(--r2);background:var(--paper-3);width:fit-content}.coll-create-details summary:hover{background:var(--paper-2)}.coll-create-details[open] summary{border-bottom-left-radius:0;border-bottom-right-radius:0}.coll-create-form{border:1px solid var(--line-soft);border-top:none;padding:14px;border-radius:0 0 var(--r2) var(--r2);background:var(--paper-3);display:flex;flex-direction:column;gap:10px;max-width:440px}@media (max-width: 900px){.topbar{grid-template-columns:1fr auto;height:auto;min-height:58px;padding:12px}.switcher,.nav{grid-column:1 / -1}.nav{justify-content:flex-start;overflow-x:auto}.app-shell{height:auto;grid-template-columns:1fr}.context-rail{border-left:0;border-top:1px solid var(--line)}.entry-table{min-width:860px}}.rail-title--editable{cursor:pointer}.rail-title--editable:hover{opacity:.75}.rail-title--editable .edit-icon{display:inline-block;width:.75em;height:.75em;margin-left:.35em;vertical-align:middle;opacity:0;transition:opacity .1s;flex-shrink:0}.rail-title--editable:hover .edit-icon{opacity:.5}.rail-title-input{width:100%;font:inherit;font-size:inherit;font-weight:600;background:transparent;border:none;border-bottom:1px solid currentColor;color:inherit;padding:0;margin:0 0 8px;outline:none}.preview-panel{flex:1;display:flex;flex-direction:column;min-height:0}.preview-panel-loading,.preview-panel-empty{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;color:var(--muted-2);font-size:14px;text-align:center;padding:24px}.preview-video-wrap{flex:1;display:flex;align-items:center;justify-content:center;background:#0d0d0b;min-height:200px}.preview-video-wrap video{max-width:100%;max-height:100%;width:100%;display:block}.preview-iframe-wrap{flex:1;display:flex;flex-direction:column;min-height:0}.preview-iframe-toolbar{display:flex;flex-direction:column;gap:2px;padding:6px 12px;background:var(--paper-2);border-bottom:1px solid var(--line);flex-shrink:0;text-transform:none;letter-spacing:normal}.preview-iframe-toolbar span{font-size:11px;color:var(--muted-2)}.preview-iframe-toolbar a{margin-left:auto;color:var(--link);font-size:12px;text-decoration:none;padding:3px 8px;border:1px solid var(--line);border-radius:var(--r2);background:var(--field);transition:border-color .12s}.preview-iframe-toolbar a:hover{border-color:var(--accent);text-decoration:none}.preview-iframe-wrap iframe{flex:1;width:100%;border:none;background:#fff;min-height:0}.preview-image-wrap{flex:1;display:flex;align-items:center;justify-content:center;padding:20px;background:var(--paper-2)}.preview-image-wrap img{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--r3);cursor:zoom-in;display:block;box-shadow:0 4px 24px #0000001a}.preview-tweet-wrap{flex:1;min-height:0;overflow-y:auto;padding:12px 16px;background:var(--paper-2);scrollbar-width:thin;scrollbar-color:var(--line) transparent}.preview-tweet-wrap::-webkit-scrollbar{width:8px}.preview-tweet-wrap::-webkit-scrollbar-track{background:transparent}.preview-tweet-wrap::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:2px solid transparent;background-clip:padding-box}.preview-tweet-wrap::-webkit-scrollbar-thumb:hover{background:var(--muted-2);background-clip:padding-box;border:2px solid transparent}.tw-card{border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--paper);max-width:560px;margin:0 auto 12px}.tw-thread{max-width:560px;margin:0 auto}.tw-thread-item{display:flex;gap:0;margin-bottom:0}.tw-thread-connector{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:52px;padding-top:2px}.tw-thread-avatar-col{display:flex;flex-direction:column;align-items:center;flex-shrink:0}.tw-thread-line{width:2px;background:var(--line);flex:1;min-height:16px;margin:4px 0}.tw-thread-body{flex:1;min-width:0;padding:0 0 16px 12px}.tw-thread-body:last-child{padding-bottom:0}.tw-avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;flex-shrink:0;display:block}.tw-avatar-ph{width:40px;height:40px;border-radius:50%;background:var(--paper-2);border:1px solid var(--line);flex-shrink:0}.tw-header{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;margin-bottom:6px}.tw-author-name{font-size:14.5px;font-weight:700;color:var(--ink)}.tw-author-handle{font-size:13px;color:var(--muted-2)}.tw-dot{color:var(--muted-2);font-size:13px}.tw-date{font-size:13px;color:var(--muted-2)}.tw-text{font-size:15px;line-height:1.65;color:var(--ink);white-space:pre-line;word-break:break-word;margin-bottom:10px}.tw-text a{color:var(--link);text-decoration:none}.tw-text a:hover{text-decoration:underline}.tw-stats{display:flex;gap:20px;color:var(--muted-2);font-size:13px;margin-top:10px;padding-top:10px;border-top:1px solid var(--line-soft)}.tw-stats span{display:flex;align-items:center;gap:4px}.tw-article{max-width:598px;margin:0 auto}.tw-article-cover{width:100%;display:block;object-fit:cover;max-height:380px}.tw-article-meta{padding:20px 16px 0}.tw-article-title{font-size:22px;font-weight:800;letter-spacing:-.3px;color:var(--ink);line-height:1.3;margin-bottom:14px}.tw-article-author-row{display:flex;align-items:center;gap:12px;margin-bottom:14px}.tw-article-author-name{font-size:14px;font-weight:700;color:var(--ink)}.tw-article-author-sub{font-size:13px;color:var(--muted-2)}.tw-article-divider{border:none;border-top:1px solid var(--line);margin:0}.tw-article-body{padding:8px 16px 60px}.tw-b-h1{font-size:24px;font-weight:800;letter-spacing:-.3px;color:var(--ink);line-height:1.25;margin:22px 0 10px}.tw-b-h2{font-size:19px;font-weight:700;letter-spacing:-.15px;color:var(--ink);line-height:1.3;margin:20px 0 8px}.tw-b-p{font-size:16px;color:var(--ink);line-height:1.72;margin-bottom:14px}.tw-b-p:empty{display:none}.tw-b-spacer{height:6px;display:block}.tw-b-blockquote{border-left:3px solid var(--line);padding:2px 14px;margin:14px 0;color:var(--muted);font-size:16px;line-height:1.65}.tw-b-hr{border:none;border-top:1px solid var(--line);margin:24px 0}.tw-b-img{width:100%;display:block;border-radius:10px;margin:14px 0}.tw-b-ul,.tw-b-ol{margin:10px 0 14px;padding-left:26px}.tw-b-li{font-size:16px;color:var(--ink);line-height:1.65;margin-bottom:6px}.tw-b-code{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:.875em;background:var(--paper-2);padding:2px 5px;border-radius:4px;color:var(--ink)}.tw-b-pre{background:var(--paper-2);border:1px solid var(--line);border-radius:8px;padding:14px 16px;overflow-x:auto;margin:12px 0}.tw-b-pre code{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px;line-height:1.6}.tw-b-tweet-link{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin:12px 0;color:var(--muted);font-size:14px;text-decoration:none;background:var(--paper-3);transition:background .12s}.tw-b-tweet-link:hover{background:var(--field)}.tw-b-tweet-link svg{flex-shrink:0;color:var(--ink)}.audio-bar{position:fixed;bottom:0;left:0;right:0;height:72px;background:var(--paper);border-top:1px solid var(--line);display:flex;align-items:center;gap:20px;padding:0 24px;z-index:100;box-shadow:0 -2px 16px #00000014}.audio-bar-info{display:flex;align-items:center;gap:10px;min-width:0;flex:0 0 200px}.audio-bar-icon{flex-shrink:0;width:20px;height:20px;display:flex;align-items:center;justify-content:center}.audio-bar-icon svg{width:20px;height:20px}.audio-bar-title{font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;font-weight:500}.audio-bar-controls{flex:1;display:flex;align-items:center;gap:12px;min-width:0}.audio-bar-play-btn{width:38px;height:38px;border-radius:50%;background:var(--accent);color:var(--paper);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .12s,transform .1s}.audio-bar-play-btn:hover{background:var(--accent-2)}.audio-bar-play-btn:active{transform:scale(.93)}.audio-bar-play-btn svg{width:15px;height:15px}.audio-bar-seek{flex:1;min-width:0;display:flex;align-items:center;gap:8px}.audio-bar-seek input[type=range]{flex:1;accent-color:var(--accent);height:4px;cursor:pointer;min-width:0}.audio-bar-time{font-size:11.5px;color:var(--muted-2);white-space:nowrap;min-width:80px;text-align:right;font-variant-numeric:tabular-nums}.audio-bar-right{flex:0 0 auto;display:flex;align-items:center;gap:10px}.audio-bar-volume{display:flex;align-items:center;gap:6px}.audio-bar-volume svg{width:14px;height:14px;color:var(--muted-2);flex-shrink:0}.audio-bar-volume input[type=range]{width:72px;accent-color:var(--accent);cursor:pointer}.audio-bar-close{width:28px;height:28px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;padding:0;line-height:1;transition:background .12s,color .12s}.audio-bar-close:hover{background:var(--field);color:var(--ink)}.preview-modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:20px}.preview-modal{background:var(--paper);border-radius:12px;box-shadow:0 24px 80px #00000047;width:90vw;max-width:1100px;max-height:88vh;display:flex;flex-direction:column;overflow:hidden}.preview-modal--full{height:88vh}.preview-modal--full .preview-modal-body{max-height:none}.preview-modal-header{display:flex;align-items:center;padding:14px 18px;border-bottom:1px solid var(--line);flex-shrink:0;gap:12px}.preview-modal-title{flex:1;min-width:0;font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-modal-close{width:30px;height:30px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .12s,color .12s}.preview-modal-close:hover{background:var(--field);color:var(--ink)}.preview-modal-body{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;max-height:calc(88vh - 52px)}.rail-preview-btn{display:block;width:100%;padding:8px 12px;margin-bottom:16px;background:var(--accent);color:var(--paper);border:none;border-radius:var(--r3);font-size:13px;font-weight:600;cursor:pointer;text-align:center;transition:background .12s}.rail-preview-btn:hover{background:var(--accent-2)}.preview-modal-newtab{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--r2);color:var(--muted-2);text-decoration:none;font-size:16px;flex-shrink:0;transition:background .12s,color .12s}.preview-modal-newtab:hover{background:var(--field);color:var(--ink)}body.has-audio-bar{padding-bottom:56px}@keyframes skeleton-shimmer{0%{background-position:200% center}to{background-position:-200% center}}.skeleton-cell{display:inline-block;border-radius:3px;background:linear-gradient(90deg,var(--paper-2) 25%,var(--line-soft) 50%,var(--paper-2) 75%);background-size:200% 100%;animation:skeleton-shimmer 1.8s ease-in-out infinite}.skeleton-row{display:flex;align-items:center;background:var(--paper-3)}.skeleton-row>div{padding:7px 10px;flex-shrink:0;overflow:hidden}.skeleton-row .col-added{padding-left:22px}.skeleton-row>div:last-child{padding-right:22px}@media (pointer: coarse){.skeleton-row .col-added{padding-left:10px}}#entries-body>.entry-row-outer{display:block;border-bottom:none}#entries-body>.entry-row-outer>.entry-row-main{display:flex;align-items:center;cursor:default;border-bottom:1px solid var(--line-soft);padding:0;flex-shrink:unset;overflow:visible}#entries-body>.entry-row-outer>.entry-row-main>div{padding:7px 10px;flex-shrink:0;overflow:hidden}#entries-body>.entry-row-outer>.entry-row-main>div:last-child{padding-right:22px}#entries-body>.entry-row-outer>.entry-row-main .col-added{padding-left:22px}#entries-body>.entry-row-outer.is-selected{background:unset;outline:none;box-shadow:none}#entries-body>.entry-row-outer.is-multi-selected{background:unset;outline:none;box-shadow:none}#entries-body>.entry-row-outer.is-selected>.entry-row-main{background:#eee2d2;box-shadow:inset 0 0 0 2px var(--accent)}#entries-body>.entry-row-outer.is-multi-selected>.entry-row-main{background:#eee2d2;box-shadow:inset 0 0 0 2px var(--accent)}#entries-body>.entry-row-outer .url-cell:hover{overflow:visible;white-space:normal}#entries-body>.entry-row-outer>.child-entries{display:block;padding:0;flex-shrink:unset;overflow:visible;border-left:2px solid var(--line-soft);margin-left:32px}.child-entry-row{display:flex;align-items:center;cursor:default;border-bottom:1px solid var(--line-soft);opacity:.88}.child-entry-row:hover{opacity:1}.child-entry-row:last-child{border-bottom:none}.child-entry-row--light{background:#fafaf8}.child-entry-row--dark{background:#f2f0ec}.child-entry-row.is-selected,.child-entry-row.is-multi-selected{background:#eee2d2;box-shadow:inset 0 0 0 2px var(--accent);opacity:1}.child-entry-row>div{padding:6px 10px;flex-shrink:0;overflow:hidden}.child-entry-row .col-added{padding-left:22px}.child-entry-row>div:last-child{padding-right:22px}.entry-expand-btn{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex-shrink:0;padding:0;margin-right:2px;background:none;border:none;cursor:pointer;opacity:.45;color:inherit;transition:opacity .15s}.entry-expand-btn:focus{outline:none}.entry-expand-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}.entry-expand-btn:before{content:"";display:block;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid currentColor;transition:transform .15s}.entry-expand-btn:hover{opacity:1}.entry-expand-btn.is-expanded:before{transform:rotate(90deg)}.child-count-badge{display:inline-block;margin-left:5px;padding:0 5px;font-size:.72em;font-weight:600;background:color-mix(in srgb,var(--line-soft) 60%,transparent);border-radius:10px;opacity:.75;vertical-align:middle;line-height:1.6}.child-entries-loading{padding:8px 12px;font-size:.85em;opacity:.55}.capture-playlist-toggle{display:inline-flex;align-items:center;gap:4px;font-size:.75rem;color:var(--muted);background:none;border:none;cursor:pointer;padding:2px 6px}.capture-playlist-toggle:hover{color:var(--text)}.capture-playlist-toggle--left{flex-shrink:0;padding:2px 4px;font-size:.7rem}.capture-conflict-badge{font-size:.7rem;color:#c07000;background:#fff3cd;border:1px solid #e0a000;border-radius:4px;padding:1px 6px;margin-left:6px}.capture-playlist-items{border-top:1px solid var(--line-soft);max-height:320px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent}.capture-playlist-item{display:flex;align-items:center;gap:8px;padding:5px 10px 5px 14px;font-size:.8rem;border-bottom:1px solid var(--line-soft);border-left:3px solid transparent}.capture-playlist-item:last-of-type{border-bottom:none}.capture-playlist-item--conflict{border-left-color:#c07000;background:color-mix(in srgb,#c07000 6%,transparent)}.capture-playlist-item-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);opacity:.9}.capture-item-quality{font-size:.73rem;padding:2px 4px;border:1px solid var(--line);border-radius:3px;background:var(--paper-3);color:var(--text);flex-shrink:0}.capture-playlist-conflict-badge{font-size:.68rem;font-weight:600;color:#c07000;white-space:nowrap;flex-shrink:0}.capture-playlist-item-remove{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;background:none;color:var(--muted);cursor:pointer;border-radius:3px;opacity:.3;transition:opacity .1s,color .1s}.capture-playlist-item:hover .capture-playlist-item-remove,.capture-playlist-item-remove:focus-visible{opacity:1}.capture-playlist-item-remove:hover{color:#c04000;opacity:1}@media (pointer: coarse){.capture-playlist-item-remove{opacity:1}}.capture-playlist-item-remove{font-size:.75rem;line-height:1}.capture-sync-row{display:flex;align-items:center;gap:6px;padding:6px 12px;font-size:.78rem;color:var(--muted);border-top:1px solid var(--line-soft);cursor:pointer}.capture-sync-row input[type=checkbox]{cursor:pointer} diff --git a/crates/archivr-server/static/assets/index-DKombv9L.css b/crates/archivr-server/static/assets/index-DKombv9L.css deleted file mode 100644 index efc8824..0000000 --- a/crates/archivr-server/static/assets/index-DKombv9L.css +++ /dev/null @@ -1 +0,0 @@ -@import"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600&family=Spectral:wght@500;600&display=swap";:root{color-scheme:light;--ink: #20251f;--muted: #6b6f66;--muted-2: #8a8d83;--paper: #f5f0e7;--paper-2: #e9e1d2;--paper-3: #fffaf0;--line: #d2c6b5;--line-soft: #e5dccd;--accent: #8d3f30;--accent-2: #b78342;--link: #245f72;--top: #141d18;--field: #fffdf7;--r: 3px;--r2: 6px;--r3: 10px;--sans: "Helvetica Neue", Helvetica, Arial, sans-serif;--serif: "Spectral", Georgia, "Times New Roman", serif;--display: "Cormorant Garamond", Georgia, serif}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:14px;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}button,input,select{font:inherit}.workspace,.context-rail{scrollbar-width:thin;scrollbar-color:var(--line) transparent}.workspace::-webkit-scrollbar,.context-rail::-webkit-scrollbar{width:11px;height:11px}.workspace::-webkit-scrollbar-track,.context-rail::-webkit-scrollbar-track{background:transparent}.workspace::-webkit-scrollbar-thumb,.context-rail::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:3px solid transparent;background-clip:padding-box}.workspace::-webkit-scrollbar-thumb:hover,.context-rail::-webkit-scrollbar-thumb:hover{background:var(--muted-2);background-clip:padding-box;border:3px solid transparent}.topbar{height:58px;display:grid;grid-template-columns:auto auto 1fr auto auto;align-items:center;gap:26px;padding:0 22px;background:var(--top);color:#efe6d6;border-bottom:3px solid var(--accent)}.brand{font-family:var(--display);font-weight:600;font-size:30px;line-height:1;letter-spacing:.01em;color:#f4ead8}.switcher{position:relative;display:inline-flex;align-items:center}.switcher select{-moz-appearance:none;appearance:none;-webkit-appearance:none;border:1px solid rgba(247,238,223,.2);background:#f7eedf0f;color:#f1e8d8;font-family:var(--sans);font-weight:550;font-size:13.5px;letter-spacing:.04em;padding:8px 32px 8px 15px;cursor:pointer;border-radius:6px;transition:background .15s ease,border-color .15s ease}.switcher select:hover{background:#f7eedf1c;border-color:#f7eedf57}.switcher:after{content:"";position:absolute;right:15px;top:50%;width:6px;height:6px;margin-top:-5px;border-right:1.2px solid #b6ab98;border-bottom:1.2px solid #b6ab98;transform:rotate(45deg);pointer-events:none}.nav{display:flex;gap:22px;justify-content:flex-end;min-width:0}.nav-link{border:0;background:transparent;color:#cdc1ad;cursor:pointer;padding:6px 0;font-size:12.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;position:relative}.nav-link:hover,.nav-link.is-active{color:#f4ead8}.nav-link.is-active:after{content:"";position:absolute;left:0;right:0;bottom:-2px;height:1px;background:var(--accent-2)}.capture-button{display:inline-flex;align-items:center;gap:8px;position:relative;overflow:hidden;border:0;background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 88%,#fff) 0%,var(--accent) 55%);color:#f7eddd;padding:10px 18px;cursor:pointer;font-size:12px;font-weight:600;letter-spacing:.15em;text-transform:uppercase;border-radius:var(--r);box-shadow:inset 0 1px #fff5e629,0 1px 2px #141d184d;transition:filter .18s ease,box-shadow .18s ease}.capture-button:hover{filter:brightness(.93);box-shadow:inset 0 1px #fff5e61f,0 2px 6px #141d1857}.capture-button:before{content:"";position:absolute;top:0;bottom:0;left:0;width:45%;background:linear-gradient(100deg,transparent 0%,rgba(255,246,232,.45) 50%,transparent 100%);transform:translate(-180%) skew(-18deg);pointer-events:none}@media (prefers-reduced-motion: no-preference){.capture-button:hover:before{animation:cap-sheen .7s ease}}@keyframes cap-sheen{0%{transform:translate(-180%) skew(-18deg)}to{transform:translate(340%) skew(-18deg)}}.app-shell{height:calc(100vh - 58px);display:grid;grid-template-columns:minmax(0,1fr) 340px}.workspace{min-width:0;overflow:auto;display:flex;flex-direction:column}.view{display:none}.view.is-active{display:block}.toolbar{position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:16px;padding:12px 22px;background:color-mix(in srgb,var(--paper) 88%,white);border-bottom:1px solid var(--line-soft)}.search-field{position:relative;flex:1 1 auto;min-width:0;display:flex;align-items:center;height:42px;background:var(--field);border:1px solid var(--line);border-radius:20px;transition:border-color .15s ease,box-shadow .15s ease}.search-field:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 14%,transparent)}.search-field .ico{display:grid;place-items:center;width:46px;height:100%;color:var(--muted-2);flex-shrink:0}.search-field .ico svg{width:16px;height:16px}.search-input{flex:1;min-width:0;height:100%;border:0;background:transparent;color:var(--ink);padding:0 6px 0 0;font-size:14px;letter-spacing:.01em;outline:none}.search-input::placeholder{color:var(--muted-2)}.search-field .kbd{margin-right:12px;display:inline-flex;align-items:center;padding:3px 7px;font-size:11px;color:var(--muted);background:var(--paper-2);border:1px solid var(--line-soft);border-radius:var(--r);flex-shrink:0}.result-count{flex-shrink:0;font-size:13px;color:var(--muted);letter-spacing:.02em;white-space:nowrap}.result-count b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}.tag-filter-badge{display:inline-flex;align-items:center;gap:4px;background:var(--accent);color:#fff;border:0;border-radius:var(--r);font-size:12px;padding:2px 8px;cursor:pointer;margin-left:8px}.tag-filter-badge:hover{opacity:.85}.entry-table{width:100%;font-size:12.5px}.entry-header-row{display:flex;align-items:stretch;position:sticky;top:67px;z-index:2;background:color-mix(in srgb,var(--paper) 92%,white);border-bottom:1px solid var(--line-soft);color:var(--muted);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em}.entry-header-row>div{padding:9px 10px;flex-shrink:0;font-weight:600;display:flex;align-items:center}#entries-body>div{display:flex;align-items:center;cursor:default;border-bottom:1px solid var(--line-soft)}#entries-body>div>div{padding:7px 10px;flex-shrink:0;overflow:hidden}#entries-body>div:nth-child(2n){background:#f2ede5}#entries-body>div:nth-child(odd){background:var(--paper-3)}#entries-body>div.is-selected{background:#eee2d2;outline:2px solid var(--accent);outline-offset:-2px}#entries-body>div.is-multi-selected{background:#eee2d2;outline:2px solid var(--accent);outline-offset:-2px}.col-added{width:162px;color:var(--muted)}.col-title{flex:1 1 0;min-width:0;overflow:hidden;display:flex;align-items:center;gap:.42em}.col-type{width:116px}.col-size{width:100px;display:flex;flex-direction:column;justify-content:center;gap:1px}.size-total{font-variant-numeric:tabular-nums}.size-cached-pct{font-size:10px;color:var(--muted);opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.col-url{flex:0 0 30%;min-width:0;overflow:hidden}.entry-header-row .col-added,#entries-body>div .col-added{padding-left:22px}.entry-header-row>div:last-child,#entries-body>div>div:last-child{padding-right:22px}.entry-title{color:var(--link);font-weight:700;min-width:0}.entry-header-row>div{padding:9px 10px;flex-shrink:0;font-weight:600}.source-icon{display:flex;align-items:center;justify-content:center;width:1.05em;height:1.05em;flex-shrink:0}.source-icon>*{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.source-icon svg{width:100%;height:100%}.url-cell{color:#555b55;white-space:nowrap;text-overflow:ellipsis;word-break:break-all}#entries-body .url-cell:hover,#entries-body .is-selected .url-cell{overflow:visible;white-space:normal}.type-pill{display:inline-block;padding:2px 6px;background:#d8e3df;color:#275a5f;border:1px solid #bfd0ca;border-radius:var(--r)}.col-check{display:none;width:40px;flex-shrink:0;align-items:center;justify-content:center;padding:0!important;cursor:pointer}.row-checkbox{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;border:1.5px solid var(--line);cursor:pointer;display:block;width:17px;height:17px;border-radius:4px;background:var(--paper-3);transition:border-color .15s ease,background .15s ease;position:relative;flex-shrink:0}.row-checkbox.is-checked{background:var(--accent);border-color:var(--accent)}.row-checkbox.is-checked:after{content:"";position:absolute;left:4px;top:2px;width:5px;height:8px;border:2px solid #fff;border-top:none;border-left:none;transform:rotate(45deg)}@media (pointer: coarse){.col-check{display:flex}.entry-header-row .col-added,#entries-body>div .col-added{padding-left:10px}}#runs-view .entry-table{border-collapse:collapse;table-layout:auto}#runs-view .entry-table th{position:sticky;top:0;z-index:1;text-align:left;padding:10px;background:color-mix(in srgb,var(--paper) 92%,white);color:var(--muted);border-bottom:1px solid var(--line-soft);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em}#runs-view .entry-table td{padding:10px;border-bottom:1px solid var(--line-soft);vertical-align:top}#runs-view .entry-table tr:nth-child(2n) td{background:#f2ede5}#runs-view .entry-table tr:nth-child(odd) td{background:var(--paper-3)}.context-rail{border-left:1px solid var(--line);background:var(--paper);padding:20px 20px 32px;overflow:auto}.rail-eyebrow{font-size:10.5px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted-2);margin-bottom:16px}.rail-title{display:block;font-family:var(--sans);font-weight:700;font-size:15.5px;line-height:1.4;color:var(--ink);margin:0 0 16px;word-break:break-word}.url-tile{display:flex;align-items:center;gap:9px;padding:10px 12px;background:var(--paper-3);border:1px solid var(--line);border-radius:var(--r3);margin-bottom:20px;text-decoration:none}.url-tile:hover{background:var(--field);border-color:var(--accent)}.url-tile .ico{color:var(--ink);flex-shrink:0;display:grid;place-items:center}.url-tile .ico svg{width:15px;height:15px}.url-tile .u-text{min-width:0;flex:1;font-size:12.5px;color:var(--link);font-family:ui-monospace,SF Mono,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.url-tile .ext{color:var(--muted-2);flex-shrink:0}.url-tile .ext svg{width:13px;height:13px;display:block}.meta-list{margin-bottom:24px;border-top:1px solid var(--line-soft)}.meta-item{display:grid;grid-template-columns:92px 1fr;gap:14px;align-items:baseline;padding:9px 0;border-bottom:1px solid var(--line-soft)}.meta-k{font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted-2)}.meta-v{font-size:13.5px;color:var(--ink);word-break:break-word;line-height:1.4}.meta-v.mono{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px;color:var(--muted)}.rail-section{margin-bottom:24px}.rail-section-heading{display:flex;align-items:baseline;gap:8px;font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.12em;color:var(--muted-2);margin-bottom:9px;padding-bottom:8px;border-bottom:1px solid var(--line)}.rail-section-heading .num{color:var(--muted-2);font-weight:600}.artifact-list{list-style:none;margin:0;padding:0}.artifact-link{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:7px 2px;border-bottom:1px solid var(--line-soft);text-decoration:none}.artifact-link:last-child{border-bottom:0}.artifact-name{font-size:13px;color:var(--link);min-width:0;word-break:break-word}.artifact-link:hover .artifact-name{text-decoration:underline}.artifact-size{font-size:11.5px;color:var(--muted-2);flex-shrink:0;font-variant-numeric:tabular-nums}.artifact-group{list-style:none}.artifact-group-header{width:100%;background:none;border:none;border-bottom:1px solid var(--line-soft);cursor:pointer;text-align:left;color:inherit;font:inherit}.artifact-group-header:hover .artifact-name{text-decoration:underline}.artifact-group-chevron{display:inline-block;font-style:normal;transition:transform .15s ease;transform:rotate(0);transform-origin:45% 50%;margin-right:3px}.artifact-group-chevron.open{transform:rotate(90deg)}.artifact-group-body{padding-left:14px}.tags-wrap{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 12px}.tag-pill{display:inline-flex;align-items:center;gap:7px;background:var(--paper-2);border:1px solid var(--line);border-radius:var(--r);font-size:12px;padding:4px 7px 4px 10px;color:var(--ink)}.tag-pill .remove{border:0;background:transparent;cursor:pointer;color:var(--muted-2);width:15px;height:15px;display:grid;place-items:center;font-size:13px;line-height:1}.tag-pill .remove:hover{color:var(--accent)}.tags-empty{font-size:13px;color:var(--muted);margin:0 0 11px}.tag-input-wrap{display:flex;align-items:center;gap:8px;background:var(--field);border:1px solid var(--line);border-radius:var(--r2);padding:2px 2px 2px 11px;transition:border-color .15s ease,box-shadow .15s ease}.tag-input-wrap:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent)}.tag-input-wrap .hash{color:var(--muted-2);font-size:13px}.tag-input{flex:1;min-width:0;border:0;background:transparent;color:var(--ink);font-size:13px;padding:6px 0;outline:none;font-family:ui-monospace,SF Mono,Menlo,monospace}.tag-add-btn{border:0;background:var(--ink);color:var(--paper-3);padding:6px 13px;font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r);cursor:pointer;white-space:nowrap}.tag-add-btn:hover{background:#2c332b}.coll-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 13px;background:var(--field);border:1px solid var(--line);border-radius:var(--r2);margin-bottom:6px;transition:border-color .15s ease}.coll-row:hover{border-color:var(--accent)}.coll-name{font-size:13px;color:var(--ink);font-weight:500}.vis-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);background:var(--paper-2);border:1px solid var(--line-soft);border-radius:999px;padding:3px 10px}.vis-badge:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent-2)}.bulk-panel{padding-top:2px}.bulk-count{font-size:13px;color:var(--muted);margin-bottom:22px}.bulk-count-num{font-size:22px;font-weight:700;color:var(--ink);line-height:1;margin-right:4px}.bulk-coll-row{display:flex;align-items:center;gap:8px}.bulk-coll-select{flex:1;min-width:0;height:32px;padding:0 8px;border:1px solid var(--line);border-radius:var(--r);background:var(--field);color:var(--ink);font-size:13px;cursor:pointer;outline:none;transition:border-color .15s ease}.bulk-coll-select:focus{border-color:var(--accent)}.tag-add-btn:disabled{opacity:.45;cursor:default}.rail-delete-zone{margin-top:24px;padding-top:20px;border-top:1px solid var(--line-soft)}.rail-delete-btn{width:100%;padding:8px 14px;background:none;border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);color:var(--accent);border-radius:var(--r);cursor:pointer;font-size:12.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;transition:background .15s ease,border-color .15s ease}.rail-delete-btn:hover{background:color-mix(in srgb,var(--accent) 8%,transparent);border-color:var(--accent)}.rail-rearchive-btn{width:100%;padding:6px 12px;background:var(--surface-2, #2a2a2a);color:var(--text, #e0e0e0);border:1px solid var(--border, #444);border-radius:4px;cursor:pointer;font-size:13px}.rail-rearchive-btn:hover:not(:disabled){background:var(--surface-3, #333)}.rail-rearchive-btn:disabled{opacity:.6;cursor:not-allowed}.capture-dialog{border:1px solid var(--line);background:var(--paper);padding:0;width:min(540px,calc(100vw - 32px));border-radius:var(--r3);box-shadow:0 20px 60px #141d1847}.capture-dialog::backdrop{background:#141d1873}.capture-dialog-inner{padding:28px}.capture-dialog-title{margin:0;font-size:22px;font-family:var(--display);font-weight:600}.capture-label{display:block;font-size:11px;font-weight:600;margin-bottom:6px;color:var(--muted-2);text-transform:uppercase;letter-spacing:.06em}.capture-input{width:100%;height:44px;border:1px solid var(--line);background:var(--field);color:var(--ink);padding:0 14px;font-size:15px;margin-bottom:10px;border-radius:var(--r2);outline:none;transition:border-color .15s ease,box-shadow .15s ease}.capture-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 14%,transparent)}.form-field .capture-input{margin-bottom:0}.capture-error{color:var(--accent);font-size:13px;margin-bottom:10px;min-height:18px}.capture-actions{display:flex;flex-direction:column;gap:8px;margin-top:18px}.capture-submit{border:0;background:var(--ink);color:var(--paper);padding:13px 20px;border-radius:var(--r2);cursor:pointer;font-weight:600;font-size:15px;width:100%;min-width:220px;letter-spacing:.01em}.capture-submit:hover{opacity:.85}.capture-submit:disabled{opacity:.45;cursor:default}.capture-cancel{border:0;background:none;color:var(--muted);padding:7px 18px;border-radius:var(--r);cursor:pointer;font-size:13px;text-align:center;width:100%}.capture-cancel:hover{color:var(--ink);background:var(--paper-2)}.capture-advanced{margin-top:12px}.capture-advanced-toggle{display:flex;align-items:center;gap:5px;background:none;border:0;cursor:pointer;color:var(--muted);font-size:12.5px;padding:4px 0;border-radius:var(--r)}.capture-advanced-toggle:hover{color:var(--ink)}.capture-chevron{width:14px;height:14px;flex-shrink:0;transition:transform .18s ease}.capture-chevron--open{transform:rotate(180deg)}.capture-advanced-panel{margin-top:8px;padding:10px 12px;border:1px solid var(--line);border-radius:var(--r2);background:var(--paper-2)}.capture-ext-row{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:default}.capture-ext-label{flex:1;min-width:0}.capture-ext-name{display:block;font-size:13px;font-weight:600;color:var(--ink)}.capture-ext-desc{display:block;font-size:11.5px;color:var(--muted);margin-top:1px}.capture-dialog-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.capture-dialog-close{flex-shrink:0;background:none;border:0;cursor:pointer;color:var(--muted);width:28px;height:28px;display:grid;place-items:center;border-radius:var(--r);padding:0}.capture-dialog-close svg{width:14px;height:14px}.capture-dialog-close:hover{background:var(--paper-2);color:var(--ink)}.capture-rows{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.capture-row{display:flex;flex-direction:column;gap:3px}.capture-row-main{display:flex;align-items:center;gap:8px}.capture-row-main .capture-input{margin-bottom:0}.capture-quality{flex-shrink:0;height:32px;padding:0 6px;border:1px solid var(--line);border-radius:var(--r);background:var(--paper);color:var(--ink);font-size:12px;cursor:pointer;outline:none;transition:border-color .15s}.capture-quality:hover{border-color:var(--accent-2)}.capture-quality:focus{border-color:var(--accent)}.capture-quality:disabled{opacity:.5;cursor:default}.capture-quality-probing{flex-shrink:0;font-size:12px;color:var(--muted);padding:0 4px;letter-spacing:.05em}.capture-quality-hint{flex-shrink:0;font-size:12px;color:var(--muted);font-style:italic}.cap-dot{flex-shrink:0;width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:700;line-height:1}.cap-dot--idle{background:var(--paper-2)}.cap-dot--running{background:#fdefd8;color:#8a4f10}.cap-dot--ok{background:#d8eddf;color:#235c35}.cap-dot--err{background:#f5ddd8;color:#8d3f30}.cap-spinner{display:block;width:10px;height:10px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:cap-spin .7s linear infinite}@keyframes cap-spin{to{transform:rotate(360deg)}}.capture-row-action{flex-shrink:0;background:none;border:0;cursor:pointer;color:var(--muted);width:28px;height:28px;display:grid;place-items:center;border-radius:var(--r);padding:0}.capture-row-action svg{width:12px;height:12px}.capture-row-action:hover{color:var(--accent);background:var(--paper-2)}.capture-row-remove:hover{color:var(--accent)}.capture-row-error{margin:0;padding-left:28px;font-size:12px;color:var(--accent);line-height:1.45}.capture-add-row{display:flex;align-items:center;gap:7px;width:100%;background:none;border:1px dashed var(--line);border-radius:var(--r);color:var(--muted);font-size:13px;cursor:pointer;padding:7px 12px;margin-top:4px;margin-bottom:18px;transition:border-color .15s,color .15s,background .15s}.capture-add-row svg{width:13px;height:13px;flex-shrink:0}.capture-add-row:hover{border-color:var(--accent-2);color:var(--ink);background:var(--paper-2)}.toast-stack{position:fixed;bottom:24px;right:24px;z-index:9000;display:flex;flex-direction:column;gap:10px;width:340px;max-width:calc(100vw - 32px);pointer-events:none}.toast{pointer-events:auto;background:var(--paper);border:1px solid var(--line);border-radius:var(--r2);box-shadow:0 6px 24px #141d182e,0 1px 4px #141d181a;overflow:hidden;animation:toast-slide-in .22s cubic-bezier(.22,.68,0,1.2)}@keyframes toast-slide-in{0%{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}.toast--error{border-left:3px solid var(--accent)}.toast--warning{border-left:3px solid #e8a000}.toast--success{border-left:3px solid #22a855}.toast-top{display:flex;align-items:flex-start;gap:10px;padding:12px 12px 12px 14px}.toast-icon{flex-shrink:0;font-size:13px;font-weight:700;color:var(--accent);margin-top:2px}.toast--success .toast-icon{color:#22a855}.toast--warning .toast-icon{color:#e8a000}.toast-body{flex:1;min-width:0}.toast-headline{display:block;font-size:13.5px;font-weight:600;color:var(--ink);line-height:1.3}.toast-locator{display:block;font-size:12px;color:var(--muted);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast-btns{display:flex;align-items:center;gap:6px;flex-shrink:0}.toast-view-btn{background:none;border:1px solid var(--line);color:var(--link);font-size:12px;cursor:pointer;padding:3px 9px;border-radius:var(--r);white-space:nowrap}.toast-view-btn:hover{background:var(--paper-2);border-color:var(--link)}.toast-dismiss{background:none;border:0;cursor:pointer;color:var(--muted);font-size:20px;line-height:1;width:24px;height:24px;display:grid;place-items:center;border-radius:var(--r);padding:0}.toast-dismiss:hover{color:var(--ink);background:var(--paper-2)}.toast-error-detail{margin:0;padding:10px 14px 12px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11.5px;line-height:1.55;color:var(--muted);background:var(--paper-2);border-top:1px solid var(--line);white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto}.toast-warning-detail{margin:0;padding:10px 14px 12px;font-size:12px;line-height:1.55;color:var(--muted);background:var(--paper-2);border-top:1px solid var(--line);white-space:pre-wrap;word-break:break-word}.toast-ignore-btn{color:var(--muted)}.ext-toggle{flex-shrink:0;position:relative;width:44px;height:24px;border-radius:12px;background:var(--line);border:0;cursor:pointer;transition:background .18s ease;padding:0}.ext-toggle--sm{width:36px;height:20px;border-radius:10px}.ext-toggle--on{background:var(--ink)}.ext-toggle:disabled{opacity:.4;cursor:default}.ext-toggle-knob{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:var(--paper);transition:transform .18s ease;display:block;box-shadow:0 1px 3px #0003}.ext-toggle--sm .ext-toggle-knob{width:14px;height:14px;top:3px;left:3px}.ext-toggle--on .ext-toggle-knob{transform:translate(20px)}.ext-toggle--sm.ext-toggle--on .ext-toggle-knob{transform:translate(16px)}.ext-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}.ext-card{border:1px solid var(--line);border-radius:var(--r2);padding:16px 18px;background:var(--paper)}.ext-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.ext-card-info{flex:1;min-width:0}.ext-card-name{display:block;font-weight:600;font-size:14px;margin-bottom:4px}.ext-card-desc{display:block;font-size:13px;color:var(--muted);line-height:1.5}.ext-card-hint{display:block;font-size:12px;color:#e8a000;margin-top:6px}.ext-card-hint code{font-size:11.5px}.form-hint{font-size:13px;color:var(--muted);line-height:1.55;margin:0}.muted{color:var(--muted)}.admin-view{padding:22px}.admin-list{display:grid;gap:10px;max-width:780px}.admin-archive{border:1px solid var(--line);background:var(--paper-3);padding:12px;border-radius:var(--r2)}.tag-tree{padding:20px 22px;max-width:320px}.tag-tree-header{display:flex;align-items:baseline;gap:10px;margin-bottom:14px;border-bottom:1px solid var(--line-soft);padding-bottom:10px}.tag-tree-title{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.07em}.tag-tree-active{font-size:12px;color:var(--muted-2);font-style:italic}.tag-tree-list{list-style:none;margin:0;padding:0}.tag-children{padding-left:16px}.tag-node-btn{border:0;background:transparent;color:var(--link);cursor:pointer;padding:3px 0;text-align:left;font-size:13px;display:block;width:100%}.tag-node-btn:hover{text-decoration:underline}.tag-node-btn.is-active{font-weight:600;color:var(--ink)}.tag-node-row{display:flex;align-items:center;gap:4px}.tag-node-row .tag-node-btn{flex:1 1 0;min-width:0;display:flex;align-items:center;gap:4px;width:auto}.tag-node-btn .edit-icon{width:.75em;height:.75em;flex-shrink:0;vertical-align:-.1em;opacity:.35}.tag-node-btn:hover .edit-icon{opacity:.65}.tag-node-delete{flex-shrink:0;border:0;background:transparent;color:var(--muted-2);cursor:pointer;padding:0 3px;font-size:14px;line-height:1;opacity:0}.tag-node-row:hover .tag-node-delete{opacity:1}.tag-node-delete:hover{color:var(--accent)}.tag-rename-input{font-size:13px;flex:1 1 0;min-width:0;padding:2px 5px;border:1px solid var(--accent);border-radius:var(--r);background:var(--field);color:var(--ink);outline:none}.tag-node-label{flex-shrink:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag-node-count{font-size:11px;color:var(--muted-2);font-weight:400;flex-shrink:0;margin-left:3px}.tag-tree-header{align-items:center}.tag-tree-actions{margin-left:auto;display:flex;gap:4px}.tag-tree-action-btn{font-size:11px;padding:2px 7px;border:1px solid var(--line);border-radius:var(--r);background:transparent;color:var(--muted);cursor:pointer;line-height:1.5;white-space:nowrap;transition:background .1s,color .1s}.tag-tree-action-btn:hover:not(:disabled){background:var(--paper-2);color:var(--ink)}.tag-tree-action-btn:disabled{opacity:.4;cursor:default}.tag-tree-action-btn--cancel{border-color:var(--accent);color:var(--accent)}.tag-tree-action-btn--cancel:hover{background:color-mix(in srgb,var(--accent) 8%,transparent)}.tag-tree-title--move{font-style:italic;color:var(--accent-2)}.tag-node-row--move-select .tag-node-btn{cursor:crosshair;color:var(--ink)}.tag-node-row--move-select .tag-node-btn:hover{background:color-mix(in srgb,var(--link) 10%,transparent);text-decoration:none;border-radius:var(--r)}.tag-picker-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#00000073;display:flex;align-items:center;justify-content:center;padding:20px}.tag-picker-modal{background:var(--paper);border-radius:var(--r3);box-shadow:0 16px 56px #0000003d;width:300px;max-width:100%;display:flex;flex-direction:column;max-height:60vh}.tag-picker-header{display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-shrink:0;gap:8px}.tag-picker-title{flex:1;font-size:13px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag-picker-close{width:26px;height:26px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tag-picker-close:hover{background:var(--paper-2);color:var(--ink)}.tag-picker-body{flex:1;min-height:0;overflow-y:auto;padding:8px 12px 12px}.tag-picker-root-btn{display:block;width:100%;text-align:left;padding:5px 8px;margin-bottom:6px;border:1px dashed var(--line);border-radius:var(--r);background:transparent;color:var(--muted);font-size:12px;cursor:pointer}.tag-picker-root-btn:hover{background:var(--paper-2);color:var(--ink);border-color:var(--muted-2)}.tag-picker-tree{border-top:1px solid var(--line-soft);padding-top:6px;margin-top:2px}.tag-picker-node-btn{border:0;background:transparent;color:var(--link);cursor:pointer;padding:3px 6px;text-align:left;font-size:13px;display:block;width:100%;border-radius:var(--r)}.tag-picker-node-btn:hover{background:var(--paper-2);text-decoration:none}.tag-picker-empty{font-size:13px;color:var(--muted-2);margin:6px 0 0}.collections-view{padding:24px}.collections-heading{margin:0 0 16px;font-size:22px;font-family:var(--display);font-weight:600}.collections-error{color:var(--accent);font-size:13px;margin-bottom:8px}.coll-dismiss{background:none;border:none;cursor:pointer;color:var(--accent);font-size:16px;line-height:1;padding:0 4px}.collections-layout{display:grid;grid-template-columns:220px 1fr;gap:0;border:1px solid var(--line);border-radius:var(--r2);min-height:340px}.collections-sidebar{border-right:1px solid var(--line);overflow-y:auto}.coll-sidebar-row{display:flex;flex-direction:column;width:100%;padding:10px 14px;border:none;border-bottom:1px solid var(--line-soft);background:none;cursor:pointer;text-align:left;gap:2px}.coll-sidebar-row:hover{background:var(--paper-2)}.coll-sidebar-row.is-active{background:var(--paper-2);border-left:3px solid var(--accent)}.coll-row-name{font-size:14px;font-weight:600;color:var(--ink)}.coll-row-meta{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.coll-detail{padding:20px 24px;display:flex;flex-direction:column;gap:16px}.coll-detail--empty{justify-content:center;align-items:center}.coll-detail-header{display:flex;align-items:center;gap:12px}.coll-detail-name{margin:0;font-size:18px;font-family:var(--display);font-weight:600;flex:1}.coll-detail-name--editable{cursor:pointer}.coll-detail-name--editable:hover{color:var(--link)}.coll-edit-hint{font-size:14px;opacity:.5;margin-left:4px}.coll-rename-input{flex:1;font-size:16px;font-family:var(--display);border:1px solid var(--line);padding:4px 8px;background:var(--field);color:var(--ink);border-radius:var(--r)}.coll-delete-btn{border:1px solid var(--accent);background:none;color:var(--accent);padding:5px 12px;font-size:13px;cursor:pointer;border-radius:var(--r)}.coll-delete-btn:hover{background:var(--accent);color:var(--paper)}.coll-detail-vis{display:flex;align-items:center;gap:10px}.coll-vis-label{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}.coll-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:4px 8px;font-size:13px;border-radius:var(--r)}.coll-section-heading{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}.coll-entries-section{flex:1}.coll-entries-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}.coll-entry-row{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid var(--line-soft)}.coll-entry-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.coll-entry-title{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.coll-entry-kind{font-size:11px;text-transform:uppercase;letter-spacing:.04em}.coll-entry-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.coll-entry-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:3px 6px;font-size:12px;border-radius:var(--r)}.coll-entry-remove{border:none;background:none;cursor:pointer;color:var(--muted);font-size:18px;line-height:1;padding:0 4px}.coll-entry-remove:hover{color:var(--accent)}.coll-add-entry-form{border-top:1px solid var(--line-soft);padding-top:12px}.coll-add-entry-row{display:flex;gap:8px;align-items:center}.coll-add-entry-input{flex:1;border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:6px 10px;font-size:13px;min-width:0;border-radius:var(--r)}.coll-add-btn{border:none;background:var(--ink);color:var(--paper);padding:6px 14px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;border-radius:var(--r)}.coll-add-btn:hover{opacity:.85}.coll-add-btn:disabled{opacity:.45;cursor:default}.auth-loading{min-height:100vh;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:15px;background:var(--paper)}.user-menu{display:flex;align-items:center;gap:14px;padding-left:6px;flex-shrink:0}.user-name{font-size:12.5px;color:var(--muted-2);letter-spacing:.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}.logout-btn{border:1px solid rgba(255,255,255,.18);background:transparent;color:#c8bfb0;font-size:12px;padding:4px 11px;border-radius:var(--r);cursor:pointer;white-space:nowrap;transition:background .15s,color .15s}.logout-btn:hover{background:#ffffff1a;color:var(--paper)}.logout-btn:disabled{opacity:.5;cursor:default}.login-page,.setup-page{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--paper)}.login-card,.setup-card{width:100%;max-width:360px;padding:40px 36px 44px;background:var(--paper-3);border:1px solid var(--line);border-radius:var(--r3);box-shadow:0 2px 20px #141d1812}.login-brand,.setup-brand{font-family:var(--display);font-size:32px;font-weight:600;color:var(--ink);letter-spacing:-.01em;margin:0 0 6px}.login-tagline,.setup-tagline{font-size:13px;color:var(--muted);margin:0 0 28px}.login-field,.setup-field{display:flex;flex-direction:column;gap:5px;margin-bottom:14px}.login-label,.setup-label{font-size:11.5px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}.login-input,.setup-input{width:100%;border:1px solid var(--line);background:var(--field);color:var(--ink);padding:9px 11px;font-size:14px;border-radius:var(--r2);outline:none;transition:border-color .15s}.login-input:focus,.setup-input:focus{border-color:var(--accent)}.login-error,.setup-error{font-size:13px;color:var(--accent);margin:4px 0 8px}.login-submit,.setup-submit{width:100%;margin-top:8px;border:none;background:var(--top);color:var(--paper);font-size:14px;font-weight:600;padding:11px 16px;border-radius:var(--r2);cursor:pointer;letter-spacing:.02em;transition:opacity .15s}.login-submit:hover,.setup-submit:hover{opacity:.85}.login-submit:disabled,.setup-submit:disabled{opacity:.45;cursor:default}.view-tabs{display:flex;gap:2px;margin-bottom:22px;border-bottom:1px solid var(--line-soft);padding-bottom:0}.view-tab{border:none;background:none;color:var(--muted);font-size:13px;font-weight:600;padding:7px 14px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;letter-spacing:.02em;transition:color .12s;text-transform:uppercase;letter-spacing:.06em;font-size:11.5px}.view-tab:hover{color:var(--ink)}.view-tab.is-active{color:var(--ink);border-bottom-color:var(--accent)}.form-section{margin-bottom:32px}.form-section h2{font-size:14px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px}.form-row{display:flex;gap:8px;margin-bottom:10px}.form-field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}.form-label{font-size:11.5px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}.field-input{border:1px solid var(--line);background:var(--field);color:var(--ink);padding:8px 10px;font-size:13.5px;border-radius:var(--r);outline:none;transition:border-color .15s;min-width:0}.field-input:focus{border-color:var(--accent)}.field-input--flex{flex:1}.form-msg{font-size:13px;margin:6px 0}.form-msg--ok{color:var(--link)}.form-msg--err{color:var(--accent)}.btn-primary{border:none;background:var(--top);color:var(--paper);font-size:13px;font-weight:600;padding:8px 16px;border-radius:var(--r);cursor:pointer;white-space:nowrap}.btn-primary:hover{opacity:.85}.btn-primary:disabled{opacity:.45;cursor:default}.btn-ghost{border:1px solid var(--line);background:none;color:var(--ink);font-size:13px;padding:7px 14px;border-radius:var(--r);cursor:pointer}.btn-ghost:hover{background:var(--paper-2)}.btn-danger{border:1px solid var(--accent);background:none;color:var(--accent);font-size:13px;padding:7px 14px;border-radius:var(--r);cursor:pointer}.btn-danger:hover{background:var(--accent);color:var(--paper)}.admin-view h1{font-family:var(--display);font-weight:600;font-size:26px;margin:0 0 20px}.admin-table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:28px}.admin-table th{text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);padding:8px 10px;border-bottom:1px solid var(--line);background:var(--paper-2)}.admin-table th:first-child{padding-left:16px}.admin-table td{padding:9px 10px;border-bottom:1px solid var(--line-soft);color:var(--ink);vertical-align:middle}.admin-table td:first-child{padding-left:16px}.admin-table tr:hover td{background:var(--paper-2)}.admin-row-disabled td{opacity:.45}.admin-section{margin-bottom:36px;max-width:860px}.admin-section h2{font-size:14px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px}.admin-section h3{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:22px 0 10px}.admin-form{display:flex;flex-direction:column;gap:8px;max-width:480px}.admin-input{border:1px solid var(--line);background:var(--field);color:var(--ink);padding:8px 10px;font-size:13.5px;border-radius:var(--r);outline:none;transition:border-color .15s}.admin-input:focus{border-color:var(--accent)}.admin-action-btn{border:1px solid var(--line);background:none;color:var(--ink);font-size:12px;padding:4px 10px;border-radius:var(--r);cursor:pointer;white-space:nowrap}.admin-action-btn:hover{border-color:var(--accent);color:var(--accent)}.status-badge{display:inline-block;padding:2px 8px;border-radius:var(--r);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.status-active{background:#d8eddf;color:#235c35;border:1px solid #b4d9be}.status-disabled{background:#ede8e0;color:var(--muted);border:1px solid var(--line)}.run-status{display:inline-block;padding:2px 8px;border-radius:var(--r);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.run-status--completed{background:#d8eddf;color:#235c35;border:1px solid #b4d9be}.run-status--failed{background:#f5ddd8;color:#8d3f30;border:1px solid #e0b8b0}.run-status--in-progress{background:#fdefd8;color:#8a4f10;border:1px solid #f0cfa0}.run-row--failed{cursor:pointer}.run-row--failed:hover td{background:#fdf0ed!important}.run-expand-hint{margin-left:6px;font-size:10px;color:var(--accent);vertical-align:middle}.run-error-row td{padding:0!important;background:var(--paper-2)!important}.run-error-detail{margin:0;padding:10px 16px 12px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11.5px;line-height:1.55;color:var(--muted);white-space:pre-wrap;word-break:break-word;max-height:220px;overflow-y:auto}.token-banner{background:#d8eddf;border:1px solid #b4d9be;padding:12px 14px;margin-bottom:16px;font-size:13px;border-radius:var(--r2)}.token-banner code{word-break:break-all;display:block;margin-top:6px;padding:6px 8px;background:var(--field);border:1px solid var(--line);border-radius:var(--r);font-size:12px;font-family:ui-monospace,SF Mono,Menlo,monospace}.token-dismiss{margin-top:8px;font-size:12px;border:1px solid var(--line);background:none;cursor:pointer;padding:3px 8px;border-radius:var(--r);color:var(--muted)}.token-dismiss:hover{background:var(--paper-2)}.token-row{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--line);background:var(--paper-3);padding:10px 12px;border-radius:var(--r);margin-bottom:6px}.token-row-info strong{font-size:14px;display:block}.token-row-info .muted{font-size:12px;margin-top:2px}.token-create-row{display:flex;gap:8px;margin-bottom:16px}.checkbox-row{display:flex;align-items:center;gap:10px;margin-bottom:12px;cursor:pointer;font-size:14px}.checkbox-row input[type=checkbox]{width:15px;height:15px;accent-color:var(--accent)}.coll-create-details{margin-top:16px}.coll-create-details summary{font-weight:600;cursor:pointer;font-size:13px;color:var(--link);list-style:none;display:flex;align-items:center;gap:6px;padding:8px 12px;border:1px solid var(--line-soft);border-radius:var(--r2);background:var(--paper-3);width:fit-content}.coll-create-details summary:hover{background:var(--paper-2)}.coll-create-details[open] summary{border-bottom-left-radius:0;border-bottom-right-radius:0}.coll-create-form{border:1px solid var(--line-soft);border-top:none;padding:14px;border-radius:0 0 var(--r2) var(--r2);background:var(--paper-3);display:flex;flex-direction:column;gap:10px;max-width:440px}@media (max-width: 900px){.topbar{grid-template-columns:1fr auto;height:auto;min-height:58px;padding:12px}.switcher,.nav{grid-column:1 / -1}.nav{justify-content:flex-start;overflow-x:auto}.app-shell{height:auto;grid-template-columns:1fr}.context-rail{border-left:0;border-top:1px solid var(--line)}.entry-table{min-width:860px}}.rail-title--editable{cursor:pointer}.rail-title--editable:hover{opacity:.75}.rail-title--editable .edit-icon{display:inline-block;width:.75em;height:.75em;margin-left:.35em;vertical-align:middle;opacity:0;transition:opacity .1s;flex-shrink:0}.rail-title--editable:hover .edit-icon{opacity:.5}.rail-title-input{width:100%;font:inherit;font-size:inherit;font-weight:600;background:transparent;border:none;border-bottom:1px solid currentColor;color:inherit;padding:0;margin:0 0 8px;outline:none}.preview-panel{flex:1;display:flex;flex-direction:column;min-height:0}.preview-panel-loading,.preview-panel-empty{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;color:var(--muted-2);font-size:14px;text-align:center;padding:24px}.preview-video-wrap{flex:1;display:flex;align-items:center;justify-content:center;background:#0d0d0b;min-height:200px}.preview-video-wrap video{max-width:100%;max-height:100%;width:100%;display:block}.preview-iframe-wrap{flex:1;display:flex;flex-direction:column;min-height:0}.preview-iframe-toolbar{display:flex;flex-direction:column;gap:2px;padding:6px 12px;background:var(--paper-2);border-bottom:1px solid var(--line);flex-shrink:0;text-transform:none;letter-spacing:normal}.preview-iframe-toolbar span{font-size:11px;color:var(--muted-2)}.preview-iframe-toolbar a{margin-left:auto;color:var(--link);font-size:12px;text-decoration:none;padding:3px 8px;border:1px solid var(--line);border-radius:var(--r2);background:var(--field);transition:border-color .12s}.preview-iframe-toolbar a:hover{border-color:var(--accent);text-decoration:none}.preview-iframe-wrap iframe{flex:1;width:100%;border:none;background:#fff;min-height:0}.preview-image-wrap{flex:1;display:flex;align-items:center;justify-content:center;padding:20px;background:var(--paper-2)}.preview-image-wrap img{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--r3);cursor:zoom-in;display:block;box-shadow:0 4px 24px #0000001a}.preview-tweet-wrap{flex:1;min-height:0;overflow-y:auto;padding:12px 16px;background:var(--paper-2);scrollbar-width:thin;scrollbar-color:var(--line) transparent}.preview-tweet-wrap::-webkit-scrollbar{width:8px}.preview-tweet-wrap::-webkit-scrollbar-track{background:transparent}.preview-tweet-wrap::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:2px solid transparent;background-clip:padding-box}.preview-tweet-wrap::-webkit-scrollbar-thumb:hover{background:var(--muted-2);background-clip:padding-box;border:2px solid transparent}.tw-card{border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--paper);max-width:560px;margin:0 auto 12px}.tw-thread{max-width:560px;margin:0 auto}.tw-thread-item{display:flex;gap:0;margin-bottom:0}.tw-thread-connector{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:52px;padding-top:2px}.tw-thread-avatar-col{display:flex;flex-direction:column;align-items:center;flex-shrink:0}.tw-thread-line{width:2px;background:var(--line);flex:1;min-height:16px;margin:4px 0}.tw-thread-body{flex:1;min-width:0;padding:0 0 16px 12px}.tw-thread-body:last-child{padding-bottom:0}.tw-avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;flex-shrink:0;display:block}.tw-avatar-ph{width:40px;height:40px;border-radius:50%;background:var(--paper-2);border:1px solid var(--line);flex-shrink:0}.tw-header{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;margin-bottom:6px}.tw-author-name{font-size:14.5px;font-weight:700;color:var(--ink)}.tw-author-handle{font-size:13px;color:var(--muted-2)}.tw-dot{color:var(--muted-2);font-size:13px}.tw-date{font-size:13px;color:var(--muted-2)}.tw-text{font-size:15px;line-height:1.65;color:var(--ink);white-space:pre-line;word-break:break-word;margin-bottom:10px}.tw-text a{color:var(--link);text-decoration:none}.tw-text a:hover{text-decoration:underline}.tw-stats{display:flex;gap:20px;color:var(--muted-2);font-size:13px;margin-top:10px;padding-top:10px;border-top:1px solid var(--line-soft)}.tw-stats span{display:flex;align-items:center;gap:4px}.tw-article{max-width:598px;margin:0 auto}.tw-article-cover{width:100%;display:block;object-fit:cover;max-height:380px}.tw-article-meta{padding:20px 16px 0}.tw-article-title{font-size:22px;font-weight:800;letter-spacing:-.3px;color:var(--ink);line-height:1.3;margin-bottom:14px}.tw-article-author-row{display:flex;align-items:center;gap:12px;margin-bottom:14px}.tw-article-author-name{font-size:14px;font-weight:700;color:var(--ink)}.tw-article-author-sub{font-size:13px;color:var(--muted-2)}.tw-article-divider{border:none;border-top:1px solid var(--line);margin:0}.tw-article-body{padding:8px 16px 60px}.tw-b-h1{font-size:24px;font-weight:800;letter-spacing:-.3px;color:var(--ink);line-height:1.25;margin:22px 0 10px}.tw-b-h2{font-size:19px;font-weight:700;letter-spacing:-.15px;color:var(--ink);line-height:1.3;margin:20px 0 8px}.tw-b-p{font-size:16px;color:var(--ink);line-height:1.72;margin-bottom:14px}.tw-b-p:empty{display:none}.tw-b-spacer{height:6px;display:block}.tw-b-blockquote{border-left:3px solid var(--line);padding:2px 14px;margin:14px 0;color:var(--muted);font-size:16px;line-height:1.65}.tw-b-hr{border:none;border-top:1px solid var(--line);margin:24px 0}.tw-b-img{width:100%;display:block;border-radius:10px;margin:14px 0}.tw-b-ul,.tw-b-ol{margin:10px 0 14px;padding-left:26px}.tw-b-li{font-size:16px;color:var(--ink);line-height:1.65;margin-bottom:6px}.tw-b-code{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:.875em;background:var(--paper-2);padding:2px 5px;border-radius:4px;color:var(--ink)}.tw-b-pre{background:var(--paper-2);border:1px solid var(--line);border-radius:8px;padding:14px 16px;overflow-x:auto;margin:12px 0}.tw-b-pre code{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px;line-height:1.6}.tw-b-tweet-link{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin:12px 0;color:var(--muted);font-size:14px;text-decoration:none;background:var(--paper-3);transition:background .12s}.tw-b-tweet-link:hover{background:var(--field)}.tw-b-tweet-link svg{flex-shrink:0;color:var(--ink)}.audio-bar{position:fixed;bottom:0;left:0;right:0;height:72px;background:var(--paper);border-top:1px solid var(--line);display:flex;align-items:center;gap:20px;padding:0 24px;z-index:100;box-shadow:0 -2px 16px #00000014}.audio-bar-info{display:flex;align-items:center;gap:10px;min-width:0;flex:0 0 200px}.audio-bar-icon{flex-shrink:0;width:20px;height:20px;display:flex;align-items:center;justify-content:center}.audio-bar-icon svg{width:20px;height:20px}.audio-bar-title{font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;font-weight:500}.audio-bar-controls{flex:1;display:flex;align-items:center;gap:12px;min-width:0}.audio-bar-play-btn{width:38px;height:38px;border-radius:50%;background:var(--accent);color:var(--paper);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .12s,transform .1s}.audio-bar-play-btn:hover{background:var(--accent-2)}.audio-bar-play-btn:active{transform:scale(.93)}.audio-bar-play-btn svg{width:15px;height:15px}.audio-bar-seek{flex:1;min-width:0;display:flex;align-items:center;gap:8px}.audio-bar-seek input[type=range]{flex:1;accent-color:var(--accent);height:4px;cursor:pointer;min-width:0}.audio-bar-time{font-size:11.5px;color:var(--muted-2);white-space:nowrap;min-width:80px;text-align:right;font-variant-numeric:tabular-nums}.audio-bar-right{flex:0 0 auto;display:flex;align-items:center;gap:10px}.audio-bar-volume{display:flex;align-items:center;gap:6px}.audio-bar-volume svg{width:14px;height:14px;color:var(--muted-2);flex-shrink:0}.audio-bar-volume input[type=range]{width:72px;accent-color:var(--accent);cursor:pointer}.audio-bar-close{width:28px;height:28px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;padding:0;line-height:1;transition:background .12s,color .12s}.audio-bar-close:hover{background:var(--field);color:var(--ink)}.preview-modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:20px}.preview-modal{background:var(--paper);border-radius:12px;box-shadow:0 24px 80px #00000047;width:90vw;max-width:1100px;max-height:88vh;display:flex;flex-direction:column;overflow:hidden}.preview-modal--full{height:88vh}.preview-modal--full .preview-modal-body{max-height:none}.preview-modal-header{display:flex;align-items:center;padding:14px 18px;border-bottom:1px solid var(--line);flex-shrink:0;gap:12px}.preview-modal-title{flex:1;min-width:0;font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-modal-close{width:30px;height:30px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .12s,color .12s}.preview-modal-close:hover{background:var(--field);color:var(--ink)}.preview-modal-body{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;max-height:calc(88vh - 52px)}.rail-preview-btn{display:block;width:100%;padding:8px 12px;margin-bottom:16px;background:var(--accent);color:var(--paper);border:none;border-radius:var(--r3);font-size:13px;font-weight:600;cursor:pointer;text-align:center;transition:background .12s}.rail-preview-btn:hover{background:var(--accent-2)}.preview-modal-newtab{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--r2);color:var(--muted-2);text-decoration:none;font-size:16px;flex-shrink:0;transition:background .12s,color .12s}.preview-modal-newtab:hover{background:var(--field);color:var(--ink)}body.has-audio-bar{padding-bottom:56px}@keyframes skeleton-shimmer{0%{background-position:200% center}to{background-position:-200% center}}.skeleton-cell{display:inline-block;border-radius:3px;background:linear-gradient(90deg,var(--paper-2) 25%,var(--line-soft) 50%,var(--paper-2) 75%);background-size:200% 100%;animation:skeleton-shimmer 1.8s ease-in-out infinite}.skeleton-row{display:flex;align-items:center;background:var(--paper-3)}.skeleton-row>div{padding:7px 10px;flex-shrink:0;overflow:hidden}.skeleton-row .col-added{padding-left:22px}.skeleton-row>div:last-child{padding-right:22px}@media (pointer: coarse){.skeleton-row .col-added{padding-left:10px}} diff --git a/crates/archivr-server/static/assets/index-DLdY9nrw.css b/crates/archivr-server/static/assets/index-DLdY9nrw.css deleted file mode 100644 index a2a5f32..0000000 --- a/crates/archivr-server/static/assets/index-DLdY9nrw.css +++ /dev/null @@ -1 +0,0 @@ -@import"https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600&family=Spectral:wght@500;600&display=swap";:root{color-scheme:light;--ink: #20251f;--muted: #6b6f66;--muted-2: #8a8d83;--paper: #f5f0e7;--paper-2: #e9e1d2;--paper-3: #fffaf0;--line: #d2c6b5;--line-soft: #e5dccd;--accent: #8d3f30;--accent-2: #b78342;--link: #245f72;--top: #141d18;--field: #fffdf7;--r: 3px;--r2: 6px;--r3: 10px;--sans: "Helvetica Neue", Helvetica, Arial, sans-serif;--serif: "Spectral", Georgia, "Times New Roman", serif;--display: "Cormorant Garamond", Georgia, serif}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:14px;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}button,input,select{font:inherit}.workspace,.context-rail{scrollbar-width:thin;scrollbar-color:var(--line) transparent}.workspace::-webkit-scrollbar,.context-rail::-webkit-scrollbar{width:11px;height:11px}.workspace::-webkit-scrollbar-track,.context-rail::-webkit-scrollbar-track{background:transparent}.workspace::-webkit-scrollbar-thumb,.context-rail::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:3px solid transparent;background-clip:padding-box}.workspace::-webkit-scrollbar-thumb:hover,.context-rail::-webkit-scrollbar-thumb:hover{background:var(--muted-2);background-clip:padding-box;border:3px solid transparent}.topbar{height:58px;display:grid;grid-template-columns:auto auto 1fr auto auto;align-items:center;gap:26px;padding:0 22px;background:var(--top);color:#efe6d6;border-bottom:3px solid var(--accent)}.brand{font-family:var(--display);font-weight:600;font-size:30px;line-height:1;letter-spacing:.01em;color:#f4ead8}.switcher{position:relative;display:inline-flex;align-items:center}.switcher select{-moz-appearance:none;appearance:none;-webkit-appearance:none;border:1px solid rgba(247,238,223,.2);background:#f7eedf0f;color:#f1e8d8;font-family:var(--sans);font-weight:550;font-size:13.5px;letter-spacing:.04em;padding:8px 32px 8px 15px;cursor:pointer;border-radius:6px;transition:background .15s ease,border-color .15s ease}.switcher select:hover{background:#f7eedf1c;border-color:#f7eedf57}.switcher:after{content:"";position:absolute;right:15px;top:50%;width:6px;height:6px;margin-top:-5px;border-right:1.2px solid #b6ab98;border-bottom:1.2px solid #b6ab98;transform:rotate(45deg);pointer-events:none}.nav{display:flex;gap:22px;justify-content:flex-end;min-width:0}.nav-link{border:0;background:transparent;color:#cdc1ad;cursor:pointer;padding:6px 0;font-size:12.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;position:relative}.nav-link:hover,.nav-link.is-active{color:#f4ead8}.nav-link.is-active:after{content:"";position:absolute;left:0;right:0;bottom:-2px;height:1px;background:var(--accent-2)}.capture-button{display:inline-flex;align-items:center;gap:8px;position:relative;overflow:hidden;border:0;background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 88%,#fff) 0%,var(--accent) 55%);color:#f7eddd;padding:10px 18px;cursor:pointer;font-size:12px;font-weight:600;letter-spacing:.15em;text-transform:uppercase;border-radius:var(--r);box-shadow:inset 0 1px #fff5e629,0 1px 2px #141d184d;transition:filter .18s ease,box-shadow .18s ease}.capture-button:hover{filter:brightness(.93);box-shadow:inset 0 1px #fff5e61f,0 2px 6px #141d1857}.capture-button:before{content:"";position:absolute;top:0;bottom:0;left:0;width:45%;background:linear-gradient(100deg,transparent 0%,rgba(255,246,232,.45) 50%,transparent 100%);transform:translate(-180%) skew(-18deg);pointer-events:none}@media (prefers-reduced-motion: no-preference){.capture-button:hover:before{animation:cap-sheen .7s ease}}@keyframes cap-sheen{0%{transform:translate(-180%) skew(-18deg)}to{transform:translate(340%) skew(-18deg)}}.app-shell{height:calc(100vh - 58px);display:grid;grid-template-columns:minmax(0,1fr) 340px}.workspace{min-width:0;overflow:auto;display:flex;flex-direction:column}.view{display:none}.view.is-active{display:block}.toolbar{position:sticky;top:0;z-index:3;display:flex;align-items:center;gap:16px;padding:12px 22px;background:color-mix(in srgb,var(--paper) 88%,white);border-bottom:1px solid var(--line-soft)}.search-field{position:relative;flex:1 1 auto;min-width:0;display:flex;align-items:center;height:42px;background:var(--field);border:1px solid var(--line);border-radius:20px;transition:border-color .15s ease,box-shadow .15s ease}.search-field:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 14%,transparent)}.search-field .ico{display:grid;place-items:center;width:46px;height:100%;color:var(--muted-2);flex-shrink:0}.search-field .ico svg{width:16px;height:16px}.search-input{flex:1;min-width:0;height:100%;border:0;background:transparent;color:var(--ink);padding:0 6px 0 0;font-size:14px;letter-spacing:.01em;outline:none}.search-input::placeholder{color:var(--muted-2)}.search-field .kbd{margin-right:12px;display:inline-flex;align-items:center;padding:3px 7px;font-size:11px;color:var(--muted);background:var(--paper-2);border:1px solid var(--line-soft);border-radius:var(--r);flex-shrink:0}.result-count{flex-shrink:0;font-size:13px;color:var(--muted);letter-spacing:.02em;white-space:nowrap}.result-count b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}.tag-filter-badge{display:inline-flex;align-items:center;gap:4px;background:var(--accent);color:#fff;border:0;border-radius:var(--r);font-size:12px;padding:2px 8px;cursor:pointer;margin-left:8px}.tag-filter-badge:hover{opacity:.85}.entry-table{width:100%;font-size:12.5px}.entry-header-row{display:flex;align-items:stretch;position:sticky;top:67px;z-index:2;background:color-mix(in srgb,var(--paper) 92%,white);border-bottom:1px solid var(--line-soft);color:var(--muted);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em}.entry-header-row>div{padding:9px 10px;flex-shrink:0;font-weight:600;display:flex;align-items:center}#entries-body>div{display:flex;align-items:center;cursor:default;border-bottom:1px solid var(--line-soft)}#entries-body>div>div{padding:7px 10px;flex-shrink:0;overflow:hidden}#entries-body>div:nth-child(2n){background:#f2ede5}#entries-body>div:nth-child(odd){background:var(--paper-3)}#entries-body>div.is-selected{background:#eee2d2;outline:2px solid var(--accent);outline-offset:-2px}#entries-body>div.is-multi-selected{background:#eee2d2;outline:2px solid var(--accent);outline-offset:-2px}.col-added{width:162px;color:var(--muted)}.col-title{flex:1 1 0;min-width:0;overflow:hidden;display:flex;align-items:center;gap:.42em}.col-type{width:116px}.col-size{width:100px;display:flex;flex-direction:column;justify-content:center;gap:1px}.size-total{font-variant-numeric:tabular-nums}.size-cached-pct{font-size:10px;color:var(--muted);opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.col-url{flex:0 0 30%;min-width:0;overflow:hidden}.entry-header-row .col-added,#entries-body>div .col-added{padding-left:22px}.entry-header-row>div:last-child,#entries-body>div>div:last-child{padding-right:22px}.entry-title{color:var(--link);font-weight:700;min-width:0}.entry-header-row>div{padding:9px 10px;flex-shrink:0;font-weight:600}.source-icon{display:flex;align-items:center;justify-content:center;width:1.05em;height:1.05em;flex-shrink:0}.source-icon>*{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.source-icon svg{width:100%;height:100%}.url-cell{color:#555b55;white-space:nowrap;text-overflow:ellipsis;word-break:break-all}#entries-body .url-cell:hover,#entries-body .is-selected .url-cell{overflow:visible;white-space:normal}.type-pill{display:inline-block;padding:2px 6px;background:#d8e3df;color:#275a5f;border:1px solid #bfd0ca;border-radius:var(--r)}.col-check{display:none;width:40px;flex-shrink:0;align-items:center;justify-content:center;padding:0!important;cursor:pointer}.row-checkbox{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;border:1.5px solid var(--line);cursor:pointer;display:block;width:17px;height:17px;border-radius:4px;background:var(--paper-3);transition:border-color .15s ease,background .15s ease;position:relative;flex-shrink:0}.row-checkbox.is-checked{background:var(--accent);border-color:var(--accent)}.row-checkbox.is-checked:after{content:"";position:absolute;left:4px;top:2px;width:5px;height:8px;border:2px solid #fff;border-top:none;border-left:none;transform:rotate(45deg)}@media (pointer: coarse){.col-check{display:flex}.entry-header-row .col-added,#entries-body>div .col-added{padding-left:10px}}#runs-view .entry-table{border-collapse:collapse;table-layout:auto}#runs-view .entry-table th{position:sticky;top:0;z-index:1;text-align:left;padding:10px;background:color-mix(in srgb,var(--paper) 92%,white);color:var(--muted);border-bottom:1px solid var(--line-soft);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em}#runs-view .entry-table td{padding:10px;border-bottom:1px solid var(--line-soft);vertical-align:top}#runs-view .entry-table tr:nth-child(2n) td{background:#f2ede5}#runs-view .entry-table tr:nth-child(odd) td{background:var(--paper-3)}.context-rail{border-left:1px solid var(--line);background:var(--paper);padding:20px 20px 32px;overflow:auto}.rail-eyebrow{font-size:10.5px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted-2);margin-bottom:16px}.rail-title{display:block;font-family:var(--sans);font-weight:700;font-size:15.5px;line-height:1.4;color:var(--ink);margin:0 0 16px;word-break:break-word}.url-tile{display:flex;align-items:center;gap:9px;padding:10px 12px;background:var(--paper-3);border:1px solid var(--line);border-radius:var(--r3);margin-bottom:20px;text-decoration:none}.url-tile:hover{background:var(--field);border-color:var(--accent)}.url-tile .ico{color:var(--ink);flex-shrink:0;display:grid;place-items:center}.url-tile .ico svg{width:15px;height:15px}.url-tile .u-text{min-width:0;flex:1;font-size:12.5px;color:var(--link);font-family:ui-monospace,SF Mono,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.url-tile .ext{color:var(--muted-2);flex-shrink:0}.url-tile .ext svg{width:13px;height:13px;display:block}.meta-list{margin-bottom:24px;border-top:1px solid var(--line-soft)}.meta-item{display:grid;grid-template-columns:92px 1fr;gap:14px;align-items:baseline;padding:9px 0;border-bottom:1px solid var(--line-soft)}.meta-k{font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted-2)}.meta-v{font-size:13.5px;color:var(--ink);word-break:break-word;line-height:1.4}.meta-v.mono{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px;color:var(--muted)}.rail-section{margin-bottom:24px}.rail-section-heading{display:flex;align-items:baseline;gap:8px;font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.12em;color:var(--muted-2);margin-bottom:9px;padding-bottom:8px;border-bottom:1px solid var(--line)}.rail-section-heading .num{color:var(--muted-2);font-weight:600}.artifact-list{list-style:none;margin:0;padding:0}.artifact-link{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:7px 2px;border-bottom:1px solid var(--line-soft);text-decoration:none}.artifact-link:last-child{border-bottom:0}.artifact-name{font-size:13px;color:var(--link);min-width:0;word-break:break-word}.artifact-link:hover .artifact-name{text-decoration:underline}.artifact-size{font-size:11.5px;color:var(--muted-2);flex-shrink:0;font-variant-numeric:tabular-nums}.artifact-group{list-style:none}.artifact-group-header{width:100%;background:none;border:none;border-bottom:1px solid var(--line-soft);cursor:pointer;text-align:left;color:inherit;font:inherit}.artifact-group-header:hover .artifact-name{text-decoration:underline}.artifact-group-chevron{display:inline-block;font-style:normal;transition:transform .15s ease;transform:rotate(0);transform-origin:45% 50%;margin-right:3px}.artifact-group-chevron.open{transform:rotate(90deg)}.artifact-group-body{padding-left:14px}.tags-wrap{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 12px}.tag-pill{display:inline-flex;align-items:center;gap:7px;background:var(--paper-2);border:1px solid var(--line);border-radius:var(--r);font-size:12px;padding:4px 7px 4px 10px;color:var(--ink)}.tag-pill .remove{border:0;background:transparent;cursor:pointer;color:var(--muted-2);width:15px;height:15px;display:grid;place-items:center;font-size:13px;line-height:1}.tag-pill .remove:hover{color:var(--accent)}.tags-empty{font-size:13px;color:var(--muted);margin:0 0 11px}.tag-input-wrap{display:flex;align-items:center;gap:8px;background:var(--field);border:1px solid var(--line);border-radius:var(--r2);padding:2px 2px 2px 11px;transition:border-color .15s ease,box-shadow .15s ease}.tag-input-wrap:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent)}.tag-input-wrap .hash{color:var(--muted-2);font-size:13px}.tag-input{flex:1;min-width:0;border:0;background:transparent;color:var(--ink);font-size:13px;padding:6px 0;outline:none;font-family:ui-monospace,SF Mono,Menlo,monospace}.tag-add-btn{border:0;background:var(--ink);color:var(--paper-3);padding:6px 13px;font-size:11.5px;letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r);cursor:pointer;white-space:nowrap}.tag-add-btn:hover{background:#2c332b}.coll-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 13px;background:var(--field);border:1px solid var(--line);border-radius:var(--r2);margin-bottom:6px;transition:border-color .15s ease}.coll-row:hover{border-color:var(--accent)}.coll-name{font-size:13px;color:var(--ink);font-weight:500}.vis-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);background:var(--paper-2);border:1px solid var(--line-soft);border-radius:999px;padding:3px 10px}.vis-badge:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent-2)}.bulk-panel{padding-top:2px}.bulk-count{font-size:13px;color:var(--muted);margin-bottom:22px}.bulk-count-num{font-size:22px;font-weight:700;color:var(--ink);line-height:1;margin-right:4px}.bulk-coll-row{display:flex;align-items:center;gap:8px}.bulk-coll-select{flex:1;min-width:0;height:32px;padding:0 8px;border:1px solid var(--line);border-radius:var(--r);background:var(--field);color:var(--ink);font-size:13px;cursor:pointer;outline:none;transition:border-color .15s ease}.bulk-coll-select:focus{border-color:var(--accent)}.tag-add-btn:disabled{opacity:.45;cursor:default}.rail-delete-zone{margin-top:24px;padding-top:20px;border-top:1px solid var(--line-soft)}.rail-delete-btn{width:100%;padding:8px 14px;background:none;border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);color:var(--accent);border-radius:var(--r);cursor:pointer;font-size:12.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;transition:background .15s ease,border-color .15s ease}.rail-delete-btn:hover{background:color-mix(in srgb,var(--accent) 8%,transparent);border-color:var(--accent)}.rail-rearchive-btn{width:100%;padding:6px 12px;background:var(--surface-2, #2a2a2a);color:var(--text, #e0e0e0);border:1px solid var(--border, #444);border-radius:4px;cursor:pointer;font-size:13px}.rail-rearchive-btn:hover:not(:disabled){background:var(--surface-3, #333)}.rail-rearchive-btn:disabled{opacity:.6;cursor:not-allowed}.capture-dialog{border:1px solid var(--line);background:var(--paper);padding:0;width:min(540px,calc(100vw - 32px));border-radius:var(--r3);box-shadow:0 20px 60px #141d1847}.capture-dialog::backdrop{background:#141d1873}.capture-dialog-inner{padding:28px}.capture-dialog-title{margin:0;font-size:22px;font-family:var(--display);font-weight:600}.capture-label{display:block;font-size:11px;font-weight:600;margin-bottom:6px;color:var(--muted-2);text-transform:uppercase;letter-spacing:.06em}.capture-input{width:100%;height:44px;border:1px solid var(--line);background:var(--field);color:var(--ink);padding:0 14px;font-size:15px;margin-bottom:10px;border-radius:var(--r2);outline:none;transition:border-color .15s ease,box-shadow .15s ease}.capture-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 14%,transparent)}.form-field .capture-input{margin-bottom:0}.capture-error{color:var(--accent);font-size:13px;margin-bottom:10px;min-height:18px}.capture-actions{display:flex;flex-direction:column;gap:8px;margin-top:18px}.capture-submit{border:0;background:var(--ink);color:var(--paper);padding:13px 20px;border-radius:var(--r2);cursor:pointer;font-weight:600;font-size:15px;width:100%;min-width:220px;letter-spacing:.01em}.capture-submit:hover{opacity:.85}.capture-submit:disabled{opacity:.45;cursor:default}.capture-cancel{border:0;background:none;color:var(--muted);padding:7px 18px;border-radius:var(--r);cursor:pointer;font-size:13px;text-align:center;width:100%}.capture-cancel:hover{color:var(--ink);background:var(--paper-2)}.capture-advanced{margin-top:12px}.capture-advanced-toggle{display:flex;align-items:center;gap:5px;background:none;border:0;cursor:pointer;color:var(--muted);font-size:12.5px;padding:4px 0;border-radius:var(--r)}.capture-advanced-toggle:hover{color:var(--ink)}.capture-chevron{width:14px;height:14px;flex-shrink:0;transition:transform .18s ease}.capture-chevron--open{transform:rotate(180deg)}.capture-advanced-panel{margin-top:8px;padding:10px 12px;border:1px solid var(--line);border-radius:var(--r2);background:var(--paper-2)}.capture-ext-row{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:default}.capture-ext-label{flex:1;min-width:0}.capture-ext-name{display:block;font-size:13px;font-weight:600;color:var(--ink)}.capture-ext-desc{display:block;font-size:11.5px;color:var(--muted);margin-top:1px}.capture-dialog-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.capture-dialog-close{flex-shrink:0;background:none;border:0;cursor:pointer;color:var(--muted);width:28px;height:28px;display:grid;place-items:center;border-radius:var(--r);padding:0}.capture-dialog-close svg{width:14px;height:14px}.capture-dialog-close:hover{background:var(--paper-2);color:var(--ink)}.capture-rows{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.capture-row{display:flex;flex-direction:column;gap:3px}.capture-row-main{display:flex;align-items:center;gap:8px}.capture-row-main .capture-input{margin-bottom:0}.capture-quality{flex-shrink:0;height:32px;padding:0 6px;border:1px solid var(--line);border-radius:var(--r);background:var(--paper);color:var(--ink);font-size:12px;cursor:pointer;outline:none;transition:border-color .15s}.capture-quality:hover{border-color:var(--accent-2)}.capture-quality:focus{border-color:var(--accent)}.capture-quality:disabled{opacity:.5;cursor:default}.capture-quality-probing{flex-shrink:0;font-size:12px;color:var(--muted);padding:0 4px;letter-spacing:.05em}.capture-quality-hint{flex-shrink:0;font-size:12px;color:var(--muted);font-style:italic}.cap-dot{flex-shrink:0;width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:700;line-height:1}.cap-dot--idle{background:var(--paper-2)}.cap-dot--running{background:#fdefd8;color:#8a4f10}.cap-dot--ok{background:#d8eddf;color:#235c35}.cap-dot--err{background:#f5ddd8;color:#8d3f30}.cap-spinner{display:block;width:10px;height:10px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:cap-spin .7s linear infinite}@keyframes cap-spin{to{transform:rotate(360deg)}}.capture-row-action{flex-shrink:0;background:none;border:0;cursor:pointer;color:var(--muted);width:28px;height:28px;display:grid;place-items:center;border-radius:var(--r);padding:0}.capture-row-action svg{width:12px;height:12px}.capture-row-action:hover{color:var(--accent);background:var(--paper-2)}.capture-row-remove:hover{color:var(--accent)}.capture-row-error{margin:0;padding-left:28px;font-size:12px;color:var(--accent);line-height:1.45}.capture-add-row{display:flex;align-items:center;gap:7px;width:100%;background:none;border:1px dashed var(--line);border-radius:var(--r);color:var(--muted);font-size:13px;cursor:pointer;padding:7px 12px;margin-top:4px;margin-bottom:18px;transition:border-color .15s,color .15s,background .15s}.capture-add-row svg{width:13px;height:13px;flex-shrink:0}.capture-add-row:hover{border-color:var(--accent-2);color:var(--ink);background:var(--paper-2)}.toast-stack{position:fixed;bottom:24px;right:24px;z-index:9000;display:flex;flex-direction:column;gap:10px;width:340px;max-width:calc(100vw - 32px);pointer-events:none}.toast{pointer-events:auto;background:var(--paper);border:1px solid var(--line);border-radius:var(--r2);box-shadow:0 6px 24px #141d182e,0 1px 4px #141d181a;overflow:hidden;animation:toast-slide-in .22s cubic-bezier(.22,.68,0,1.2)}@keyframes toast-slide-in{0%{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}.toast--error{border-left:3px solid var(--accent)}.toast--warning{border-left:3px solid #e8a000}.toast--success{border-left:3px solid #22a855}.toast-top{display:flex;align-items:flex-start;gap:10px;padding:12px 12px 12px 14px}.toast-icon{flex-shrink:0;font-size:13px;font-weight:700;color:var(--accent);margin-top:2px}.toast--success .toast-icon{color:#22a855}.toast--warning .toast-icon{color:#e8a000}.toast-body{flex:1;min-width:0}.toast-headline{display:block;font-size:13.5px;font-weight:600;color:var(--ink);line-height:1.3}.toast-locator{display:block;font-size:12px;color:var(--muted);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast-btns{display:flex;align-items:center;gap:6px;flex-shrink:0}.toast-view-btn{background:none;border:1px solid var(--line);color:var(--link);font-size:12px;cursor:pointer;padding:3px 9px;border-radius:var(--r);white-space:nowrap}.toast-view-btn:hover{background:var(--paper-2);border-color:var(--link)}.toast-dismiss{background:none;border:0;cursor:pointer;color:var(--muted);font-size:20px;line-height:1;width:24px;height:24px;display:grid;place-items:center;border-radius:var(--r);padding:0}.toast-dismiss:hover{color:var(--ink);background:var(--paper-2)}.toast-error-detail{margin:0;padding:10px 14px 12px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11.5px;line-height:1.55;color:var(--muted);background:var(--paper-2);border-top:1px solid var(--line);white-space:pre-wrap;word-break:break-word;max-height:200px;overflow-y:auto}.toast-warning-detail{margin:0;padding:10px 14px 12px;font-size:12px;line-height:1.55;color:var(--muted);background:var(--paper-2);border-top:1px solid var(--line);white-space:pre-wrap;word-break:break-word}.toast-ignore-btn{color:var(--muted)}.ext-toggle{flex-shrink:0;position:relative;width:44px;height:24px;border-radius:12px;background:var(--line);border:0;cursor:pointer;transition:background .18s ease;padding:0}.ext-toggle--sm{width:36px;height:20px;border-radius:10px}.ext-toggle--on{background:var(--ink)}.ext-toggle:disabled{opacity:.4;cursor:default}.ext-toggle-knob{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:var(--paper);transition:transform .18s ease;display:block;box-shadow:0 1px 3px #0003}.ext-toggle--sm .ext-toggle-knob{width:14px;height:14px;top:3px;left:3px}.ext-toggle--on .ext-toggle-knob{transform:translate(20px)}.ext-toggle--sm.ext-toggle--on .ext-toggle-knob{transform:translate(16px)}.ext-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}.ext-card{border:1px solid var(--line);border-radius:var(--r2);padding:16px 18px;background:var(--paper)}.ext-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.ext-card-info{flex:1;min-width:0}.ext-card-name{display:block;font-weight:600;font-size:14px;margin-bottom:4px}.ext-card-desc{display:block;font-size:13px;color:var(--muted);line-height:1.5}.ext-card-hint{display:block;font-size:12px;color:#e8a000;margin-top:6px}.ext-card-hint code{font-size:11.5px}.form-hint{font-size:13px;color:var(--muted);line-height:1.55;margin:0}.muted{color:var(--muted)}.admin-view{padding:22px}.admin-list{display:grid;gap:10px;max-width:780px}.admin-archive{border:1px solid var(--line);background:var(--paper-3);padding:12px;border-radius:var(--r2)}.tag-tree{padding:20px 22px;max-width:320px}.tag-tree-header{display:flex;align-items:baseline;gap:10px;margin-bottom:14px;border-bottom:1px solid var(--line-soft);padding-bottom:10px}.tag-tree-title{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.07em}.tag-tree-active{font-size:12px;color:var(--muted-2);font-style:italic}.tag-tree-list{list-style:none;margin:0;padding:0}.tag-children{padding-left:16px}.tag-node-btn{border:0;background:transparent;color:var(--link);cursor:pointer;padding:3px 0;text-align:left;font-size:13px;display:block;width:100%}.tag-node-btn:hover{text-decoration:underline}.tag-node-btn.is-active{font-weight:600;color:var(--ink)}.tag-node-row{display:flex;align-items:center;gap:4px}.tag-node-row .tag-node-btn{flex:1 1 0;min-width:0;display:flex;align-items:center;gap:4px;width:auto}.tag-node-btn .edit-icon{width:.75em;height:.75em;flex-shrink:0;vertical-align:-.1em;opacity:.35}.tag-node-btn:hover .edit-icon{opacity:.65}.tag-node-delete{flex-shrink:0;border:0;background:transparent;color:var(--muted-2);cursor:pointer;padding:0 3px;font-size:14px;line-height:1;opacity:0}.tag-node-row:hover .tag-node-delete{opacity:1}.tag-node-delete:hover{color:var(--accent)}.tag-rename-input{font-size:13px;flex:1 1 0;min-width:0;padding:2px 5px;border:1px solid var(--accent);border-radius:var(--r);background:var(--field);color:var(--ink);outline:none}.tag-node-label{flex-shrink:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag-node-count{font-size:11px;color:var(--muted-2);font-weight:400;flex-shrink:0;margin-left:3px}.tag-tree-header{align-items:center}.tag-tree-actions{margin-left:auto;display:flex;gap:4px}.tag-tree-action-btn{font-size:11px;padding:2px 7px;border:1px solid var(--line);border-radius:var(--r);background:transparent;color:var(--muted);cursor:pointer;line-height:1.5;white-space:nowrap;transition:background .1s,color .1s}.tag-tree-action-btn:hover:not(:disabled){background:var(--paper-2);color:var(--ink)}.tag-tree-action-btn:disabled{opacity:.4;cursor:default}.tag-tree-action-btn--cancel{border-color:var(--accent);color:var(--accent)}.tag-tree-action-btn--cancel:hover{background:color-mix(in srgb,var(--accent) 8%,transparent)}.tag-tree-title--move{font-style:italic;color:var(--accent-2)}.tag-node-row--move-select .tag-node-btn{cursor:crosshair;color:var(--ink)}.tag-node-row--move-select .tag-node-btn:hover{background:color-mix(in srgb,var(--link) 10%,transparent);text-decoration:none;border-radius:var(--r)}.tag-picker-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#00000073;display:flex;align-items:center;justify-content:center;padding:20px}.tag-picker-modal{background:var(--paper);border-radius:var(--r3);box-shadow:0 16px 56px #0000003d;width:300px;max-width:100%;display:flex;flex-direction:column;max-height:60vh}.tag-picker-header{display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-shrink:0;gap:8px}.tag-picker-title{flex:1;font-size:13px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag-picker-close{width:26px;height:26px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tag-picker-close:hover{background:var(--paper-2);color:var(--ink)}.tag-picker-body{flex:1;min-height:0;overflow-y:auto;padding:8px 12px 12px}.tag-picker-root-btn{display:block;width:100%;text-align:left;padding:5px 8px;margin-bottom:6px;border:1px dashed var(--line);border-radius:var(--r);background:transparent;color:var(--muted);font-size:12px;cursor:pointer}.tag-picker-root-btn:hover{background:var(--paper-2);color:var(--ink);border-color:var(--muted-2)}.tag-picker-tree{border-top:1px solid var(--line-soft);padding-top:6px;margin-top:2px}.tag-picker-node-btn{border:0;background:transparent;color:var(--link);cursor:pointer;padding:3px 6px;text-align:left;font-size:13px;display:block;width:100%;border-radius:var(--r)}.tag-picker-node-btn:hover{background:var(--paper-2);text-decoration:none}.tag-picker-empty{font-size:13px;color:var(--muted-2);margin:6px 0 0}.collections-view{padding:24px}.collections-heading{margin:0 0 16px;font-size:22px;font-family:var(--display);font-weight:600}.collections-error{color:var(--accent);font-size:13px;margin-bottom:8px}.coll-dismiss{background:none;border:none;cursor:pointer;color:var(--accent);font-size:16px;line-height:1;padding:0 4px}.collections-layout{display:grid;grid-template-columns:220px 1fr;gap:0;border:1px solid var(--line);border-radius:var(--r2);min-height:340px}.collections-sidebar{border-right:1px solid var(--line);overflow-y:auto}.coll-sidebar-row{display:flex;flex-direction:column;width:100%;padding:10px 14px;border:none;border-bottom:1px solid var(--line-soft);background:none;cursor:pointer;text-align:left;gap:2px}.coll-sidebar-row:hover{background:var(--paper-2)}.coll-sidebar-row.is-active{background:var(--paper-2);border-left:3px solid var(--accent)}.coll-row-name{font-size:14px;font-weight:600;color:var(--ink)}.coll-row-meta{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}.coll-detail{padding:20px 24px;display:flex;flex-direction:column;gap:16px}.coll-detail--empty{justify-content:center;align-items:center}.coll-detail-header{display:flex;align-items:center;gap:12px}.coll-detail-name{margin:0;font-size:18px;font-family:var(--display);font-weight:600;flex:1}.coll-detail-name--editable{cursor:pointer}.coll-detail-name--editable:hover{color:var(--link)}.coll-edit-hint{font-size:14px;opacity:.5;margin-left:4px}.coll-rename-input{flex:1;font-size:16px;font-family:var(--display);border:1px solid var(--line);padding:4px 8px;background:var(--field);color:var(--ink);border-radius:var(--r)}.coll-delete-btn{border:1px solid var(--accent);background:none;color:var(--accent);padding:5px 12px;font-size:13px;cursor:pointer;border-radius:var(--r)}.coll-delete-btn:hover{background:var(--accent);color:var(--paper)}.coll-detail-vis{display:flex;align-items:center;gap:10px}.coll-vis-label{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}.coll-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:4px 8px;font-size:13px;border-radius:var(--r)}.coll-section-heading{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}.coll-entries-section{flex:1}.coll-entries-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}.coll-entry-row{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid var(--line-soft)}.coll-entry-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.coll-entry-title{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.coll-entry-kind{font-size:11px;text-transform:uppercase;letter-spacing:.04em}.coll-entry-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.coll-entry-vis-select{border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:3px 6px;font-size:12px;border-radius:var(--r)}.coll-entry-remove{border:none;background:none;cursor:pointer;color:var(--muted);font-size:18px;line-height:1;padding:0 4px}.coll-entry-remove:hover{color:var(--accent)}.coll-add-entry-form{border-top:1px solid var(--line-soft);padding-top:12px}.coll-add-entry-row{display:flex;gap:8px;align-items:center}.coll-add-entry-input{flex:1;border:1px solid var(--line);background:var(--paper-3);color:var(--ink);padding:6px 10px;font-size:13px;min-width:0;border-radius:var(--r)}.coll-add-btn{border:none;background:var(--ink);color:var(--paper);padding:6px 14px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;border-radius:var(--r)}.coll-add-btn:hover{opacity:.85}.coll-add-btn:disabled{opacity:.45;cursor:default}.auth-loading{min-height:100vh;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:15px;background:var(--paper)}.user-menu{display:flex;align-items:center;gap:14px;padding-left:6px;flex-shrink:0}.user-name{font-size:12.5px;color:var(--muted-2);letter-spacing:.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}.logout-btn{border:1px solid rgba(255,255,255,.18);background:transparent;color:#c8bfb0;font-size:12px;padding:4px 11px;border-radius:var(--r);cursor:pointer;white-space:nowrap;transition:background .15s,color .15s}.logout-btn:hover{background:#ffffff1a;color:var(--paper)}.logout-btn:disabled{opacity:.5;cursor:default}.login-page,.setup-page{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--paper)}.login-card,.setup-card{width:100%;max-width:360px;padding:40px 36px 44px;background:var(--paper-3);border:1px solid var(--line);border-radius:var(--r3);box-shadow:0 2px 20px #141d1812}.login-brand,.setup-brand{font-family:var(--display);font-size:32px;font-weight:600;color:var(--ink);letter-spacing:-.01em;margin:0 0 6px}.login-tagline,.setup-tagline{font-size:13px;color:var(--muted);margin:0 0 28px}.login-field,.setup-field{display:flex;flex-direction:column;gap:5px;margin-bottom:14px}.login-label,.setup-label{font-size:11.5px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}.login-input,.setup-input{width:100%;border:1px solid var(--line);background:var(--field);color:var(--ink);padding:9px 11px;font-size:14px;border-radius:var(--r2);outline:none;transition:border-color .15s}.login-input:focus,.setup-input:focus{border-color:var(--accent)}.login-error,.setup-error{font-size:13px;color:var(--accent);margin:4px 0 8px}.login-submit,.setup-submit{width:100%;margin-top:8px;border:none;background:var(--top);color:var(--paper);font-size:14px;font-weight:600;padding:11px 16px;border-radius:var(--r2);cursor:pointer;letter-spacing:.02em;transition:opacity .15s}.login-submit:hover,.setup-submit:hover{opacity:.85}.login-submit:disabled,.setup-submit:disabled{opacity:.45;cursor:default}.view-tabs{display:flex;gap:2px;margin-bottom:22px;border-bottom:1px solid var(--line-soft);padding-bottom:0}.view-tab{border:none;background:none;color:var(--muted);font-size:13px;font-weight:600;padding:7px 14px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;letter-spacing:.02em;transition:color .12s;text-transform:uppercase;letter-spacing:.06em;font-size:11.5px}.view-tab:hover{color:var(--ink)}.view-tab.is-active{color:var(--ink);border-bottom-color:var(--accent)}.form-section{margin-bottom:32px}.form-section h2{font-size:14px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px}.form-row{display:flex;gap:8px;margin-bottom:10px}.form-field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}.form-label{font-size:11.5px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}.field-input{border:1px solid var(--line);background:var(--field);color:var(--ink);padding:8px 10px;font-size:13.5px;border-radius:var(--r);outline:none;transition:border-color .15s;min-width:0}.field-input:focus{border-color:var(--accent)}.field-input--flex{flex:1}.form-msg{font-size:13px;margin:6px 0}.form-msg--ok{color:var(--link)}.form-msg--err{color:var(--accent)}.btn-primary{border:none;background:var(--top);color:var(--paper);font-size:13px;font-weight:600;padding:8px 16px;border-radius:var(--r);cursor:pointer;white-space:nowrap}.btn-primary:hover{opacity:.85}.btn-primary:disabled{opacity:.45;cursor:default}.btn-ghost{border:1px solid var(--line);background:none;color:var(--ink);font-size:13px;padding:7px 14px;border-radius:var(--r);cursor:pointer}.btn-ghost:hover{background:var(--paper-2)}.btn-danger{border:1px solid var(--accent);background:none;color:var(--accent);font-size:13px;padding:7px 14px;border-radius:var(--r);cursor:pointer}.btn-danger:hover{background:var(--accent);color:var(--paper)}.admin-view h1{font-family:var(--display);font-weight:600;font-size:26px;margin:0 0 20px}.admin-table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:28px}.admin-table th{text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);padding:8px 10px;border-bottom:1px solid var(--line);background:var(--paper-2)}.admin-table th:first-child{padding-left:16px}.admin-table td{padding:9px 10px;border-bottom:1px solid var(--line-soft);color:var(--ink);vertical-align:middle}.admin-table td:first-child{padding-left:16px}.admin-table tr:hover td{background:var(--paper-2)}.admin-row-disabled td{opacity:.45}.admin-section{margin-bottom:36px;max-width:860px}.admin-section h2{font-size:14px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:0 0 14px}.admin-section h3{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:22px 0 10px}.admin-form{display:flex;flex-direction:column;gap:8px;max-width:480px}.admin-input{border:1px solid var(--line);background:var(--field);color:var(--ink);padding:8px 10px;font-size:13.5px;border-radius:var(--r);outline:none;transition:border-color .15s}.admin-input:focus{border-color:var(--accent)}.admin-action-btn{border:1px solid var(--line);background:none;color:var(--ink);font-size:12px;padding:4px 10px;border-radius:var(--r);cursor:pointer;white-space:nowrap}.admin-action-btn:hover{border-color:var(--accent);color:var(--accent)}.status-badge{display:inline-block;padding:2px 8px;border-radius:var(--r);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.status-active{background:#d8eddf;color:#235c35;border:1px solid #b4d9be}.status-disabled{background:#ede8e0;color:var(--muted);border:1px solid var(--line)}.run-status{display:inline-block;padding:2px 8px;border-radius:var(--r);font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.run-status--completed{background:#d8eddf;color:#235c35;border:1px solid #b4d9be}.run-status--failed{background:#f5ddd8;color:#8d3f30;border:1px solid #e0b8b0}.run-status--in-progress{background:#fdefd8;color:#8a4f10;border:1px solid #f0cfa0}.run-row--failed{cursor:pointer}.run-row--failed:hover td{background:#fdf0ed!important}.run-expand-hint{margin-left:6px;font-size:10px;color:var(--accent);vertical-align:middle}.run-error-row td{padding:0!important;background:var(--paper-2)!important}.run-error-detail{margin:0;padding:10px 16px 12px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11.5px;line-height:1.55;color:var(--muted);white-space:pre-wrap;word-break:break-word;max-height:220px;overflow-y:auto}.token-banner{background:#d8eddf;border:1px solid #b4d9be;padding:12px 14px;margin-bottom:16px;font-size:13px;border-radius:var(--r2)}.token-banner code{word-break:break-all;display:block;margin-top:6px;padding:6px 8px;background:var(--field);border:1px solid var(--line);border-radius:var(--r);font-size:12px;font-family:ui-monospace,SF Mono,Menlo,monospace}.token-dismiss{margin-top:8px;font-size:12px;border:1px solid var(--line);background:none;cursor:pointer;padding:3px 8px;border-radius:var(--r);color:var(--muted)}.token-dismiss:hover{background:var(--paper-2)}.token-row{display:flex;align-items:center;justify-content:space-between;border:1px solid var(--line);background:var(--paper-3);padding:10px 12px;border-radius:var(--r);margin-bottom:6px}.token-row-info strong{font-size:14px;display:block}.token-row-info .muted{font-size:12px;margin-top:2px}.token-create-row{display:flex;gap:8px;margin-bottom:16px}.checkbox-row{display:flex;align-items:center;gap:10px;margin-bottom:12px;cursor:pointer;font-size:14px}.checkbox-row input[type=checkbox]{width:15px;height:15px;accent-color:var(--accent)}.coll-create-details{margin-top:16px}.coll-create-details summary{font-weight:600;cursor:pointer;font-size:13px;color:var(--link);list-style:none;display:flex;align-items:center;gap:6px;padding:8px 12px;border:1px solid var(--line-soft);border-radius:var(--r2);background:var(--paper-3);width:fit-content}.coll-create-details summary:hover{background:var(--paper-2)}.coll-create-details[open] summary{border-bottom-left-radius:0;border-bottom-right-radius:0}.coll-create-form{border:1px solid var(--line-soft);border-top:none;padding:14px;border-radius:0 0 var(--r2) var(--r2);background:var(--paper-3);display:flex;flex-direction:column;gap:10px;max-width:440px}@media (max-width: 900px){.topbar{grid-template-columns:1fr auto;height:auto;min-height:58px;padding:12px}.switcher,.nav{grid-column:1 / -1}.nav{justify-content:flex-start;overflow-x:auto}.app-shell{height:auto;grid-template-columns:1fr}.context-rail{border-left:0;border-top:1px solid var(--line)}.entry-table{min-width:860px}}.rail-title--editable{cursor:pointer}.rail-title--editable:hover{opacity:.75}.rail-title--editable .edit-icon{display:inline-block;width:.75em;height:.75em;margin-left:.35em;vertical-align:middle;opacity:0;transition:opacity .1s;flex-shrink:0}.rail-title--editable:hover .edit-icon{opacity:.5}.rail-title-input{width:100%;font:inherit;font-size:inherit;font-weight:600;background:transparent;border:none;border-bottom:1px solid currentColor;color:inherit;padding:0;margin:0 0 8px;outline:none}.preview-panel{flex:1;display:flex;flex-direction:column;min-height:0}.preview-panel-loading,.preview-panel-empty{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;color:var(--muted-2);font-size:14px;text-align:center;padding:24px}.preview-video-wrap{flex:1;display:flex;align-items:center;justify-content:center;background:#0d0d0b;min-height:200px}.preview-video-wrap video{max-width:100%;max-height:100%;width:100%;display:block}.preview-iframe-wrap{flex:1;display:flex;flex-direction:column;min-height:0}.preview-iframe-toolbar{display:flex;flex-direction:column;gap:2px;padding:6px 12px;background:var(--paper-2);border-bottom:1px solid var(--line);flex-shrink:0;text-transform:none;letter-spacing:normal}.preview-iframe-toolbar span{font-size:11px;color:var(--muted-2)}.preview-iframe-toolbar a{margin-left:auto;color:var(--link);font-size:12px;text-decoration:none;padding:3px 8px;border:1px solid var(--line);border-radius:var(--r2);background:var(--field);transition:border-color .12s}.preview-iframe-toolbar a:hover{border-color:var(--accent);text-decoration:none}.preview-iframe-wrap iframe{flex:1;width:100%;border:none;background:#fff;min-height:0}.preview-image-wrap{flex:1;display:flex;align-items:center;justify-content:center;padding:20px;background:var(--paper-2)}.preview-image-wrap img{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--r3);cursor:zoom-in;display:block;box-shadow:0 4px 24px #0000001a}.preview-tweet-wrap{flex:1;min-height:0;overflow-y:auto;padding:12px 16px;background:var(--paper-2);scrollbar-width:thin;scrollbar-color:var(--line) transparent}.preview-tweet-wrap::-webkit-scrollbar{width:8px}.preview-tweet-wrap::-webkit-scrollbar-track{background:transparent}.preview-tweet-wrap::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px;border:2px solid transparent;background-clip:padding-box}.preview-tweet-wrap::-webkit-scrollbar-thumb:hover{background:var(--muted-2);background-clip:padding-box;border:2px solid transparent}.tw-card{border:1px solid var(--line);border-radius:12px;padding:16px;background:var(--paper);max-width:560px;margin:0 auto 12px}.tw-thread{max-width:560px;margin:0 auto}.tw-thread-item{display:flex;gap:0;margin-bottom:0}.tw-thread-connector{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:52px;padding-top:2px}.tw-thread-avatar-col{display:flex;flex-direction:column;align-items:center;flex-shrink:0}.tw-thread-line{width:2px;background:var(--line);flex:1;min-height:16px;margin:4px 0}.tw-thread-body{flex:1;min-width:0;padding:0 0 16px 12px}.tw-thread-body:last-child{padding-bottom:0}.tw-avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;flex-shrink:0;display:block}.tw-avatar-ph{width:40px;height:40px;border-radius:50%;background:var(--paper-2);border:1px solid var(--line);flex-shrink:0}.tw-header{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;margin-bottom:6px}.tw-author-name{font-size:14.5px;font-weight:700;color:var(--ink)}.tw-author-handle{font-size:13px;color:var(--muted-2)}.tw-dot{color:var(--muted-2);font-size:13px}.tw-date{font-size:13px;color:var(--muted-2)}.tw-text{font-size:15px;line-height:1.65;color:var(--ink);white-space:pre-line;word-break:break-word;margin-bottom:10px}.tw-text a{color:var(--link);text-decoration:none}.tw-text a:hover{text-decoration:underline}.tw-stats{display:flex;gap:20px;color:var(--muted-2);font-size:13px;margin-top:10px;padding-top:10px;border-top:1px solid var(--line-soft)}.tw-stats span{display:flex;align-items:center;gap:4px}.tw-article{max-width:598px;margin:0 auto}.tw-article-cover{width:100%;display:block;object-fit:cover;max-height:380px}.tw-article-meta{padding:20px 16px 0}.tw-article-title{font-size:22px;font-weight:800;letter-spacing:-.3px;color:var(--ink);line-height:1.3;margin-bottom:14px}.tw-article-author-row{display:flex;align-items:center;gap:12px;margin-bottom:14px}.tw-article-author-name{font-size:14px;font-weight:700;color:var(--ink)}.tw-article-author-sub{font-size:13px;color:var(--muted-2)}.tw-article-divider{border:none;border-top:1px solid var(--line);margin:0}.tw-article-body{padding:8px 16px 60px}.tw-b-h1{font-size:24px;font-weight:800;letter-spacing:-.3px;color:var(--ink);line-height:1.25;margin:22px 0 10px}.tw-b-h2{font-size:19px;font-weight:700;letter-spacing:-.15px;color:var(--ink);line-height:1.3;margin:20px 0 8px}.tw-b-p{font-size:16px;color:var(--ink);line-height:1.72;margin-bottom:14px}.tw-b-p:empty{display:none}.tw-b-spacer{height:6px;display:block}.tw-b-blockquote{border-left:3px solid var(--line);padding:2px 14px;margin:14px 0;color:var(--muted);font-size:16px;line-height:1.65}.tw-b-hr{border:none;border-top:1px solid var(--line);margin:24px 0}.tw-b-img{width:100%;display:block;border-radius:10px;margin:14px 0}.tw-b-ul,.tw-b-ol{margin:10px 0 14px;padding-left:26px}.tw-b-li{font-size:16px;color:var(--ink);line-height:1.65;margin-bottom:6px}.tw-b-code{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:.875em;background:var(--paper-2);padding:2px 5px;border-radius:4px;color:var(--ink)}.tw-b-pre{background:var(--paper-2);border:1px solid var(--line);border-radius:8px;padding:14px 16px;overflow-x:auto;margin:12px 0}.tw-b-pre code{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px;line-height:1.6}.tw-b-tweet-link{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin:12px 0;color:var(--muted);font-size:14px;text-decoration:none;background:var(--paper-3);transition:background .12s}.tw-b-tweet-link:hover{background:var(--field)}.tw-b-tweet-link svg{flex-shrink:0;color:var(--ink)}.audio-bar{position:fixed;bottom:0;left:0;right:0;height:72px;background:var(--paper);border-top:1px solid var(--line);display:flex;align-items:center;gap:20px;padding:0 24px;z-index:100;box-shadow:0 -2px 16px #00000014}.audio-bar-info{display:flex;align-items:center;gap:10px;min-width:0;flex:0 0 200px}.audio-bar-icon{flex-shrink:0;width:20px;height:20px;display:flex;align-items:center;justify-content:center}.audio-bar-icon svg{width:20px;height:20px}.audio-bar-title{font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;font-weight:500}.audio-bar-controls{flex:1;display:flex;align-items:center;gap:12px;min-width:0}.audio-bar-play-btn{width:38px;height:38px;border-radius:50%;background:var(--accent);color:var(--paper);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .12s,transform .1s}.audio-bar-play-btn:hover{background:var(--accent-2)}.audio-bar-play-btn:active{transform:scale(.93)}.audio-bar-play-btn svg{width:15px;height:15px}.audio-bar-seek{flex:1;min-width:0;display:flex;align-items:center;gap:8px}.audio-bar-seek input[type=range]{flex:1;accent-color:var(--accent);height:4px;cursor:pointer;min-width:0}.audio-bar-time{font-size:11.5px;color:var(--muted-2);white-space:nowrap;min-width:80px;text-align:right;font-variant-numeric:tabular-nums}.audio-bar-right{flex:0 0 auto;display:flex;align-items:center;gap:10px}.audio-bar-volume{display:flex;align-items:center;gap:6px}.audio-bar-volume svg{width:14px;height:14px;color:var(--muted-2);flex-shrink:0}.audio-bar-volume input[type=range]{width:72px;accent-color:var(--accent);cursor:pointer}.audio-bar-close{width:28px;height:28px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;padding:0;line-height:1;transition:background .12s,color .12s}.audio-bar-close:hover{background:var(--field);color:var(--ink)}.preview-modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:20px}.preview-modal{background:var(--paper);border-radius:12px;box-shadow:0 24px 80px #00000047;width:90vw;max-width:1100px;max-height:88vh;display:flex;flex-direction:column;overflow:hidden}.preview-modal--full{height:88vh}.preview-modal--full .preview-modal-body{max-height:none}.preview-modal-header{display:flex;align-items:center;padding:14px 18px;border-bottom:1px solid var(--line);flex-shrink:0;gap:12px}.preview-modal-title{flex:1;min-width:0;font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-modal-close{width:30px;height:30px;border-radius:50%;border:none;background:transparent;color:var(--muted-2);font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .12s,color .12s}.preview-modal-close:hover{background:var(--field);color:var(--ink)}.preview-modal-body{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;max-height:calc(88vh - 52px)}.rail-preview-btn{display:block;width:100%;padding:8px 12px;margin-bottom:16px;background:var(--accent);color:var(--paper);border:none;border-radius:var(--r3);font-size:13px;font-weight:600;cursor:pointer;text-align:center;transition:background .12s}.rail-preview-btn:hover{background:var(--accent-2)}.preview-modal-newtab{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--r2);color:var(--muted-2);text-decoration:none;font-size:16px;flex-shrink:0;transition:background .12s,color .12s}.preview-modal-newtab:hover{background:var(--field);color:var(--ink)}body.has-audio-bar{padding-bottom:56px} diff --git a/crates/archivr-server/static/assets/index-De3b80Fv.js b/crates/archivr-server/static/assets/index-De3b80Fv.js new file mode 100644 index 0000000..eb19449 --- /dev/null +++ b/crates/archivr-server/static/assets/index-De3b80Fv.js @@ -0,0 +1,47 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&r(a)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var ju={exports:{}},cs={},Su={exports:{}},Z={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Zr=Symbol.for("react.element"),ef=Symbol.for("react.portal"),tf=Symbol.for("react.fragment"),nf=Symbol.for("react.strict_mode"),rf=Symbol.for("react.profiler"),lf=Symbol.for("react.provider"),sf=Symbol.for("react.context"),af=Symbol.for("react.forward_ref"),of=Symbol.for("react.suspense"),uf=Symbol.for("react.memo"),cf=Symbol.for("react.lazy"),eo=Symbol.iterator;function df(e){return e===null||typeof e!="object"?null:(e=eo&&e[eo]||e["@@iterator"],typeof e=="function"?e:null)}var Nu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_u=Object.assign,Cu={};function ar(e,t,n){this.props=e,this.context=t,this.refs=Cu,this.updater=n||Nu}ar.prototype.isReactComponent={};ar.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};ar.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Eu(){}Eu.prototype=ar.prototype;function Zi(e,t,n){this.props=e,this.context=t,this.refs=Cu,this.updater=n||Nu}var ea=Zi.prototype=new Eu;ea.constructor=Zi;_u(ea,ar.prototype);ea.isPureReactComponent=!0;var to=Array.isArray,Tu=Object.prototype.hasOwnProperty,ta={current:null},bu={key:!0,ref:!0,__self:!0,__source:!0};function Pu(e,t,n){var r,l={},i=null,a=null;if(t!=null)for(r in t.ref!==void 0&&(a=t.ref),t.key!==void 0&&(i=""+t.key),t)Tu.call(t,r)&&!bu.hasOwnProperty(r)&&(l[r]=t[r]);var o=arguments.length-2;if(o===1)l.children=n;else if(1>>1,V=z[G];if(0>>1;Gl($e,B))Lel(He,$e)?(z[G]=He,z[Le]=B,G=Le):(z[G]=$e,z[oe]=B,G=oe);else if(Lel(He,B))z[G]=He,z[Le]=B,G=Le;else break e}}return T}function l(z,T){var B=z.sortIndex-T.sortIndex;return B!==0?B:z.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var a=Date,o=a.now();e.unstable_now=function(){return a.now()-o}}var u=[],c=[],y=1,h=null,g=3,x=!1,v=!1,k=!1,j=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,d=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function m(z){for(var T=n(c);T!==null;){if(T.callback===null)r(c);else if(T.startTime<=z)r(c),T.sortIndex=T.expirationTime,t(u,T);else break;T=n(c)}}function w(z){if(k=!1,m(z),!v)if(n(u)!==null)v=!0,de(E);else{var T=n(c);T!==null&&te(w,T.startTime-z)}}function E(z,T){v=!1,k&&(k=!1,p(S),S=-1),x=!0;var B=g;try{for(m(T),h=n(u);h!==null&&(!(h.expirationTime>T)||z&&!K());){var G=h.callback;if(typeof G=="function"){h.callback=null,g=h.priorityLevel;var V=G(h.expirationTime<=T);T=e.unstable_now(),typeof V=="function"?h.callback=V:h===n(u)&&r(u),m(T)}else r(u);h=n(u)}if(h!==null)var fe=!0;else{var oe=n(c);oe!==null&&te(w,oe.startTime-T),fe=!1}return fe}finally{h=null,g=B,x=!1}}var P=!1,_=null,S=-1,I=5,b=-1;function K(){return!(e.unstable_now()-bz||125G?(z.sortIndex=B,t(c,z),n(u)===null&&z===n(c)&&(k?(p(S),S=-1):k=!0,te(w,B-G))):(z.sortIndex=V,t(u,z),v||x||(v=!0,de(E))),z},e.unstable_shouldYield=K,e.unstable_wrapCallback=function(z){var T=g;return function(){var B=g;g=T;try{return z.apply(this,arguments)}finally{g=B}}}})(Du);Iu.exports=Du;var jf=Iu.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Sf=f,ut=jf;function L(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ii=Object.prototype.hasOwnProperty,Nf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,ro={},lo={};function _f(e){return ii.call(lo,e)?!0:ii.call(ro,e)?!1:Nf.test(e)?lo[e]=!0:(ro[e]=!0,!1)}function Cf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Ef(e,t,n,r){if(t===null||typeof t>"u"||Cf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Je(e,t,n,r,l,i,a){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=a}var De={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){De[e]=new Je(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];De[t]=new Je(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){De[e]=new Je(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){De[e]=new Je(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){De[e]=new Je(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){De[e]=new Je(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){De[e]=new Je(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){De[e]=new Je(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){De[e]=new Je(e,5,!1,e.toLowerCase(),null,!1,!1)});var ra=/[\-:]([a-z])/g;function la(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ra,la);De[t]=new Je(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ra,la);De[t]=new Je(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ra,la);De[t]=new Je(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){De[e]=new Je(e,1,!1,e.toLowerCase(),null,!1,!1)});De.xlinkHref=new Je("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){De[e]=new Je(e,1,!1,e.toLowerCase(),null,!0,!0)});function sa(e,t,n,r){var l=De.hasOwnProperty(t)?De[t]:null;(l!==null?l.type!==0:r||!(2o||l[a]!==i[o]){var u=` +`+l[a].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=a&&0<=o);break}}}finally{zs=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?wr(e):""}function Tf(e){switch(e.tag){case 5:return wr(e.type);case 16:return wr("Lazy");case 13:return wr("Suspense");case 19:return wr("SuspenseList");case 0:case 2:case 15:return e=Rs(e.type,!1),e;case 11:return e=Rs(e.type.render,!1),e;case 1:return e=Rs(e.type,!0),e;default:return""}}function ci(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 $n:return"Fragment";case Dn:return"Portal";case ai:return"Profiler";case ia:return"StrictMode";case oi:return"Suspense";case ui:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Au:return(e.displayName||"Context")+".Consumer";case Ou:return(e._context.displayName||"Context")+".Provider";case aa:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case oa:return t=e.displayName||null,t!==null?t:ci(e.type)||"Memo";case Xt:t=e._payload,e=e._init;try{return ci(e(t))}catch{}}return null}function bf(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 ci(t);case 8:return t===ia?"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 cn(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Fu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Pf(e){var t=Fu(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(a){r=""+a,i.call(this,a)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(a){r=""+a},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ul(e){e._valueTracker||(e._valueTracker=Pf(e))}function Bu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Fu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Ml(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 di(e,t){var n=t.checked;return ve({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function io(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=cn(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 Hu(e,t){t=t.checked,t!=null&&sa(e,"checked",t,!1)}function fi(e,t){Hu(e,t);var n=cn(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")?pi(e,t.type,n):t.hasOwnProperty("defaultValue")&&pi(e,t.type,cn(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ao(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 pi(e,t,n){(t!=="number"||Ml(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var kr=Array.isArray;function Kn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=cl.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function $r(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Cr={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},Lf=["Webkit","ms","Moz","O"];Object.keys(Cr).forEach(function(e){Lf.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Cr[t]=Cr[e]})});function Qu(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Cr.hasOwnProperty(e)&&Cr[e]?(""+t).trim():t+"px"}function Ku(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Qu(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var zf=ve({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 gi(e,t){if(t){if(zf[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(L(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(L(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(L(61))}if(t.style!=null&&typeof t.style!="object")throw Error(L(62))}}function yi(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 vi=null;function ua(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var xi=null,qn=null,Xn=null;function co(e){if(e=nl(e)){if(typeof xi!="function")throw Error(L(280));var t=e.stateNode;t&&(t=ms(t),xi(e.stateNode,e.type,t))}}function qu(e){qn?Xn?Xn.push(e):Xn=[e]:qn=e}function Xu(){if(qn){var e=qn,t=Xn;if(Xn=qn=null,co(e),t)for(e=0;e>>=0,e===0?32:31-(Uf(e)/Wf|0)|0}var dl=64,fl=4194304;function jr(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 Ul(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,a=n&268435455;if(a!==0){var o=a&~l;o!==0?r=jr(o):(i&=a,i!==0&&(r=jr(i)))}else a=n&~l,a!==0?r=jr(a):i!==0&&(r=jr(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 el(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Et(t),e[t]=n}function qf(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=Tr),wo=" ",ko=!1;function mc(e,t){switch(e){case"keyup":return jp.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function gc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var On=!1;function Np(e,t){switch(e){case"compositionend":return gc(t);case"keypress":return t.which!==32?null:(ko=!0,wo);case"textInput":return e=t.data,e===wo&&ko?null:e;default:return null}}function _p(e,t){if(On)return e==="compositionend"||!ya&&mc(e,t)?(e=pc(),bl=ha=Zt=null,On=!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=_o(n)}}function wc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?wc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function kc(){for(var e=window,t=Ml();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ml(e.document)}return t}function va(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 Ip(e){var t=kc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&wc(n.ownerDocument.documentElement,n)){if(r!==null&&va(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=Co(n,i);var a=Co(n,r);l&&a&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==a.node||e.focusOffset!==a.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(a.node,a.offset)):(t.setEnd(a.node,a.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,An=null,_i=null,Pr=null,Ci=!1;function Eo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ci||An==null||An!==Ml(r)||(r=An,"selectionStart"in r&&va(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}),Pr&&Hr(Pr,r)||(Pr=r,r=Ql(_i,"onSelect"),0Bn||(e.current=zi[Bn],zi[Bn]=null,Bn--)}function ae(e,t){Bn++,zi[Bn]=e.current,e.current=t}var dn={},Be=pn(dn),tt=pn(!1),Cn=dn;function tr(e,t){var n=e.type.contextTypes;if(!n)return dn;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 nt(e){return e=e.childContextTypes,e!=null}function ql(){ce(tt),ce(Be)}function Io(e,t,n){if(Be.current!==dn)throw Error(L(168));ae(Be,t),ae(tt,n)}function Pc(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(L(108,bf(e)||"Unknown",l));return ve({},n,r)}function Xl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||dn,Cn=Be.current,ae(Be,e),ae(tt,tt.current),!0}function Do(e,t,n){var r=e.stateNode;if(!r)throw Error(L(169));n?(e=Pc(e,t,Cn),r.__reactInternalMemoizedMergedChildContext=e,ce(tt),ce(Be),ae(Be,e)):ce(tt),ae(tt,n)}var At=null,gs=!1,Ks=!1;function Lc(e){At===null?At=[e]:At.push(e)}function Qp(e){gs=!0,Lc(e)}function hn(){if(!Ks&&At!==null){Ks=!0;var e=0,t=se;try{var n=At;for(se=1;e>=a,l-=a,Mt=1<<32-Et(t)+l|n<S?(I=_,_=null):I=_.sibling;var b=g(p,_,m[S],w);if(b===null){_===null&&(_=I);break}e&&_&&b.alternate===null&&t(p,_),d=i(b,d,S),P===null?E=b:P.sibling=b,P=b,_=I}if(S===m.length)return n(p,_),he&&vn(p,S),E;if(_===null){for(;SS?(I=_,_=null):I=_.sibling;var K=g(p,_,b.value,w);if(K===null){_===null&&(_=I);break}e&&_&&K.alternate===null&&t(p,_),d=i(K,d,S),P===null?E=K:P.sibling=K,P=K,_=I}if(b.done)return n(p,_),he&&vn(p,S),E;if(_===null){for(;!b.done;S++,b=m.next())b=h(p,b.value,w),b!==null&&(d=i(b,d,S),P===null?E=b:P.sibling=b,P=b);return he&&vn(p,S),E}for(_=r(p,_);!b.done;S++,b=m.next())b=x(_,p,S,b.value,w),b!==null&&(e&&b.alternate!==null&&_.delete(b.key===null?S:b.key),d=i(b,d,S),P===null?E=b:P.sibling=b,P=b);return e&&_.forEach(function(W){return t(p,W)}),he&&vn(p,S),E}function j(p,d,m,w){if(typeof m=="object"&&m!==null&&m.type===$n&&m.key===null&&(m=m.props.children),typeof m=="object"&&m!==null){switch(m.$$typeof){case ol:e:{for(var E=m.key,P=d;P!==null;){if(P.key===E){if(E=m.type,E===$n){if(P.tag===7){n(p,P.sibling),d=l(P,m.props.children),d.return=p,p=d;break e}}else if(P.elementType===E||typeof E=="object"&&E!==null&&E.$$typeof===Xt&&Ao(E)===P.type){n(p,P.sibling),d=l(P,m.props),d.ref=gr(p,P,m),d.return=p,p=d;break e}n(p,P);break}else t(p,P);P=P.sibling}m.type===$n?(d=_n(m.props.children,p.mode,w,m.key),d.return=p,p=d):(w=Ol(m.type,m.key,m.props,null,p.mode,w),w.ref=gr(p,d,m),w.return=p,p=w)}return a(p);case Dn:e:{for(P=m.key;d!==null;){if(d.key===P)if(d.tag===4&&d.stateNode.containerInfo===m.containerInfo&&d.stateNode.implementation===m.implementation){n(p,d.sibling),d=l(d,m.children||[]),d.return=p,p=d;break e}else{n(p,d);break}else t(p,d);d=d.sibling}d=ti(m,p.mode,w),d.return=p,p=d}return a(p);case Xt:return P=m._init,j(p,d,P(m._payload),w)}if(kr(m))return v(p,d,m,w);if(dr(m))return k(p,d,m,w);xl(p,m)}return typeof m=="string"&&m!==""||typeof m=="number"?(m=""+m,d!==null&&d.tag===6?(n(p,d.sibling),d=l(d,m),d.return=p,p=d):(n(p,d),d=ei(m,p.mode,w),d.return=p,p=d),a(p)):n(p,d)}return j}var rr=Dc(!0),$c=Dc(!1),Gl=pn(null),Zl=null,Wn=null,ja=null;function Sa(){ja=Wn=Zl=null}function Na(e){var t=Gl.current;ce(Gl),e._currentValue=t}function Di(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 Yn(e,t){Zl=e,ja=Wn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(et=!0),e.firstContext=null)}function wt(e){var t=e._currentValue;if(ja!==e)if(e={context:e,memoizedValue:t,next:null},Wn===null){if(Zl===null)throw Error(L(308));Wn=e,Zl.dependencies={lanes:0,firstContext:e}}else Wn=Wn.next=e;return t}var kn=null;function _a(e){kn===null?kn=[e]:kn.push(e)}function Oc(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,_a(t)):(n.next=l.next,l.next=n),t.interleaved=n,Wt(e,r)}function Wt(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 Jt=!1;function Ca(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ac(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 Bt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function sn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,re&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Wt(e,n)}return l=r.interleaved,l===null?(t.next=t,_a(r)):(t.next=l.next,l.next=t),r.interleaved=t,Wt(e,n)}function Ll(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,da(e,n)}}function Mo(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 a={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=a:i=i.next=a,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 es(e,t,n,r){var l=e.updateQueue;Jt=!1;var i=l.firstBaseUpdate,a=l.lastBaseUpdate,o=l.shared.pending;if(o!==null){l.shared.pending=null;var u=o,c=u.next;u.next=null,a===null?i=c:a.next=c,a=u;var y=e.alternate;y!==null&&(y=y.updateQueue,o=y.lastBaseUpdate,o!==a&&(o===null?y.firstBaseUpdate=c:o.next=c,y.lastBaseUpdate=u))}if(i!==null){var h=l.baseState;a=0,y=c=u=null,o=i;do{var g=o.lane,x=o.eventTime;if((r&g)===g){y!==null&&(y=y.next={eventTime:x,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var v=e,k=o;switch(g=t,x=n,k.tag){case 1:if(v=k.payload,typeof v=="function"){h=v.call(x,h,g);break e}h=v;break e;case 3:v.flags=v.flags&-65537|128;case 0:if(v=k.payload,g=typeof v=="function"?v.call(x,h,g):v,g==null)break e;h=ve({},h,g);break e;case 2:Jt=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,g=l.effects,g===null?l.effects=[o]:g.push(o))}else x={eventTime:x,lane:g,tag:o.tag,payload:o.payload,callback:o.callback,next:null},y===null?(c=y=x,u=h):y=y.next=x,a|=g;if(o=o.next,o===null){if(o=l.shared.pending,o===null)break;g=o,o=g.next,g.next=null,l.lastBaseUpdate=g,l.shared.pending=null}}while(!0);if(y===null&&(u=h),l.baseState=u,l.firstBaseUpdate=c,l.lastBaseUpdate=y,t=l.shared.interleaved,t!==null){l=t;do a|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);bn|=a,e.lanes=a,e.memoizedState=h}}function Fo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Xs.transition;Xs.transition={};try{e(!1),t()}finally{se=n,Xs.transition=r}}function td(){return kt().memoizedState}function Jp(e,t,n){var r=on(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},nd(e))rd(t,n);else if(n=Oc(e,t,n,r),n!==null){var l=qe();Tt(n,e,r,l),ld(n,t,r)}}function Yp(e,t,n){var r=on(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(nd(e))rd(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var a=t.lastRenderedState,o=i(a,n);if(l.hasEagerState=!0,l.eagerState=o,bt(o,a)){var u=t.interleaved;u===null?(l.next=l,_a(t)):(l.next=u.next,u.next=l),t.interleaved=l;return}}catch{}finally{}n=Oc(e,t,l,r),n!==null&&(l=qe(),Tt(n,e,r,l),ld(n,t,r))}}function nd(e){var t=e.alternate;return e===ye||t!==null&&t===ye}function rd(e,t){Lr=ns=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ld(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,da(e,n)}}var rs={readContext:wt,useCallback:Ae,useContext:Ae,useEffect:Ae,useImperativeHandle:Ae,useInsertionEffect:Ae,useLayoutEffect:Ae,useMemo:Ae,useReducer:Ae,useRef:Ae,useState:Ae,useDebugValue:Ae,useDeferredValue:Ae,useTransition:Ae,useMutableSource:Ae,useSyncExternalStore:Ae,useId:Ae,unstable_isNewReconciler:!1},Gp={readContext:wt,useCallback:function(e,t){return Lt().memoizedState=[e,t===void 0?null:t],e},useContext:wt,useEffect:Ho,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Rl(4194308,4,Jc.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Rl(4194308,4,e,t)},useInsertionEffect:function(e,t){return Rl(4,2,e,t)},useMemo:function(e,t){var n=Lt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Lt();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=Jp.bind(null,ye,e),[r.memoizedState,e]},useRef:function(e){var t=Lt();return e={current:e},t.memoizedState=e},useState:Bo,useDebugValue:Ia,useDeferredValue:function(e){return Lt().memoizedState=e},useTransition:function(){var e=Bo(!1),t=e[0];return e=Xp.bind(null,e[1]),Lt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=ye,l=Lt();if(he){if(n===void 0)throw Error(L(407));n=n()}else{if(n=t(),Pe===null)throw Error(L(349));Tn&30||Hc(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Ho(Wc.bind(null,r,i,e),[e]),r.flags|=2048,Jr(9,Uc.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Lt(),t=Pe.identifierPrefix;if(he){var n=Ft,r=Mt;n=(r&~(1<<32-Et(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=qr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=a.createElement(n,{is:r.is}):(e=a.createElement(n),n==="select"&&(a=e,r.multiple?a.multiple=!0:r.size&&(a.size=r.size))):e=a.createElementNS(e,n),e[zt]=t,e[Vr]=r,hd(e,t,!1,!1),t.stateNode=e;e:{switch(a=yi(n,r),n){case"dialog":ue("cancel",e),ue("close",e),l=r;break;case"iframe":case"object":case"embed":ue("load",e),l=r;break;case"video":case"audio":for(l=0;lir&&(t.flags|=128,r=!0,yr(i,!1),t.lanes=4194304)}else{if(!r)if(e=ts(a),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),yr(i,!0),i.tail===null&&i.tailMode==="hidden"&&!a.alternate&&!he)return Me(t),null}else 2*je()-i.renderingStartTime>ir&&n!==1073741824&&(t.flags|=128,r=!0,yr(i,!1),t.lanes=4194304);i.isBackwards?(a.sibling=t.child,t.child=a):(n=i.last,n!==null?n.sibling=a:t.child=a,i.last=a)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=je(),t.sibling=null,n=ge.current,ae(ge,r?n&1|2:n&1),t):(Me(t),null);case 22:case 23:return Fa(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?it&1073741824&&(Me(t),t.subtreeFlags&6&&(t.flags|=8192)):Me(t),null;case 24:return null;case 25:return null}throw Error(L(156,t.tag))}function ih(e,t){switch(wa(t),t.tag){case 1:return nt(t.type)&&ql(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return lr(),ce(tt),ce(Be),ba(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ta(t),null;case 13:if(ce(ge),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(L(340));nr()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ce(ge),null;case 4:return lr(),null;case 10:return Na(t.type._context),null;case 22:case 23:return Fa(),null;case 24:return null;default:return null}}var kl=!1,Fe=!1,ah=typeof WeakSet=="function"?WeakSet:Set,M=null;function Vn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){we(e,t,r)}else n.current=null}function Wi(e,t,n){try{n()}catch(r){we(e,t,r)}}var Zo=!1;function oh(e,t){if(Ei=Wl,e=kc(),va(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 a=0,o=-1,u=-1,c=0,y=0,h=e,g=null;t:for(;;){for(var x;h!==n||l!==0&&h.nodeType!==3||(o=a+l),h!==i||r!==0&&h.nodeType!==3||(u=a+r),h.nodeType===3&&(a+=h.nodeValue.length),(x=h.firstChild)!==null;)g=h,h=x;for(;;){if(h===e)break t;if(g===n&&++c===l&&(o=a),g===i&&++y===r&&(u=a),(x=h.nextSibling)!==null)break;h=g,g=h.parentNode}h=x}n=o===-1||u===-1?null:{start:o,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ti={focusedElem:e,selectionRange:n},Wl=!1,M=t;M!==null;)if(t=M,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,M=e;else for(;M!==null;){t=M;try{var v=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(v!==null){var k=v.memoizedProps,j=v.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?k:St(t.type,k),j);p.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var m=t.stateNode.containerInfo;m.nodeType===1?m.textContent="":m.nodeType===9&&m.documentElement&&m.removeChild(m.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(L(163))}}catch(w){we(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,M=e;break}M=t.return}return v=Zo,Zo=!1,v}function zr(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&&Wi(t,n,i)}l=l.next}while(l!==r)}}function xs(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Vi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function yd(e){var t=e.alternate;t!==null&&(e.alternate=null,yd(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[zt],delete t[Vr],delete t[Li],delete t[Wp],delete t[Vp])),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 vd(e){return e.tag===5||e.tag===3||e.tag===4}function eu(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||vd(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 Qi(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=Kl));else if(r!==4&&(e=e.child,e!==null))for(Qi(e,t,n),e=e.sibling;e!==null;)Qi(e,t,n),e=e.sibling}function Ki(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(Ki(e,t,n),e=e.sibling;e!==null;)Ki(e,t,n),e=e.sibling}var Re=null,Nt=!1;function qt(e,t,n){for(n=n.child;n!==null;)xd(e,t,n),n=n.sibling}function xd(e,t,n){if(Rt&&typeof Rt.onCommitFiberUnmount=="function")try{Rt.onCommitFiberUnmount(ds,n)}catch{}switch(n.tag){case 5:Fe||Vn(n,t);case 6:var r=Re,l=Nt;Re=null,qt(e,t,n),Re=r,Nt=l,Re!==null&&(Nt?(e=Re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Re.removeChild(n.stateNode));break;case 18:Re!==null&&(Nt?(e=Re,n=n.stateNode,e.nodeType===8?Qs(e.parentNode,n):e.nodeType===1&&Qs(e,n),Fr(e)):Qs(Re,n.stateNode));break;case 4:r=Re,l=Nt,Re=n.stateNode.containerInfo,Nt=!0,qt(e,t,n),Re=r,Nt=l;break;case 0:case 11:case 14:case 15:if(!Fe&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,a=i.destroy;i=i.tag,a!==void 0&&(i&2||i&4)&&Wi(n,t,a),l=l.next}while(l!==r)}qt(e,t,n);break;case 1:if(!Fe&&(Vn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){we(n,t,o)}qt(e,t,n);break;case 21:qt(e,t,n);break;case 22:n.mode&1?(Fe=(r=Fe)||n.memoizedState!==null,qt(e,t,n),Fe=r):qt(e,t,n);break;default:qt(e,t,n)}}function tu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new ah),t.forEach(function(r){var l=yh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function jt(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=a),r&=~i}if(r=l,r=je()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*ch(r/1960))-r,10e?16:e,en===null)var r=!1;else{if(e=en,en=null,is=0,re&6)throw Error(L(331));var l=re;for(re|=4,M=e.current;M!==null;){var i=M,a=i.child;if(M.flags&16){var o=i.deletions;if(o!==null){for(var u=0;uje()-Aa?Nn(e,0):Oa|=n),rt(e,t)}function Ed(e,t){t===0&&(e.mode&1?(t=fl,fl<<=1,!(fl&130023424)&&(fl=4194304)):t=1);var n=qe();e=Wt(e,t),e!==null&&(el(e,t,n),rt(e,n))}function gh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Ed(e,n)}function yh(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(L(314))}r!==null&&r.delete(t),Ed(e,n)}var Td;Td=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||tt.current)et=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return et=!1,lh(e,t,n);et=!!(e.flags&131072)}else et=!1,he&&t.flags&1048576&&zc(t,Yl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Il(e,t),e=t.pendingProps;var l=tr(t,Be.current);Yn(t,n),l=La(null,t,r,e,l,n);var i=za();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,nt(r)?(i=!0,Xl(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Ca(t),l.updater=vs,t.stateNode=l,l._reactInternals=t,Oi(t,r,e,n),t=Fi(null,t,r,!0,i,n)):(t.tag=0,he&&i&&xa(t),Ke(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Il(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=xh(r),e=St(r,e),l){case 0:t=Mi(null,t,r,e,n);break e;case 1:t=Jo(null,t,r,e,n);break e;case 11:t=qo(null,t,r,e,n);break e;case 14:t=Xo(null,t,r,St(r.type,e),n);break e}throw Error(L(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:St(r,l),Mi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:St(r,l),Jo(e,t,r,l,n);case 3:e:{if(dd(t),e===null)throw Error(L(387));r=t.pendingProps,i=t.memoizedState,l=i.element,Ac(e,t),es(t,r,null,n);var a=t.memoizedState;if(r=a.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:a.cache,pendingSuspenseBoundaries:a.pendingSuspenseBoundaries,transitions:a.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=sr(Error(L(423)),t),t=Yo(e,t,r,n,l);break e}else if(r!==l){l=sr(Error(L(424)),t),t=Yo(e,t,r,n,l);break e}else for(at=ln(t.stateNode.containerInfo.firstChild),ot=t,he=!0,_t=null,n=$c(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(nr(),r===l){t=Vt(e,t,n);break e}Ke(e,t,r,n)}t=t.child}return t;case 5:return Mc(t),e===null&&Ii(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,a=l.children,bi(r,l)?a=null:i!==null&&bi(r,i)&&(t.flags|=32),cd(e,t),Ke(e,t,a,n),t.child;case 6:return e===null&&Ii(t),null;case 13:return fd(e,t,n);case 4:return Ea(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=rr(t,null,r,n):Ke(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:St(r,l),qo(e,t,r,l,n);case 7:return Ke(e,t,t.pendingProps,n),t.child;case 8:return Ke(e,t,t.pendingProps.children,n),t.child;case 12:return Ke(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,a=l.value,ae(Gl,r._currentValue),r._currentValue=a,i!==null)if(bt(i.value,a)){if(i.children===l.children&&!tt.current){t=Vt(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var o=i.dependencies;if(o!==null){a=i.child;for(var u=o.firstContext;u!==null;){if(u.context===r){if(i.tag===1){u=Bt(-1,n&-n),u.tag=2;var c=i.updateQueue;if(c!==null){c=c.shared;var y=c.pending;y===null?u.next=u:(u.next=y.next,y.next=u),c.pending=u}}i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Di(i.return,n,t),o.lanes|=n;break}u=u.next}}else if(i.tag===10)a=i.type===t.type?null:i.child;else if(i.tag===18){if(a=i.return,a===null)throw Error(L(341));a.lanes|=n,o=a.alternate,o!==null&&(o.lanes|=n),Di(a,n,t),a=i.sibling}else a=i.child;if(a!==null)a.return=i;else for(a=i;a!==null;){if(a===t){a=null;break}if(i=a.sibling,i!==null){i.return=a.return,a=i;break}a=a.return}i=a}Ke(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Yn(t,n),l=wt(l),r=r(l),t.flags|=1,Ke(e,t,r,n),t.child;case 14:return r=t.type,l=St(r,t.pendingProps),l=St(r.type,l),Xo(e,t,r,l,n);case 15:return od(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:St(r,l),Il(e,t),t.tag=1,nt(r)?(e=!0,Xl(t)):e=!1,Yn(t,n),sd(t,r,l),Oi(t,r,l,n),Fi(null,t,r,!0,e,n);case 19:return pd(e,t,n);case 22:return ud(e,t,n)}throw Error(L(156,t.tag))};function bd(e,t){return nc(e,t)}function vh(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 vt(e,t,n,r){return new vh(e,t,n,r)}function Ha(e){return e=e.prototype,!(!e||!e.isReactComponent)}function xh(e){if(typeof e=="function")return Ha(e)?1:0;if(e!=null){if(e=e.$$typeof,e===aa)return 11;if(e===oa)return 14}return 2}function un(e,t){var n=e.alternate;return n===null?(n=vt(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 Ol(e,t,n,r,l,i){var a=2;if(r=e,typeof e=="function")Ha(e)&&(a=1);else if(typeof e=="string")a=5;else e:switch(e){case $n:return _n(n.children,l,i,t);case ia:a=8,l|=8;break;case ai:return e=vt(12,n,t,l|2),e.elementType=ai,e.lanes=i,e;case oi:return e=vt(13,n,t,l),e.elementType=oi,e.lanes=i,e;case ui:return e=vt(19,n,t,l),e.elementType=ui,e.lanes=i,e;case Mu:return ks(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ou:a=10;break e;case Au:a=9;break e;case aa:a=11;break e;case oa:a=14;break e;case Xt:a=16,r=null;break e}throw Error(L(130,e==null?e:typeof e,""))}return t=vt(a,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function _n(e,t,n,r){return e=vt(7,e,r,t),e.lanes=n,e}function ks(e,t,n,r){return e=vt(22,e,r,t),e.elementType=Mu,e.lanes=n,e.stateNode={isHidden:!1},e}function ei(e,t,n){return e=vt(6,e,null,t),e.lanes=n,e}function ti(e,t,n){return t=vt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function wh(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=Ds(0),this.expirationTimes=Ds(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ds(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ua(e,t,n,r,l,i,a,o,u){return e=new wh(e,t,n,o,u),t===1?(t=1,i===!0&&(t|=8)):t=0,i=vt(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ca(i),e}function kh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Rd)}catch(e){console.error(e)}}Rd(),Ru.exports=ct;var Ch=Ru.exports,Id,uu=Ch;Id=uu.createRoot,uu.hydrateRoot;async function _e(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function Eh(){return _e("/api/archives")}async function Th(e){return _e(`/api/archives/${e}/entries`)}async function bh(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),_e(`/api/archives/${e}/entries/search?${r}`)}async function Nr(e,t){return _e(`/api/archives/${e}/entries/${t}`)}async function Ph(e,t){return _e(`/api/archives/${e}/entries/${t}/children`)}function Lh(e,t,n){return Promise.all(n.map(r=>_e(`/api/archives/${e}/entries/${t}/artifacts/${r}`)))}async function zh(e){if(!e||e.length===0)return{};const t=await fetch("/api/util/resolve-tco",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});return t.ok?t.json():{}}async function Rh(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:n??null})});if(!r.ok)throw new Error(await r.text())}async function Nl(e,t){return _e(`/api/archives/${e}/entries/${t}/tags`)}async function cu(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 Ih(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 du(e,t){const n=await fetch(`/api/archives/${e}/entries/${t}`,{method:"DELETE"});if(!n.ok)throw new Error(`Delete failed (${n.status})`)}async function Dh(e,t,n){const r=await fetch(`/api/archives/${e}/tags/${t}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n})});if(!r.ok)throw new Error(await r.text());return r.json()}async function $h(e,t){const n=await fetch(`/api/archives/${e}/tags/${t}`,{method:"DELETE"});if(!n.ok)throw new Error(await n.text())}async function Oh(e,t){const n=await fetch(`/api/archives/${e}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok)throw new Error(await n.text());return n.json()}async function Ah(e,t,n){const r=await fetch(`/api/archives/${e}/tags/${t}/move`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({parent_uid:n??null})});if(!r.ok)throw new Error(await r.text());return r.json()}async function ni(e){return _e(`/api/archives/${e}/runs`)}async function _l(e){return _e(`/api/archives/${e}/tags`)}async function Mh(e,t,n=null,r=null){const l={locator:t};n&&n!=="best"&&(l.quality=n),r&&(typeof r.ublock_enabled=="boolean"&&(l.ublock_enabled=r.ublock_enabled),typeof r.reader_mode=="boolean"&&(l.reader_mode=r.reader_mode),typeof r.cookie_ext_enabled=="boolean"&&(l.cookie_ext_enabled=r.cookie_ext_enabled),typeof r.modal_closer_enabled=="boolean"&&(l.modal_closer_enabled=r.modal_closer_enabled),typeof r.via_freedium=="boolean"&&(l.via_freedium=r.via_freedium),r.per_item_quality&&typeof r.per_item_quality=="object"&&Object.keys(r.per_item_quality).length>0&&(l.per_item_quality=r.per_item_quality),r.sync===!0&&(l.sync=!0));const i=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!i.ok){const a=await i.json().catch(()=>({}));throw new Error(a.error||`HTTP ${i.status}`)}return i.json()}async function Fh(e,t){return _e(`/api/archives/${e}/captures/probe?locator=${encodeURIComponent(t)}`)}async function Bh(e,t){const n=await fetch(`/api/archives/${e}/captures/probe-playlist`,{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 Dd(e,t){return _e(`/api/archives/${e}/capture_jobs/${t}`)}async function Hh(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function Uh(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 Wh(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 Vh(){await fetch("/api/auth/logout",{method:"POST"})}async function Qh(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function Kh(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 qh(e){const t=await fetch("/api/auth/me",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok)throw new Error(await t.text())}async function Xh(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 Jh(){return _e("/api/auth/tokens")}async function Yh(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 Gh(e){const t=await fetch(`/api/auth/tokens/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(await t.text())}async function Ka(){return _e("/api/admin/instance-settings")}async function Al(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 Zh(){return _e("/api/admin/users")}async function em(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 tm(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 nm(){return _e("/api/admin/roles")}async function rm(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 $d(e){return _e(`/api/archives/${e}/collections`)}async function lm(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 sm(e,t){return _e(`/api/archives/${e}/collections/${t}`)}async function Od(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 im(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 am(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 om(e,t){return _e(`/api/archives/${e}/entries/${t}/collections`)}async function fu(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 um(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)}}async function cm(e){return _e(`/api/archives/${e}/blob-cleanup`)}async function dm(e){const t=await fetch(`/api/archives/${e}/blob-cleanup`,{method:"DELETE"});if(!t.ok){const n=await t.json().catch(()=>({}));throw new Error(n.error??t.statusText)}return t.json()}async function fm(e,t){const n=await fetch(`/api/archives/${e}/entries/${t}/rearchive`,{method:"POST"});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.message||`rearchive failed: ${n.status}`)}return n.json()}async function pm(){return _e("/api/admin/cookie-rules")}async function hm(e,t,n){const r=await fetch("/api/admin/cookie-rules",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url_pattern:e||null,pattern_kind:t,cookies_json:n})});if(!r.ok){const l=await r.json().catch(()=>({}));throw new Error(l.message||`HTTP ${r.status}`)}return r.json()}async function mm(e,t){const n=await fetch(`/api/admin/cookie-rules/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.message||`HTTP ${n.status}`)}}async function gm(e){const t=await fetch(`/api/admin/cookie-rules/${e}`,{method:"DELETE"});if(!t.ok){const n=await t.json().catch(()=>({}));throw new Error(n.message||`HTTP ${t.status}`)}}const ym=window.fetch;window.fetch=async(...e)=>{var n;const t=await ym(...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 vm({onLogin:e}){const[t,n]=f.useState(""),[r,l]=f.useState(""),[i,a]=f.useState(null),[o,u]=f.useState(!1);async function c(y){y.preventDefault(),a(null),u(!0);try{const h=await Wh(t,r);e(h)}catch(h){a(h.message)}finally{u(!1)}}return s.jsx("div",{className:"login-page",children:s.jsxs("div",{className:"login-card",children:[s.jsx("h1",{className:"login-brand",children:"Archivr"}),s.jsx("p",{className:"login-tagline",children:"Sign in to your archive"}),s.jsxs("form",{onSubmit:c,children:[s.jsxs("div",{className:"login-field",children:[s.jsx("label",{className:"login-label",htmlFor:"login-username",children:"Username"}),s.jsx("input",{className:"login-input",id:"login-username",type:"text",value:t,onChange:y=>n(y.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),s.jsxs("div",{className:"login-field",children:[s.jsx("label",{className:"login-label",htmlFor:"login-password",children:"Password"}),s.jsx("input",{className:"login-input",id:"login-password",type:"password",value:r,onChange:y=>l(y.target.value),required:!0,autoComplete:"current-password"})]}),i&&s.jsx("p",{className:"login-error",children:i}),s.jsx("button",{className:"login-submit",type:"submit",disabled:o,children:o?"Signing in…":"Sign in"})]})]})})}function xm({onComplete:e}){const[t,n]=f.useState(""),[r,l]=f.useState(""),[i,a]=f.useState(""),[o,u]=f.useState(null),[c,y]=f.useState(!1);async function h(g){if(g.preventDefault(),r!==i){u("Passwords do not match");return}if(r.length<8){u("Password must be at least 8 characters");return}u(null),y(!0);try{await Uh(t,r),e()}catch(x){u(x.message)}finally{y(!1)}}return s.jsx("div",{className:"setup-page",children:s.jsxs("div",{className:"setup-card",children:[s.jsx("h1",{className:"setup-brand",children:"Archivr"}),s.jsx("p",{className:"setup-tagline",children:"Create your owner account to get started."}),s.jsxs("form",{onSubmit:h,children:[s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-username",children:"Username"}),s.jsx("input",{className:"setup-input",id:"setup-username",type:"text",value:t,onChange:g=>n(g.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-password",children:"Password"}),s.jsx("input",{className:"setup-input",id:"setup-password",type:"password",value:r,onChange:g=>l(g.target.value),required:!0,autoComplete:"new-password"})]}),s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-confirm",children:"Confirm password"}),s.jsx("input",{className:"setup-input",id:"setup-confirm",type:"password",value:i,onChange:g=>a(g.target.value),required:!0,autoComplete:"new-password"})]}),o&&s.jsx("p",{className:"setup-error",children:o}),s.jsx("button",{className:"setup-submit",type:"submit",disabled:c,children:c?"Creating account…":"Create account"})]})]})})}function wm({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:a,setCurrentUser:o}=f.useContext(Es)??{},[u,c]=f.useState(!1);async function y(){c(!0),await Vh(),o(null),window.location.reload()}return s.jsxs("header",{className:"topbar",children:[s.jsx("div",{className:"brand",children:"Archivr"}),s.jsx("div",{className:"switcher",children:s.jsx("select",{"aria-label":"Select archive",value:t??"",onChange:h=>n(h.target.value),children:e.map(h=>s.jsx("option",{value:h.id,children:h.label},h.id))})}),s.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","tags","collections","runs","admin","settings"].map(h=>s.jsx("button",{className:`nav-link${r===h?" is-active":""}`,onClick:()=>l(h),children:h.charAt(0).toUpperCase()+h.slice(1)},h))}),s.jsx("button",{className:"capture-button",onClick:i,children:"Capture"}),a&&s.jsxs("div",{className:"user-menu",children:[s.jsx("span",{className:"user-name",children:a.display_name||a.username}),s.jsx("button",{className:"logout-btn",onClick:y,disabled:u,children:u?"Logging out…":"Log out"})]})]})}let Gi=1;function Ad(e){const t=e.trim(),n=t.toLowerCase();for(const r of["yt:","youtube:"])if(n.startsWith(r)){const l=n.slice(r.length);return!!(l.startsWith("video/")||l.startsWith("short/")||l.startsWith("shorts/")||/^[a-z0-9_-]{11}$/i.test(l))}if(n.startsWith("ytm:"))return!n.slice(4).startsWith("playlist/");for(const r of["x:","twitter:","tweet:"])if(n.startsWith(r))return n.slice(r.length).startsWith("media:");if(n.startsWith("spotify:"))return!1;if(n.startsWith("instagram:")||n.startsWith("facebook:")||n.startsWith("tiktok:")||n.startsWith("reddit:")||n.startsWith("snapchat:"))return!0;if(n.startsWith("http://")||n.startsWith("https://")){if(/^https?:\/\/music\.youtube\.com\/watch/.test(n)||/^https?:\/\/(?:www\.)?(?:youtu\.be\/[0-9A-Za-z_-]+|youtube\.com\/watch\?v=[0-9A-Za-z_-]+|youtube\.com\/shorts\/[0-9A-Za-z_-]+)/.test(t)||n.startsWith("https://x.com/")||n.startsWith("http://x.com/")||/^https?:\/\/(?:www\.)?instagram\.com\//.test(n)||/^https?:\/\/(?:www\.)?facebook\.com\//.test(n)||n.startsWith("https://fb.watch/")||n.startsWith("http://fb.watch/")||/^https?:\/\/(?:www\.)?tiktok\.com\//.test(n)||/^https?:\/\/(?:www\.)?reddit\.com\//.test(n)||n.startsWith("https://redd.it/")||n.startsWith("http://redd.it/")||/^https?:\/\/(?:www\.)?snapchat\.com\//.test(n))return!0;if(n.startsWith("https://open.spotify.com/")||n.startsWith("http://open.spotify.com/"))return!1}return!1}function Sn(e){const t=e.trim(),n=t.toLowerCase();for(const r of["yt:","youtube:"])if(n.startsWith(r)){const l=n.slice(r.length);return l.startsWith("playlist/")||l.startsWith("@")||l.startsWith("channel/")||l.startsWith("c/")||l.startsWith("user/")}if(n.startsWith("ytm:"))return n.slice(4).startsWith("playlist/");if(n.startsWith("spotify:")){const r=n.slice(8);return r.startsWith("album:")||r.startsWith("playlist:")}if(n.startsWith("http://")||n.startsWith("https://"))try{const r=new URL(t),l=r.hostname;if((l==="youtube.com"||l==="www.youtube.com")&&(r.pathname==="/playlist"&&r.searchParams.has("list")||r.pathname.startsWith("/@")||r.pathname.startsWith("/channel/")||r.pathname.startsWith("/c/")||r.pathname.startsWith("/user/"))||l==="music.youtube.com"&&r.pathname==="/playlist"&&r.searchParams.has("list")||l==="open.spotify.com"&&(r.pathname.startsWith("/album/")||r.pathname.startsWith("/playlist/")))return!0}catch{}return!1}function In(e=""){return{id:Gi++,locator:e,quality:"best",probeState:"idle",probeQualities:null,probeHasAudio:!1,status:"idle",error:null,jobUid:null,archiveId:null,playlistProbeState:"idle",playlistInfo:null,playlistItems:null,playlistQuality:null,playlistExpanded:!1,syncEnabled:!1}}function km(e,t){return e==="best"?t.map(n=>({...n,quality:"best"})):e==="audio"?t.map(n=>n.has_audio?{...n,quality:"audio"}:n.quality!==null?n:{...n,quality:null}):t.map(n=>n.qualities.includes(e)?{...n,quality:e}:n.quality!==null?n:{...n,quality:null})}function pu(e){return Array.isArray(e.playlistItems)&&e.playlistItems.some(t=>t.quality===null)}function jm({open:e,archiveId:t,onClose:n,onCaptured:r,onToast:l,onJobStarted:i,onJobSettled:a,activeJobs:o=[]}){const u=f.useRef(null),c=f.useRef(!0),y=f.useRef(new Map),h=f.useRef(new Map),g=f.useRef(new Map),x=f.useRef(t);f.useEffect(()=>{x.current=t},[t]);const v=f.useRef(r),k=f.useRef(l);f.useEffect(()=>{v.current=r},[r]),f.useEffect(()=>{k.current=l},[l]);const j=f.useRef(i),p=f.useRef(a);f.useEffect(()=>{j.current=i},[i]),f.useEffect(()=>{p.current=a},[a]);const[d,m]=f.useState(()=>{try{const N=JSON.parse(sessionStorage.getItem("captureItems")||"null");if(Array.isArray(N)&&N.length>0){const F=N.filter(O=>!O.status||O.status==="idle");if(F.length>0)return F.forEach(O=>{O.id>=Gi&&(Gi=O.id+1)}),F.map(O=>({...In(O.locator),...O}))}}catch{}return[In()]});f.useEffect(()=>{sessionStorage.setItem("captureItems",JSON.stringify(d))},[d]);const[w,E]=f.useState(!1),[P,_]=f.useState(null),[S,I]=f.useState(null),[b,K]=f.useState(!0),[W,q]=f.useState(!0),[ee,me]=f.useState(!0);f.useEffect(()=>{Ka().then(N=>{I(N),K(N.cookie_ext_enabled??!0),q(N.modal_closer_enabled??!0)}).catch(()=>I({}))},[]);const de=P!==null?P:(S==null?void 0:S.ublock_enabled)??!0,[te,z]=f.useState(!1);f.useEffect(()=>{["captureDialogLocator","captureDialogError","captureDialogBusy","captureDialogJobStatus","captureDialogJobUid"].forEach(N=>sessionStorage.removeItem(N)),o.forEach(N=>{N.jobUid&&!y.current.has(N.jobUid)&&T(N.id,N.jobUid,N.locator,N.archiveId,null)})},[]),f.useEffect(()=>{const N=u.current;if(!N)return;const F=()=>{h.current.forEach(O=>clearTimeout(O)),h.current.clear(),n()};return N.addEventListener("close",F),()=>N.removeEventListener("close",F)},[n]),f.useEffect(()=>{const N=u.current;N&&(e?(c.current||m([In()]),c.current=!1,N.open||N.showModal()):(h.current.forEach(F=>clearTimeout(F)),h.current.clear(),N.open&&N.close()))},[e]),f.useEffect(()=>()=>{y.current.forEach(N=>clearInterval(N)),h.current.forEach(N=>clearTimeout(N))},[]);function T(N,F,O,H,U=null){if(y.current.has(F))return;const X=setInterval(async()=>{var pe,ze,Ue,We;try{const Ve=await Dd(H,F);if(Ve.status==="completed"){clearInterval(y.current.get(F)),y.current.delete(F),await Promise.resolve((pe=v.current)==null?void 0:pe.call(v)),(ze=p.current)==null||ze.call(p,N);try{const xe=Ve.notes_json?JSON.parse(Ve.notes_json):null;if(xe!=null&&xe.ublock_skipped||xe!=null&&xe.cookie_ext_skipped){const Qe=xe.ublock_skipped&&xe.cookie_ext_skipped?"Captured without ad-blocking or cookie-consent extension. Check ARCHIVR_UBLOCK_EXT and ARCHIVR_COOKIE_EXT in your server config.":xe.ublock_skipped?"Captured without ad-blocking. ARCHIVR_UBLOCK=true but ARCHIVR_UBLOCK_EXT is not set or the path is invalid.":"Captured without cookie-consent extension. ARCHIVR_COOKIE_EXT is not set or the path is invalid.";k.current(Qe,O,"warning"),B(U,"warning",O)}else U||k.current(null,O,"success"),B(U,"archived")}catch{U||k.current(null,O,"success"),B(U,"archived")}}else if(Ve.status==="failed"){clearInterval(y.current.get(F)),y.current.delete(F),(Ue=p.current)==null||Ue.call(p,N);const xe=Ve.error_text||"Capture failed.";k.current(xe,O),B(U,"failed",O)}}catch(Ve){clearInterval(y.current.get(F)),y.current.delete(F),(We=p.current)==null||We.call(p,N);const xe=Ve.message||"Network error";k.current(xe,O),B(U,"failed",O)}},500);y.current.set(F,X)}function B(N,F,O=null){if(!N)return;const H=g.current.get(N);if(!H||(F==="archived"||F==="warning"?(H.archived++,F==="warning"&&(H.warnings++,O&&H.warningLocators.push(O))):(H.failed++,O&&H.failedLocators.push(O)),H.archived+H.failed0?`${U} archived (${X} with warnings)`:`${U} archived`;We=pe>0?`${Qe}, ${pe} failed`:Qe}const Ve=U===0?"error":pe>0||X>0?"warning":"success",xe=[];ze.length>0&&xe.push(`Failed: +${ze.map(Qe=>` ${Qe}`).join(` +`)}`),Ue.length>0&&xe.push(`With warnings: +${Ue.map(Qe=>` ${Qe}`).join(` +`)}`);const Dt=xe.length>0?xe.join(` +`):null;k.current(Dt,null,Ve,We)}async function G(N,F,O,H={}){var ze,Ue;const U=x.current,X=((ze=crypto.randomUUID)==null?void 0:ze.call(crypto))??`job-${Date.now()}-${Math.random()}`,pe={ublock_enabled:de,reader_mode:te,cookie_ext_enabled:b,modal_closer_enabled:W,via_freedium:ee,...H};try{const We=await Mh(U,N,F,pe);(Ue=j.current)==null||Ue.call(j,{id:X,jobUid:We.job_uid,locator:N,archiveId:U}),T(X,We.job_uid,N,U,O)}catch(We){const Ve=We.message||"Submission failed.";k.current(Ve,N),B(O,"failed",N)}}function V(){var H,U;const N=d.filter(X=>X.locator.trim());if(N.length===0||N.some(X=>pu(X))||N.some(X=>X.probeState==="probing"||Sn(X.locator)&&X.playlistProbeState!=="done")||N.some(X=>Array.isArray(X.playlistItems)&&X.playlistItems.length===0))return;const F=N.length>1?((H=crypto.randomUUID)==null?void 0:H.call(crypto))??`batch-${Date.now()}`:null;F&&g.current.set(F,{total:N.length,archived:0,warnings:0,failed:0,failedLocators:[],warningLocators:[]});const O=N.map(X=>({locator:X.locator.trim(),quality:X.playlistItems!==null?null:X.quality||"best",extraExtensions:X.playlistItems!==null?{per_item_quality:Object.fromEntries(X.playlistItems.map(pe=>[pe.id,pe.quality])),sync:X.syncEnabled}:{}}));m([In()]),(U=u.current)==null||U.close(),O.forEach(({locator:X,quality:pe,extraExtensions:ze})=>G(X,pe,F,ze))}function fe(){m(N=>[...N,In()])}function oe(N){clearTimeout(h.current.get(N)),h.current.delete(N),m(F=>{const O=F.filter(H=>H.id!==N);return O.length===0?[In()]:O})}function $e(N,F){if(clearTimeout(h.current.get(N)),m(O=>O.map(H=>H.id===N?{...H,locator:F,probeState:"idle",probeQualities:null,probeHasAudio:!1,quality:"best",playlistProbeState:"idle",playlistInfo:null,playlistItems:null,playlistQuality:null,playlistExpanded:!1}:H)),Ad(F)){const O=setTimeout(async()=>{h.current.delete(N),m(H=>H.map(U=>U.id===N?{...U,probeState:"probing"}:U));try{const H=await Fh(x.current,F.trim());m(U=>U.map(X=>{if(X.id!==N||X.locator!==F)return X;const pe=H.qualities??[],ze=H.has_audio??!1,Ue=pe.length===0&&ze?"audio":"best";return{...X,probeState:"done",probeQualities:pe,probeHasAudio:ze,quality:Ue}}))}catch{m(H=>H.map(U=>U.id===N?{...U,probeState:"idle",probeQualities:null}:U))}},600);h.current.set(N,O)}else if(Sn(F)){const O=setTimeout(async()=>{h.current.delete(N),m(H=>H.map(U=>U.id===N?{...U,playlistProbeState:"probing"}:U));try{const H=await Bh(x.current,F.trim());m(U=>U.map(X=>X.id!==N||X.locator!==F?X:{...X,playlistProbeState:"done",playlistInfo:H,playlistItems:H.items.map(pe=>({...pe,quality:null})),playlistQuality:null}))}catch{m(H=>H.map(U=>U.id===N?{...U,playlistProbeState:"error"}:U))}},800);h.current.set(N,O)}}function Le(N,F){m(O=>O.map(H=>H.id===N?{...H,quality:F}:H))}function He(N,F){m(O=>O.map(H=>{if(H.id!==N)return H;const U=km(F,H.playlistItems);return{...H,playlistQuality:F,playlistItems:U}}))}function ft(N,F,O){m(H=>H.map(U=>U.id!==N?U:{...U,playlistItems:U.playlistItems.map(X=>X.id===F?{...X,quality:O}:X)}))}function lt(N){m(F=>F.map(O=>O.id===N?{...O,playlistExpanded:!O.playlistExpanded}:O))}function pt(N,F){m(O=>O.map(H=>H.id===N?{...H,syncEnabled:F}:H))}function ht(N,F){m(O=>O.map(H=>H.id!==N?H:{...H,playlistItems:H.playlistItems.filter(U=>U.id!==F)}))}const Ye=d.filter(N=>N.locator.trim()).length,R=d.some(N=>pu(N)),J=d.some(N=>Array.isArray(N.playlistItems)&&N.playlistItems.length===0),Se=d.some(N=>N.probeState==="probing"||Sn(N.locator)&&N.playlistProbeState!=="done");return s.jsx("dialog",{ref:u,className:"capture-dialog",children:s.jsxs("div",{className:"capture-dialog-inner",children:[s.jsxs("div",{className:"capture-dialog-header",children:[s.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),s.jsx("button",{type:"button",className:"capture-dialog-close",onClick:()=>{var N;return(N=u.current)==null?void 0:N.close()},"aria-label":"Close",children:s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"3",y1:"3",x2:"13",y2:"13"}),s.jsx("line",{x1:"13",y1:"3",x2:"3",y2:"13"})]})})]}),s.jsx("div",{className:"capture-rows",children:d.map((N,F)=>s.jsx(Sm,{item:N,autoFocus:F===d.length-1,onLocatorChange:O=>$e(N.id,O),onQualityChange:O=>Le(N.id,O),onRemove:()=>oe(N.id),onSubmit:V,onPlaylistQualityChange:O=>He(N.id,O),onPlaylistItemQualityChange:(O,H)=>ft(N.id,O,H),onPlaylistToggle:()=>lt(N.id),onSyncChange:O=>pt(N.id,O),onPlaylistItemDelete:O=>ht(N.id,O)},N.id))}),s.jsxs("button",{type:"button",className:"capture-add-row",onClick:fe,children:[s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"8",y1:"2",x2:"8",y2:"14"}),s.jsx("line",{x1:"2",y1:"8",x2:"14",y2:"8"})]}),"Add another"]}),s.jsxs("div",{className:"capture-advanced",children:[s.jsxs("button",{type:"button",className:"capture-advanced-toggle",onClick:()=>E(N=>!N),"aria-expanded":w,children:[s.jsx("svg",{className:`capture-chevron${w?" capture-chevron--open":""}`,viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:s.jsx("polyline",{points:"4 6 8 10 12 6"})}),"Advanced options"]}),w&&s.jsxs("div",{className:"capture-advanced-panel",children:[s.jsxs("label",{className:"capture-ext-row",children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"uBlock Origin Lite"}),s.jsx("span",{className:"capture-ext-desc",children:"Block ads during this capture"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":de,className:`ext-toggle ext-toggle--sm${de?" ext-toggle--on":""}`,onClick:()=>_(N=>N===null?!de:!N),"aria-label":"Toggle uBlock for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Block cookie banners"}),s.jsx("span",{className:"capture-ext-desc",children:"Dismiss cookie consent banners during this capture"}),!(S!=null&&S.cookie_ext_available)&&s.jsxs("span",{className:"capture-ext-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_COOKIE_EXT"})]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":b,className:`ext-toggle ext-toggle--sm${b?" ext-toggle--on":""}`,onClick:()=>K(N=>!N),"aria-label":"Toggle cookie banner blocking for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Reader mode"}),s.jsx("span",{className:"capture-ext-desc",children:"Distil to article text via Readability (off by default)"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":te,className:`ext-toggle ext-toggle--sm${te?" ext-toggle--on":""}`,onClick:()=>z(N=>!N),"aria-label":"Toggle reader mode for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Close modals and dialogs"}),s.jsx("span",{className:"capture-ext-desc",children:"Auto-dismiss cookie banners and overlays during this capture"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":W,className:`ext-toggle ext-toggle--sm${W?" ext-toggle--on":""}`,onClick:()=>q(N=>!N),"aria-label":"Toggle modal closer for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Freedium mirror"}),s.jsx("span",{className:"capture-ext-desc",children:"Route paywalled articles through a Freedium mirror (Medium, NYT, WaPo, etc.)"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":ee,className:`ext-toggle ext-toggle--sm${ee?" ext-toggle--on":""}`,onClick:()=>me(N=>!N),"aria-label":"Toggle Freedium mirror for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]})]})]}),s.jsxs("div",{className:"capture-actions",children:[s.jsx("button",{type:"button",className:"capture-submit",onClick:V,disabled:Ye===0||R||Se||J,children:Ye>1?`Archive ${Ye}`:"Archive"}),s.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var N;return(N=u.current)==null?void 0:N.close()},children:"Cancel"})]})]})})}function Sm({item:e,autoFocus:t,onLocatorChange:n,onQualityChange:r,onRemove:l,onSubmit:i,onPlaylistQualityChange:a,onPlaylistItemQualityChange:o,onPlaylistToggle:u,onSyncChange:c,onPlaylistItemDelete:y}){const h=f.useRef(null);f.useEffect(()=>{var v;t&&((v=h.current)==null||v.focus())},[t]);const g=(()=>{if(Sn(e.locator)){if(e.playlistProbeState==="probing")return s.jsx("span",{className:"capture-quality-probing","aria-label":"Probing playlist…",children:"…"});if(e.playlistProbeState==="done"){const v=[...new Set(e.playlistItems.flatMap(p=>p.qualities.map(d=>parseInt(d))))].sort((p,d)=>d-p),k=e.playlistItems.every(p=>p.has_audio),j=e.playlistItems.filter(p=>p.quality===null).length;return s.jsxs(s.Fragment,{children:[s.jsxs("select",{className:"capture-quality",value:e.playlistQuality??"",onChange:p=>a(p.target.value),"aria-label":"Playlist quality",children:[!e.playlistQuality&&s.jsx("option",{value:"",disabled:!0,children:"Select quality…"}),s.jsx("option",{value:"best",children:"Best quality"}),v.map(p=>s.jsxs("option",{value:`${p}p`,children:[p,"p"]},p)),k&&s.jsx("option",{value:"audio",children:"Audio only"})]}),j>0&&s.jsxs("span",{className:"capture-conflict-badge",children:[j," need selection"]})]})}return e.playlistProbeState==="error"?s.jsx("span",{className:"capture-quality-hint capture-quality-hint--error",children:"Probe failed — edit URL to retry"}):null}if(!Ad(e.locator))return null;if(e.probeState==="probing")return s.jsx("span",{className:"capture-quality-probing","aria-label":"Checking available qualities",children:"…"});if(e.probeState==="done"){const v=e.probeQualities??[],k=e.probeHasAudio??!1;return v.length===0&&!k?s.jsx("span",{className:"capture-quality-hint",children:"No media detected"}):v.length===0&&k?s.jsx("select",{className:"capture-quality",value:"audio",onChange:j=>r(j.target.value),"aria-label":"Video quality",children:s.jsx("option",{value:"audio",children:"Audio only"})}):s.jsxs("select",{className:"capture-quality",value:e.quality||"best",onChange:j=>r(j.target.value),"aria-label":"Video quality",children:[s.jsx("option",{value:"best",children:"Best quality"}),v.map(j=>s.jsx("option",{value:j,children:j},j)),k&&s.jsx("option",{value:"audio",children:"Audio only"})]})}return null})(),x=Sn(e.locator)&&e.playlistProbeState==="done"?s.jsxs("label",{className:"capture-sync-row",children:[s.jsx("input",{type:"checkbox",checked:e.syncEnabled,onChange:v=>c(v.target.checked)}),s.jsx("span",{children:"Sync — skip already-archived videos"})]}):null;return s.jsxs("div",{className:"capture-row",children:[s.jsxs("div",{className:"capture-row-main",children:[Sn(e.locator)&&e.playlistProbeState==="done"?s.jsx("button",{type:"button",className:"capture-playlist-toggle capture-playlist-toggle--left",onClick:u,"aria-label":e.playlistExpanded?"Collapse video list":"Expand video list","aria-expanded":e.playlistExpanded,children:e.playlistExpanded?"▲":"▼"}):null,s.jsx("input",{ref:h,className:"capture-input",type:"text",placeholder:"https://… · yt:ID · ytm:ID · tweet:ID · x:ID",value:e.locator,onChange:v=>n(v.target.value),onKeyDown:v=>{v.key==="Enter"&&i()},autoComplete:"off",spellCheck:!1}),g,s.jsx("button",{type:"button",className:"capture-row-action capture-row-remove",onClick:l,"aria-label":"Remove",children:s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"3",y1:"3",x2:"13",y2:"13"}),s.jsx("line",{x1:"13",y1:"3",x2:"3",y2:"13"})]})})]}),e.error&&s.jsx("p",{className:"capture-row-error",children:e.error}),Sn(e.locator)&&e.playlistProbeState==="done"&&e.playlistExpanded?s.jsxs("div",{className:"capture-playlist-items",children:[e.playlistItems.map(v=>s.jsxs("div",{className:`capture-playlist-item${v.quality===null?" capture-playlist-item--conflict":""}`,children:[s.jsx("span",{className:"capture-playlist-item-title",children:v.title||v.url}),s.jsxs("select",{className:"capture-item-quality",value:v.quality??"",onChange:k=>o(v.id,k.target.value),"aria-label":`Quality for ${v.title||v.url}`,children:[v.quality===null&&s.jsx("option",{value:"",disabled:!0,children:"Choose…"}),s.jsx("option",{value:"best",children:"Best quality"}),v.qualities.map(k=>s.jsx("option",{value:k,children:k},k)),v.has_audio&&s.jsx("option",{value:"audio",children:"Audio only"})]}),v.quality===null&&s.jsx("span",{className:"capture-playlist-conflict-badge",children:"Choose quality"}),s.jsx("button",{type:"button",className:"capture-playlist-item-remove","aria-label":`Remove ${v.title||v.url}`,onClick:()=>y(v.id),children:"×"})]},v.id)),x]}):x]})}function Nm(){return s.jsxs("div",{className:"skeleton-row",children:[s.jsx("div",{className:"col-check","aria-hidden":"true"}),s.jsx("div",{className:"col-added",children:s.jsx("span",{className:"skeleton-cell",style:{width:108,height:13}})}),s.jsxs("div",{className:"col-title",style:{gap:"0.42em",display:"flex",alignItems:"center"},children:[s.jsx("span",{className:"skeleton-cell",style:{width:14,height:14,borderRadius:"50%",flexShrink:0}}),s.jsx("span",{className:"skeleton-cell",style:{width:"58%",height:13}})]}),s.jsx("div",{className:"col-type",children:s.jsx("span",{className:"skeleton-cell",style:{width:58,height:20,borderRadius:99}})}),s.jsx("div",{className:"col-size",children:s.jsx("span",{className:"skeleton-cell",style:{width:44,height:12}})}),s.jsx("div",{className:"col-url",children:s.jsx("span",{className:"skeleton-cell",style:{width:"65%",height:12}})})]})}function Gr(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&r").replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'")}function Ct(e){return _m(e)??""}function qa(e){if(!e)return"";const t=new Date(e);if(isNaN(t))return e;const n=r=>String(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const hu={youtube:'',youtube_music:'',spotify:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function Cs(e){return hu[e]??hu.other}function Md(e){return e.split("/").map(t=>t?t.split("-").map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join(" "):"").join("/")}function Cm({entry:e,index:t,onRowClick:n,selectedUids:r}){const l=(r==null?void 0:r.size)===1&&r.has(e.entry_uid),i=(r==null?void 0:r.size)>=2&&r.has(e.entry_uid),a=["child-entry-row",t%2===0?"child-entry-row--light":"child-entry-row--dark",l&&"is-selected",i&&"is-multi-selected"].filter(Boolean).join(" ");return s.jsxs("div",{className:a,tabIndex:0,"data-entry-uid":e.entry_uid,onMouseDown:o=>{o.shiftKey&&o.preventDefault()},onClick:o=>n(e,o),onKeyDown:o=>{o.key==="Enter"&&n(e,o)},children:[s.jsx("div",{className:"col-check","aria-hidden":"true"}),s.jsx("div",{className:"col-added",children:qa(e.archived_at)}),s.jsxs("div",{className:"col-title",children:[s.jsx("span",{className:"source-icon",children:s.jsx("span",{dangerouslySetInnerHTML:{__html:Cs(e.source_kind)}})}),s.jsx("span",{className:"entry-title",children:Ct(e.title)||Ct(e.entry_uid)})]}),s.jsx("div",{className:"col-type",children:s.jsx("span",{className:"type-pill",children:Ct(e.entity_kind)})}),s.jsx("div",{className:"col-size",children:s.jsx("span",{className:"size-total",children:Gr(e.total_artifact_bytes)})}),s.jsx("div",{className:"url-cell col-url",children:Ct(e.original_url)})]})}function Em({entry:e,archiveId:t,rowIndex:n,isSelected:r,isMultiSelected:l,onRowClick:i,selectedUids:a,deletedUids:o}){const[u,c]=f.useState(!1),[y,h]=f.useState(!1),[g,x]=f.useState(null),[v,k]=f.useState(!1),p=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!u?s.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>c(!0),style:{objectFit:"contain"}}):s.jsx("span",{dangerouslySetInnerHTML:{__html:Cs(e.source_kind)}}),d=r||l,m=e.child_count>0;function w(_){_.stopPropagation(),i(e,{ctrlKey:!0,metaKey:!1,shiftKey:!1,preventDefault(){}})}async function E(_){if(_.stopPropagation(),y){h(!1);return}if(h(!0),g===null&&!v){k(!0);try{const S=await Ph(t,e.entry_uid);x(S)}catch{x([])}finally{k(!1)}}}const P=["entry-row-outer",n%2===0?"entry-row-outer--light":"entry-row-outer--dark",r&&"is-selected",l&&"is-multi-selected"].filter(Boolean).join(" ");return s.jsxs("div",{className:P,"data-entry-uid":e.entry_uid,children:[s.jsxs("div",{className:"entry-row-main",tabIndex:0,onMouseDown:_=>{_.shiftKey&&_.preventDefault()},onClick:_=>i(e,_),onKeyDown:_=>{_.key==="Enter"&&i(e,_)},children:[s.jsx("div",{className:"col-check",children:s.jsx("button",{type:"button",className:`row-checkbox${d?" is-checked":""}`,"aria-pressed":d,"aria-label":d?"Deselect entry":"Select entry",onClick:w,onKeyDown:_=>_.stopPropagation()})}),s.jsx("div",{className:"col-added",children:qa(e.archived_at)}),s.jsxs("div",{className:"col-title",children:[m&&s.jsx("button",{type:"button",className:`entry-expand-btn${y?" is-expanded":""}`,"aria-label":y?"Collapse children":`Expand ${e.child_count} items`,"aria-expanded":y,onClick:E,onKeyDown:_=>_.stopPropagation()}),s.jsx("span",{className:"source-icon",children:p}),s.jsx("span",{className:"entry-title",children:Ct(e.title)||Ct(e.entry_uid)}),m&&s.jsx("span",{className:"child-count-badge","aria-hidden":"true",children:e.child_count})]}),s.jsx("div",{className:"col-type",children:s.jsx("span",{className:"type-pill",children:Ct(e.entity_kind)})}),s.jsxs("div",{className:"col-size",children:[s.jsx("span",{className:"size-total",children:Gr(e.total_artifact_bytes)}),e.cached_bytes>0&&e.cacheable_bytes>0&&s.jsxs("span",{className:"size-cached-pct",title:`${Gr(e.cached_bytes)} already on disk from an earlier entry`,children:[Math.round(e.cached_bytes/e.cacheable_bytes*100),"% cached"]})]}),s.jsx("div",{className:"url-cell col-url",children:Ct(e.original_url)})]}),y&&s.jsxs(s.Fragment,{children:[v&&s.jsx("div",{className:"child-entries-loading",children:"Loading…"}),s.jsx("div",{className:"child-entries","aria-label":`${e.child_count} child entries`,children:g&&g.filter(_=>!(o!=null&&o.has(_.entry_uid))).map((_,S)=>s.jsx(Cm,{entry:_,index:S,onRowClick:i,selectedUids:a},_.entry_uid))})]})]})}function Tm({entries:e,selectedUids:t,onRowClick:n,archiveId:r,pendingCaptures:l=[],deletedUids:i}){return s.jsx("section",{id:"archive-view",className:"view is-active",children:s.jsxs("div",{className:"entry-table",children:[s.jsxs("div",{className:"entry-header-row",children:[s.jsx("div",{className:"col-check","aria-hidden":"true"}),s.jsx("div",{className:"col-added",children:"Added"}),s.jsx("div",{className:"col-title",children:"Title"}),s.jsx("div",{className:"col-type",children:"Type"}),s.jsx("div",{className:"col-size",children:"Size"}),s.jsx("div",{className:"col-url",children:"Original URL"})]}),s.jsxs("div",{id:"entries-body",children:[l.filter(a=>a.archiveId===r).reverse().map(a=>s.jsx(Nm,{},a.id)),e.map((a,o)=>s.jsx(Em,{entry:a,rowIndex:o,archiveId:r,isSelected:t.size===1&&t.has(a.entry_uid),isMultiSelected:t.size>=2&&t.has(a.entry_uid),onRowClick:n,selectedUids:t,deletedUids:i},a.entry_uid))]})]})})}function bm(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 Pm({status:e}){const t=e==="completed"?"run-status--completed":e==="failed"?"run-status--failed":e==="in_progress"?"run-status--in-progress":"",n=e?e.replace(/_/g," "):"—";return s.jsx("span",{className:`run-status ${t}`,children:n})}function Lm({runs:e}){const[t,n]=f.useState(null);function r(l){n(i=>i===l?null:l)}return s.jsx("section",{id:"runs-view",className:"view is-active",children:s.jsxs("table",{className:"entry-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Started"}),s.jsx("th",{children:"Status"}),s.jsx("th",{children:"Requested"}),s.jsx("th",{children:"Completed"}),s.jsx("th",{children:"Failed"})]})}),s.jsx("tbody",{children:e.length===0?s.jsx("tr",{children:s.jsx("td",{colSpan:5,style:{color:"var(--muted)",padding:"24px 16px",textAlign:"center"},children:"No runs yet."})}):e.map(l=>{const i=l.status==="failed"&&l.error_summary,a=t===l.run_uid;return[s.jsxs("tr",{className:i?"run-row run-row--failed":"run-row",onClick:i?()=>r(l.run_uid):void 0,title:i?a?"Click to hide error":"Click to view error":void 0,children:[s.jsx("td",{children:bm(l.started_at)}),s.jsxs("td",{children:[s.jsx(Pm,{status:l.status}),i&&s.jsx("span",{className:"run-expand-hint","aria-hidden":"true",children:a?"▴":"▾"})]}),s.jsx("td",{children:l.requested_count??"—"}),s.jsx("td",{children:l.completed_count??"—"}),s.jsx("td",{children:l.failed_count??"—"})]},l.run_uid),i&&a&&s.jsx("tr",{className:"run-error-row",children:s.jsx("td",{colSpan:5,children:s.jsx("pre",{className:"run-error-detail",children:l.error_summary})})},`${l.run_uid}-detail`)]})})]})})}const zm=4;function Rm({archives:e}){const{currentUser:t}=f.useContext(Es)??{},n=t&&(t.role_bits&zm)!==0,[r,l]=f.useState("users"),[i,a]=f.useState([]),[o,u]=f.useState([]),[c,y]=f.useState(!1),[h,g]=f.useState(null),[x,v]=f.useState(""),[k,j]=f.useState(""),[p,d]=f.useState(""),[m,w]=f.useState(null),[E,P]=f.useState(!1),[_,S]=f.useState(""),[I,b]=f.useState(""),[K,W]=f.useState(null),[q,ee]=f.useState(!1),me=f.useCallback(async()=>{if(n){y(!0),g(null);try{const[T,B]=await Promise.all([Zh(),nm()]);a(T),u(B)}catch(T){g(T.message)}finally{y(!1)}}},[n]);f.useEffect(()=>{me()},[me]);async function de(T){const B=T.status==="active"?"disabled":"active";try{await tm(T.user_uid,B),a(G=>G.map(V=>V.user_uid===T.user_uid?{...V,status:B}:V))}catch(G){g(G.message)}}async function te(T){if(T.preventDefault(),!x.trim()||!k){w("Username and password required");return}P(!0),w(null);try{await em(x.trim(),k,p.trim()||void 0),v(""),j(""),d(""),await me()}catch(B){w(B.message)}finally{P(!1)}}async function z(T){if(T.preventDefault(),!_.trim()||!I.trim()){W("Slug and name required");return}ee(!0),W(null);try{await rm(_.trim(),I.trim()),S(""),b(""),await me()}catch(B){W(B.message)}finally{ee(!1)}}return n?s.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[s.jsx("h1",{children:"Admin"}),s.jsxs("div",{className:"view-tabs",children:[s.jsx("button",{className:`view-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),s.jsx("button",{className:`view-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),s.jsx("button",{className:`view-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),h&&s.jsx("div",{className:"form-msg form-msg--err",children:h}),r==="users"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Users"}),c?s.jsx("p",{className:"muted",children:"Loading…"}):s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Username"}),s.jsx("th",{children:"Email"}),s.jsx("th",{children:"Roles"}),s.jsx("th",{children:"Status"}),s.jsx("th",{children:"Actions"})]})}),s.jsx("tbody",{children:i.map(T=>s.jsxs("tr",{className:T.status==="disabled"?"admin-row-disabled":"",children:[s.jsx("td",{children:T.username}),s.jsx("td",{className:"muted",children:T.email||"—"}),s.jsx("td",{children:T.role_slugs.join(", ")||"—"}),s.jsx("td",{children:s.jsx("span",{className:`status-badge status-${T.status}`,children:T.status})}),s.jsx("td",{children:s.jsx("button",{className:"admin-action-btn",onClick:()=>de(T),children:T.status==="active"?"Ban":"Unban"})})]},T.user_uid))})]}),s.jsx("h3",{children:"Create User"}),s.jsxs("form",{className:"admin-form",onSubmit:te,children:[s.jsx("input",{className:"admin-input",placeholder:"Username",value:x,onChange:T=>v(T.target.value),required:!0}),s.jsx("input",{className:"admin-input",type:"password",placeholder:"Password (min 8 chars)",value:k,onChange:T=>j(T.target.value),required:!0}),s.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:p,onChange:T=>d(T.target.value)}),m&&s.jsx("div",{className:"form-msg form-msg--err",children:m}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:E,children:E?"Creating…":"Create User"})]})]}),r==="roles"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Roles"}),c?s.jsx("p",{className:"muted",children:"Loading…"}):s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Slug"}),s.jsx("th",{children:"Name"}),s.jsx("th",{children:"Level"}),s.jsx("th",{children:"Bit"}),s.jsx("th",{children:"Built-in"})]})}),s.jsx("tbody",{children:o.map(T=>s.jsxs("tr",{children:[s.jsx("td",{children:s.jsx("code",{children:T.slug})}),s.jsx("td",{children:T.name}),s.jsx("td",{children:T.level}),s.jsx("td",{children:T.bit_position}),s.jsx("td",{children:T.is_builtin?"✓":""})]},T.role_uid))})]}),s.jsx("h3",{children:"Create Custom Role"}),s.jsxs("form",{className:"admin-form",onSubmit:z,children:[s.jsx("input",{className:"admin-input",placeholder:"Slug (e.g. moderator)",value:_,onChange:T=>S(T.target.value),required:!0}),s.jsx("input",{className:"admin-input",placeholder:"Display Name (e.g. Moderator)",value:I,onChange:T=>b(T.target.value),required:!0}),K&&s.jsx("div",{className:"form-msg form-msg--err",children:K}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:q,children:q?"Creating…":"Create Role"})]})]}),r==="archives"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Mounted Archives"}),s.jsx("div",{className:"admin-list",children:e.map(T=>s.jsxs("div",{className:"admin-archive",children:[s.jsx("strong",{children:T.label}),s.jsx("div",{className:"muted",children:T.archive_path})]},T.id))})]})]}):s.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[s.jsx("h1",{children:"Admin"}),s.jsx("p",{className:"muted",children:"You need admin privileges to access this panel."}),s.jsx("h2",{children:"Mounted Archives"}),s.jsx("div",{className:"admin-list",children:e.map(T=>s.jsxs("div",{className:"admin-archive",children:[s.jsx("strong",{children:T.label}),s.jsx("div",{className:"muted",children:T.archive_path})]},T.id))})]})}function Fd({node:e,onPick:t}){var n;return s.jsxs("li",{children:[s.jsx("button",{className:"tag-picker-node-btn",title:e.tag.full_path,onClick:()=>t(e.tag),children:e.tag.slug}),((n=e.children)==null?void 0:n.length)>0&&s.jsx("div",{className:"tag-children",children:s.jsx("ul",{className:"tag-tree-list",children:e.children.map(r=>s.jsx(Fd,{node:r,onPick:t},r.tag.tag_uid))})})]})}function mu({title:e,tagNodes:t,excludeUid:n,onPick:r,onCancel:l}){f.useEffect(()=>{function o(u){u.key==="Escape"&&(u.preventDefault(),u.stopPropagation(),l())}return document.addEventListener("keydown",o),()=>document.removeEventListener("keydown",o)},[l]);function i(o){return o.filter(u=>u.tag.tag_uid!==n).map(u=>({...u,children:i(u.children)}))}const a=n?i(t):t;return s.jsx("div",{className:"tag-picker-backdrop",onClick:o=>{o.target===o.currentTarget&&l()},children:s.jsxs("div",{className:"tag-picker-modal",role:"dialog","aria-modal":"true",children:[s.jsxs("div",{className:"tag-picker-header",children:[s.jsx("span",{className:"tag-picker-title",children:e}),s.jsx("button",{className:"tag-picker-close",onClick:l,title:"Cancel","aria-label":"Cancel",children:"×"})]}),s.jsxs("div",{className:"tag-picker-body",children:[s.jsx("button",{className:"tag-picker-root-btn",onClick:()=>r(null),title:"Place at root level (no parent)",children:"↑ Root tag (no parent)"}),a.length>0?s.jsx("ul",{className:"tag-tree-list tag-picker-tree",children:a.map(o=>s.jsx(Fd,{node:o,onPick:r},o.tag.tag_uid))}):s.jsx("p",{className:"tag-picker-empty",children:"No other tags available."})]})]})})}function Bd({parentPath:e,archiveId:t,onDone:n,onCancel:r}){const[l,i]=f.useState(""),a=f.useRef(!1);async function o(){const u=l.trim();if(!u){r();return}const c=e?`${e}/${u}`:`/${u}`;try{await Oh(t,c),n()}catch(y){alert(y.message||"Create failed"),r()}}return s.jsx("input",{className:"tag-rename-input",autoFocus:!0,placeholder:"tag name",value:l,onChange:u=>i(u.target.value),onKeyDown:u=>{u.key==="Enter"&&u.currentTarget.blur(),u.key==="Escape"&&(a.current=!0,u.currentTarget.blur())},onBlur:()=>{a.current?(a.current=!1,r()):o()}})}function Hd({node:e,archiveId:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u,moveSelectMode:c,onMoveSourceSelect:y,pendingCreateParentUid:h,onCreateDone:g,onCreateCancel:x}){var K;const v=n===e.tag.full_path,[k,j]=f.useState(!1),[p,d]=f.useState(""),m=f.useRef(!1);function w(){if(k)return;if(c){y(e);return}const W=v?null:e.tag.full_path;r(W),l("archive")}function E(W){W.stopPropagation(),!c&&(d(e.tag.slug),j(!0))}async function P(){const W=p.trim();if(!W||W===e.tag.slug){j(!1);return}try{const q=await Dh(t,e.tag.tag_uid,W);i(e.tag.full_path,q.full_path),o()}catch{}finally{j(!1)}}async function _(W){var ee;W.stopPropagation();const q=((ee=e.children)==null?void 0:ee.length)>0?`Delete tag "${e.tag.full_path}" and all its child tags? This cannot be undone.`:`Delete tag "${e.tag.full_path}"? This cannot be undone.`;if(window.confirm(q))try{await $h(t,e.tag.tag_uid),a(e.tag.full_path),o()}catch{}}const S={archiveId:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u,moveSelectMode:c,onMoveSourceSelect:y,pendingCreateParentUid:h,onCreateDone:g,onCreateCancel:x},I=h===e.tag.tag_uid,b=((K=e.children)==null?void 0:K.length)>0;return s.jsxs("li",{children:[s.jsxs("div",{className:`tag-node-row${c?" tag-node-row--move-select":""}`,children:[k?s.jsx("input",{className:"tag-rename-input",autoFocus:!0,value:p,onChange:W=>d(W.target.value),onKeyDown:W=>{W.key==="Enter"&&W.currentTarget.blur(),W.key==="Escape"&&(m.current=!0,W.currentTarget.blur())},onBlur:()=>{m.current?(m.current=!1,j(!1)):P()}}):s.jsxs("button",{className:`tag-node-btn${v?" is-active":""}${c?" tag-node-btn--move-select":""}`,title:c?`Select "${e.tag.full_path}" to move`:e.tag.full_path,onClick:w,onDoubleClick:c?void 0:E,children:[s.jsx("span",{className:"tag-node-label",children:u?e.tag.name:e.tag.slug}),s.jsx("span",{className:"tag-node-count",children:e.children.length===0?`(${e.entry_count})`:`(${e.entry_count}) (${e.subtree_count} Total)`}),!c&&s.jsx("svg",{className:"edit-icon",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",title:"Rename tag",onClick:W=>{W.stopPropagation(),E(W)},children:s.jsx("path",{d:"M11.5 2.5a1.5 1.5 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"})})]}),!k&&!c&&s.jsx("button",{className:"remove tag-node-delete",title:`Delete "${e.tag.full_path}"`,onClick:_,"aria-label":`Delete "${e.tag.full_path}"`,children:"×"})]}),(b||I)&&s.jsx("div",{className:"tag-children",children:s.jsxs("ul",{className:"tag-tree-list",children:[e.children.map(W=>s.jsx(Hd,{node:W,...S},W.tag.tag_uid)),I&&s.jsx("li",{children:s.jsx(Bd,{parentPath:e.tag.full_path,archiveId:t,onDone:g,onCancel:x})})]})})]})}function Im({archiveId:e,tagNodes:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u}){const[c,y]=f.useState(null),[h,g]=f.useState(null);function x(){y("select-source"),g(null)}function v(){y(null),g(null)}f.useEffect(()=>{if(c!=="select-source")return;function q(ee){ee.key==="Escape"&&(ee.preventDefault(),ee.stopPropagation(),v())}return document.addEventListener("keydown",q),()=>document.removeEventListener("keydown",q)},[c]);function k(q){g(q),y("select-dest")}async function j(q){const ee=h.tag.full_path,me=h.tag.tag_uid,de=(q==null?void 0:q.tag_uid)??null;v();try{const te=await Ah(e,me,de);i(ee,te.full_path),o()}catch(te){alert(te.message||"Move failed")}}const[p,d]=f.useState(null),[m,w]=f.useState(void 0);function E(){d("select-parent"),w(void 0)}function P(){d(null),w(void 0)}function _(q){w(q??null),d("input")}function S(){d(null),w(void 0),o()}const I=c==="select-source",b=p==="input"?m?m.tag_uid:"__root__":void 0,K={archiveId:e,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u,moveSelectMode:I,onMoveSourceSelect:k,pendingCreateParentUid:b,onCreateDone:S,onCreateCancel:P},W=b==="__root__";return s.jsxs("section",{id:"tags-view",className:"view is-active",children:[s.jsxs("div",{className:"tag-tree",children:[s.jsx("div",{className:"tag-tree-header",children:I?s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"tag-tree-title tag-tree-title--move",children:"Select a tag to move"}),s.jsx("button",{className:"tag-tree-action-btn tag-tree-action-btn--cancel",onClick:v,children:"Cancel"})]}):s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"tag-tree-title",children:"Tags"}),n&&s.jsxs("span",{className:"tag-tree-active",title:n,children:["Filtering: ",n]}),s.jsxs("div",{className:"tag-tree-actions",children:[s.jsx("button",{className:"tag-tree-action-btn",onClick:E,title:"Create a new tag",disabled:!!p||!!c,children:"+ New"}),s.jsx("button",{className:"tag-tree-action-btn",onClick:x,title:"Move a tag to a different parent",disabled:!!p||!!c||t.length===0,children:"Move"})]})]})}),t.length===0&&!W?s.jsx("p",{className:"muted",style:{padding:"8px 0"},children:"No tags yet."}):s.jsxs("ul",{className:"tag-tree-list",children:[t.map(q=>s.jsx(Hd,{node:q,...K},q.tag.tag_uid)),W&&s.jsx("li",{children:s.jsx(Bd,{parentPath:null,archiveId:e,onDone:S,onCancel:P})})]})]}),p==="select-parent"&&s.jsx(mu,{title:"Create tag under…",tagNodes:t,excludeUid:null,onPick:_,onCancel:P}),c==="select-dest"&&h&&s.jsx(mu,{title:`Move "${h.tag.slug}" under…`,tagNodes:t,excludeUid:h.tag.tag_uid,onPick:j,onCancel:v})]})}const _r=[{value:0,label:"Private"},{value:2,label:"Users only"},{value:3,label:"Public"}],Dm=e=>{var t;return((t=_r.find(n=>n.value===e))==null?void 0:t.label)??String(e)};function $m({archiveId:e}){const[t,n]=f.useState([]),[r,l]=f.useState(!1),[i,a]=f.useState(null),[o,u]=f.useState(null),[c,y]=f.useState(null),[h,g]=f.useState(!1),[x,v]=f.useState(null),[k,j]=f.useState(""),[p,d]=f.useState(""),[m,w]=f.useState(2),[E,P]=f.useState(!1),[_,S]=f.useState(null),[I,b]=f.useState(""),[K,W]=f.useState(2),[q,ee]=f.useState(!1),[me,de]=f.useState(null),[te,z]=f.useState(!1),[T,B]=f.useState(""),G=f.useRef(null),V=t.find(R=>R.collection_uid===o)??null,fe=(V==null?void 0:V.slug)==="_default_",oe=f.useCallback(async()=>{if(e){l(!0),a(null);try{const R=await $d(e);n(R)}catch(R){a(R.message)}finally{l(!1)}}},[e]),$e=f.useCallback(async R=>{if(!R){y(null);return}g(!0),v(null);try{const J=await sm(e,R);y(J)}catch(J){v(J.message)}finally{g(!1)}},[e]);f.useEffect(()=>{oe()},[oe]),f.useEffect(()=>{$e(o)},[o,$e]),f.useEffect(()=>{te&&G.current&&G.current.focus()},[te]);async function Le(R){R.preventDefault();const J=k.trim(),Se=p.trim();if(!(!J||!Se)){P(!0),S(null);try{const N=await lm(e,J,Se,m);j(""),d(""),w(2),await oe(),u(N.collection_uid)}catch(N){S(N.message)}finally{P(!1)}}}async function He(){const R=T.trim();if(!R||!V){z(!1);return}try{await fu(e,V.collection_uid,{name:R}),await oe(),y(J=>J&&{...J,name:R})}catch(J){a(J.message)}finally{z(!1)}}async function ft(R){if(V)try{await fu(e,V.collection_uid,{default_visibility_bits:R}),await oe(),y(J=>J&&{...J,default_visibility_bits:R})}catch(J){a(J.message)}}async function lt(){if(V&&window.confirm(`Delete collection "${V.name}"? Entries will not be deleted.`))try{await um(e,V.collection_uid),u(null),y(null),await oe()}catch(R){a(R.message)}}async function pt(R){R.preventDefault();const J=I.trim();if(!(!J||!V)){ee(!0),de(null);try{await Od(e,V.collection_uid,J,K),b(""),await $e(V.collection_uid)}catch(Se){de(Se.message)}finally{ee(!1)}}}async function ht(R){if(V)try{await im(e,V.collection_uid,R),await $e(V.collection_uid)}catch(J){v(J.message)}}async function Ye(R,J){if(V)try{await am(e,V.collection_uid,R,J),y(Se=>Se&&{...Se,entries:Se.entries.map(N=>N.entry_uid===R?{...N,collection_visibility_bits:J}:N)})}catch(Se){v(Se.message)}}return e?s.jsxs("div",{className:"collections-view",children:[s.jsx("h2",{className:"collections-heading",children:"Collections"}),r&&s.jsx("div",{className:"muted",children:"Loading…"}),i&&s.jsxs("div",{className:"collections-error",children:[i," ",s.jsx("button",{onClick:()=>a(null),className:"coll-dismiss",children:"×"})]}),s.jsxs("div",{className:"collections-layout",children:[s.jsxs("div",{className:"collections-sidebar",children:[t.map(R=>s.jsxs("button",{className:`coll-sidebar-row${o===R.collection_uid?" is-active":""}`,onClick:()=>u(R.collection_uid),children:[s.jsx("span",{className:"coll-row-name",children:R.name}),s.jsx("span",{className:"coll-row-meta",children:Dm(R.default_visibility_bits)})]},R.collection_uid)),t.length===0&&!r&&s.jsx("div",{className:"muted",style:{padding:"8px 12px"},children:"No collections yet."})]}),V?s.jsxs("div",{className:"coll-detail",children:[s.jsxs("div",{className:"coll-detail-header",children:[te?s.jsx("input",{ref:G,className:"coll-rename-input",value:T,onChange:R=>B(R.target.value),onBlur:He,onKeyDown:R=>{R.key==="Enter"&&He(),R.key==="Escape"&&z(!1)}}):s.jsxs("h3",{className:`coll-detail-name${fe?"":" coll-detail-name--editable"}`,title:fe?void 0:"Click to rename",onClick:()=>{fe||(B(V.name),z(!0))},children:[(c==null?void 0:c.name)??V.name,!fe&&s.jsx("span",{className:"coll-edit-hint",children:" ✎"})]}),!fe&&s.jsx("button",{className:"coll-delete-btn",onClick:lt,title:"Delete collection",children:"Delete"})]}),s.jsxs("div",{className:"coll-detail-vis",children:[s.jsx("span",{className:"coll-vis-label",children:"Default visibility"}),s.jsx("select",{className:"coll-vis-select",value:(c==null?void 0:c.default_visibility_bits)??V.default_visibility_bits,onChange:R=>ft(Number(R.target.value)),disabled:fe,children:_r.map(R=>s.jsx("option",{value:R.value,children:R.label},R.value))})]}),s.jsxs("div",{className:"coll-entries-section",children:[s.jsx("div",{className:"coll-section-heading",children:"Entries"}),h&&s.jsx("div",{className:"muted",children:"Loading…"}),x&&s.jsx("div",{className:"collections-error",children:x}),!h&&c&&(c.entries.length===0?s.jsx("div",{className:"muted",children:"No entries in this collection."}):s.jsx("ul",{className:"coll-entries-list",children:c.entries.map(R=>s.jsxs("li",{className:"coll-entry-row",children:[s.jsxs("div",{className:"coll-entry-info",children:[s.jsx("span",{className:"coll-entry-title",children:R.title||R.entry_uid}),s.jsx("span",{className:"coll-entry-kind muted",children:R.source_kind})]}),s.jsxs("div",{className:"coll-entry-actions",children:[s.jsx("select",{className:"coll-entry-vis-select",value:R.collection_visibility_bits,onChange:J=>Ye(R.entry_uid,Number(J.target.value)),children:_r.map(J=>s.jsx("option",{value:J.value,children:J.label},J.value))}),!fe&&s.jsx("button",{className:"coll-entry-remove",onClick:()=>ht(R.entry_uid),title:"Remove from collection",children:"×"})]})]},R.entry_uid))}))]}),!fe&&s.jsxs("form",{className:"coll-add-entry-form",onSubmit:pt,children:[s.jsx("div",{className:"coll-section-heading",children:"Add entry"}),s.jsxs("div",{className:"coll-add-entry-row",children:[s.jsx("input",{className:"coll-add-entry-input",type:"text",value:I,onChange:R=>b(R.target.value),placeholder:"entry_uid",required:!0}),s.jsx("select",{className:"coll-vis-select",value:K,onChange:R=>W(Number(R.target.value)),children:_r.map(R=>s.jsx("option",{value:R.value,children:R.label},R.value))}),s.jsx("button",{className:"coll-add-btn",type:"submit",disabled:q,children:q?"…":"Add"})]}),me&&s.jsx("div",{className:"collections-error",style:{marginTop:4},children:me})]})]}):s.jsx("div",{className:"coll-detail coll-detail--empty",children:s.jsx("div",{className:"muted",children:"Select a collection to view details."})})]}),s.jsxs("details",{className:"coll-create-details",children:[s.jsx("summary",{children:"+ Create collection"}),s.jsxs("form",{className:"coll-create-form",onSubmit:Le,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-name",children:"Name"}),s.jsx("input",{className:"capture-input",id:"coll-name",type:"text",value:k,onChange:R=>{j(R.target.value),p||d(R.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-slug",children:"Slug"}),s.jsx("input",{className:"capture-input",id:"coll-slug",type:"text",value:p,onChange:R=>d(R.target.value),placeholder:"my-collection",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-vis",children:"Default visibility"}),s.jsx("select",{className:"capture-input",id:"coll-vis",style:{height:42},value:m,onChange:R=>w(Number(R.target.value)),children:_r.map(R=>s.jsx("option",{value:R.value,children:R.label},R.value))})]}),_&&s.jsx("div",{className:"collections-error",children:_}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:E,children:E?"Creating…":"Create collection"})]})]})]}):s.jsx("div",{className:"view-placeholder",children:"Select an archive."})}const Om=4;function Am({tab:e,onTabChange:t,archiveId:n}){const{currentUser:r,setCurrentUser:l}=f.useContext(Es)??{},i=r&&(r.role_bits&Om)!==0,a=["profile","tokens",...i?["instance","cookies","extensions","storage"]:[]],o={profile:"Profile",tokens:"API Tokens",instance:"Instance",cookies:"Cookies",extensions:"Extensions",storage:"Storage"};return s.jsxs("section",{className:"admin-view",children:[s.jsx("h1",{children:"Settings"}),s.jsx("div",{className:"view-tabs",children:a.map(u=>s.jsx("button",{className:`view-tab${e===u?" is-active":""}`,onClick:()=>t(u),children:o[u]},u))}),e==="profile"&&s.jsx(Mm,{currentUser:r,setCurrentUser:l}),e==="tokens"&&s.jsx(Fm,{}),e==="instance"&&i&&s.jsx(Bm,{}),e==="cookies"&&i&&s.jsx(Um,{}),e==="extensions"&&i&&s.jsx(Wm,{}),e==="storage"&&i&&s.jsx(Hm,{archiveId:n})]})}function Mm({currentUser:e,setCurrentUser:t}){const[n,r]=f.useState((e==null?void 0:e.display_name)??""),[l,i]=f.useState(!1),[a,o]=f.useState(null),[u,c]=f.useState(""),[y,h]=f.useState(""),[g,x]=f.useState(""),[v,k]=f.useState(!1),[j,p]=f.useState(null);async function d(w){w.preventDefault(),i(!0),o(null);try{await Kh(n),t(E=>({...E,display_name:n||null})),o({ok:!0,text:"Saved."})}catch(E){o({ok:!1,text:E.message})}finally{i(!1)}}async function m(w){if(w.preventDefault(),y!==g){p({ok:!1,text:"Passwords do not match."});return}k(!0),p(null);try{await Xh(u,y),c(""),h(""),x(""),p({ok:!0,text:"Password changed."})}catch(E){p({ok:!1,text:E.message})}finally{k(!1)}}return s.jsxs("div",{style:{maxWidth:440},children:[s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Display Name"}),s.jsxs("form",{onSubmit:d,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"display-name",children:"Name shown in the UI"}),s.jsx("input",{className:"field-input",id:"display-name",placeholder:(e==null?void 0:e.username)??"",value:n,onChange:w=>r(w.target.value)})]}),a&&s.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:l,children:l?"Saving…":"Save"})]})]}),s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Display Preferences"}),s.jsxs("label",{className:"checkbox-row",children:[s.jsx("input",{type:"checkbox",checked:(e==null?void 0:e.humanize_slugs)??!1,onChange:async w=>{const E=w.target.checked;try{await qh({humanize_slugs:E}),t(P=>({...P,humanize_slugs:E}))}catch{}}}),s.jsx("span",{className:"form-label",style:{margin:0},children:"Humanize tag display"})]}),s.jsx("p",{className:"muted",style:{fontSize:13,margin:"4px 0 0"},children:'When on, tag paths show as "X / Articles" instead of "x/articles".'})]}),s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Change Password"}),s.jsxs("form",{onSubmit:m,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"cur-pw",children:"Current password"}),s.jsx("input",{className:"field-input",id:"cur-pw",type:"password",value:u,onChange:w=>c(w.target.value),required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"new-pw",children:"New password"}),s.jsx("input",{className:"field-input",id:"new-pw",type:"password",value:y,onChange:w=>h(w.target.value),required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"confirm-pw",children:"Confirm new password"}),s.jsx("input",{className:"field-input",id:"confirm-pw",type:"password",value:g,onChange:w=>x(w.target.value),required:!0})]}),j&&s.jsx("div",{className:`form-msg form-msg--${j.ok?"ok":"err"}`,children:j.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:v,children:v?"Changing…":"Change Password"})]})]})]})}function Fm(){const[e,t]=f.useState([]),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,o]=f.useState(""),[u,c]=f.useState(!1),[y,h]=f.useState(null),g=f.useCallback(async()=>{r(!0),i(null);try{t(await Jh())}catch(k){i(k.message)}finally{r(!1)}},[]);f.useEffect(()=>{g()},[g]);async function x(k){if(k.preventDefault(),!!a.trim()){c(!0);try{const j=await Yh(a.trim());h(j),o(""),g()}catch(j){i(j.message)}finally{c(!1)}}}async function v(k){try{await Gh(k),t(j=>j.filter(p=>p.token_uid!==k))}catch(j){i(j.message)}}return s.jsx("div",{style:{maxWidth:600},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"API Tokens"}),y&&s.jsxs("div",{className:"token-banner",children:[s.jsx("strong",{children:"Token created."})," Copy it now — it won't be shown again.",s.jsx("code",{children:y.raw_token}),s.jsx("button",{className:"token-dismiss",onClick:()=>h(null),children:"Dismiss"})]}),s.jsxs("form",{className:"token-create-row",onSubmit:x,children:[s.jsx("input",{className:"field-input field-input--flex",placeholder:"Token name",value:a,onChange:k=>o(k.target.value),required:!0}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:u,children:u?"Creating…":"Create token"})]}),l&&s.jsx("div",{className:"form-msg form-msg--err",children:l}),n?s.jsx("div",{className:"muted",children:"Loading…"}):s.jsxs("div",{children:[e.length===0&&s.jsx("div",{className:"muted",children:"No tokens yet."}),e.map(k=>s.jsxs("div",{className:"token-row",children:[s.jsxs("div",{className:"token-row-info",children:[s.jsx("strong",{children:k.name}),s.jsxs("div",{className:"muted",children:["Created ",k.created_at.slice(0,10),k.last_used_at&&` · Last used ${k.last_used_at.slice(0,10)}`]})]}),s.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"4px 10px"},onClick:()=>v(k.token_uid),children:"Revoke"})]},k.token_uid))]})]})})}function Bm(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,o]=f.useState(!1),[u,c]=f.useState(null);f.useEffect(()=>{(async()=>{try{t(await Ka())}catch(h){i(h.message)}finally{r(!1)}})()},[]);async function y(h){h.preventDefault(),o(!0),c(null);try{await Al(e),c({ok:!0,text:"Saved."})}catch(g){c({ok:!1,text:g.message})}finally{o(!1)}}return n?s.jsx("div",{className:"muted",children:"Loading…"}):l?s.jsx("div",{className:"form-msg form-msg--err",children:l}):e?s.jsx("div",{style:{maxWidth:440},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Instance Settings"}),s.jsxs("form",{onSubmit:y,children:[[["public_index_enabled","Public index (unauthenticated browsing)"],["public_entry_content_enabled","Public entry content"],["open_registration_enabled","Open registration"]].map(([h,g])=>s.jsxs("label",{className:"checkbox-row",children:[s.jsx("input",{type:"checkbox",checked:!!e[h],onChange:x=>t(v=>({...v,[h]:x.target.checked}))}),g]},h)),s.jsxs("div",{className:"form-field",style:{marginTop:4},children:[s.jsx("label",{className:"form-label",children:"Default entry visibility"}),s.jsxs("select",{className:"field-input",value:e.default_entry_visibility,onChange:h=>t(g=>({...g,default_entry_visibility:Number(h.target.value)})),children:[s.jsx("option",{value:0,children:"Private"}),s.jsx("option",{value:2,children:"Unlisted"}),s.jsx("option",{value:3,children:"Public"})]})]}),u&&s.jsx("div",{className:`form-msg form-msg--${u.ok?"ok":"err"}`,children:u.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:a,children:a?"Saving…":"Save Settings"})]})]})}):null}function ri(e){if(e===0)return"0 B";const t=["B","KB","MB","GB","TB"],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/Math.pow(1024,n)).toFixed(n===0?0:1)} ${t[n]}`}function Hm({archiveId:e}){const[t,n]=f.useState("idle"),[r,l]=f.useState(null),[i,a]=f.useState(null),[o,u]=f.useState(null);function c(){n("idle"),l(null),a(null),u(null)}async function y(){n("scanning"),u(null),l(null);try{const x=await cm(e);l(x),n("scanned")}catch(x){u(x.message),n("error")}}async function h(){n("deleting"),u(null);try{const x=await dm(e);a(x),n("done")}catch(x){u(x.message),n("error")}}const g=r&&r.deletable_files===0&&r.orphaned_blob_rows===0;return s.jsx("div",{style:{maxWidth:440},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Orphan Cleanup"}),s.jsxs("p",{className:"muted",style:{marginBottom:16},children:["Scan for blob files and database records that are no longer referenced by any archive entry and safely delete them."," ",s.jsx("strong",{children:"Cleanup is blocked while captures are running."})]}),!e&&s.jsx("div",{className:"muted",children:"No archive selected."}),e&&t==="idle"&&s.jsx("button",{className:"btn-ghost",onClick:y,children:"Scan for orphaned blobs"}),t==="scanning"&&s.jsx("div",{className:"muted",children:"Scanning…"}),t==="scanned"&&r&&g&&s.jsxs(s.Fragment,{children:[s.jsx("div",{className:"form-msg form-msg--ok",children:"Archive is clean — nothing to remove."}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Done"})]}),t==="scanned"&&r&&!g&&s.jsxs("div",{children:[s.jsxs("div",{style:{marginBottom:14,lineHeight:1.6},children:["Found ",s.jsx("strong",{children:r.deletable_files})," unreferenced file",r.deletable_files!==1?"s":""," ","and ",s.jsx("strong",{children:r.orphaned_blob_rows})," orphaned DB record",r.orphaned_blob_rows!==1?"s":""," ","— ",s.jsx("strong",{children:ri(r.total_bytes)})," recoverable."]}),s.jsxs("div",{style:{display:"flex",gap:8},children:[s.jsxs("button",{className:"btn-danger",onClick:h,children:["Delete (",ri(r.total_bytes),")"]}),s.jsx("button",{className:"btn-ghost",onClick:c,children:"Cancel"})]})]}),t==="deleting"&&s.jsx("div",{className:"muted",children:"Deleting…"}),t==="done"&&i&&s.jsxs("div",{children:[s.jsxs("div",{className:"form-msg form-msg--ok",children:["Freed ",s.jsx("strong",{children:ri(i.freed_bytes)})," ","— removed ",i.deleted_files," file",i.deleted_files!==1?"s":""," ","and ",i.deleted_blob_rows," DB record",i.deleted_blob_rows!==1?"s":"","."]}),i.errors&&i.errors.length>0&&s.jsxs("div",{className:"form-msg form-msg--err",style:{marginTop:6},children:[i.errors.length," file",i.errors.length!==1?"s":""," could not be deleted."]}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Scan again"})]}),t==="error"&&s.jsxs("div",{children:[s.jsx("div",{className:"form-msg form-msg--err",children:o}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Try again"})]})]})})}function Um(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,o]=f.useState("global"),[u,c]=f.useState(""),[y,h]=f.useState("{}"),[g,x]=f.useState(null),[v,k]=f.useState(!1),[j,p]=f.useState({});f.useEffect(()=>{d()},[]);async function d(){r(!0),i(null);try{t(await pm())}catch(S){i(S.message)}finally{r(!1)}}async function m(S){S.preventDefault();try{JSON.parse(y)}catch{x({ok:!1,text:'cookies must be valid JSON, e.g. {"session": "abc"}'});return}k(!0),x(null);try{await hm(a==="global"?null:u.trim(),a,y),c(""),h("{}"),o("global"),x({ok:!0,text:"Rule added."}),await d()}catch(I){x({ok:!1,text:I.message})}finally{k(!1)}}async function w(S){try{await gm(S),await d()}catch(I){i(I.message)}}function E(S){p(I=>({...I,[S.rule_uid]:{cookiesInput:S.cookies_json,saving:!1,msg:null}}))}function P(S){p(I=>{const b={...I};return delete b[S],b})}async function _(S){const I=j[S.rule_uid];try{JSON.parse(I.cookiesInput)}catch{p(b=>({...b,[S.rule_uid]:{...b[S.rule_uid],msg:{ok:!1,text:"Invalid JSON"}}}));return}p(b=>({...b,[S.rule_uid]:{...b[S.rule_uid],saving:!0,msg:null}}));try{await mm(S.rule_uid,{cookies_json:I.cookiesInput}),P(S.rule_uid),await d()}catch(b){p(K=>({...K,[S.rule_uid]:{...K[S.rule_uid],saving:!1,msg:{ok:!1,text:b.message}}}))}}return n?s.jsx("div",{className:"muted",children:"Loading…"}):l?s.jsx("div",{className:"form-msg form-msg--err",children:l}):s.jsx("div",{style:{maxWidth:560},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Cookie Rules"}),s.jsx("p",{className:"muted",style:{marginBottom:12},children:"Cookies are injected into every capture network request (yt-dlp, HTTP downloads, web-page snapshots). Global rules apply to all URLs; wildcard and regex rules apply only to matching URLs."}),e&&e.length>0?s.jsxs("table",{className:"data-table",style:{width:"100%",marginBottom:16},children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Pattern"}),s.jsx("th",{children:"Cookies"}),s.jsx("th",{style:{width:100},children:"Actions"})]})}),s.jsx("tbody",{children:e.map(S=>{const I=j[S.rule_uid],b=S.url_pattern?s.jsxs(s.Fragment,{children:[s.jsxs("span",{className:"muted",children:[S.pattern_kind,":"]})," ",s.jsx("code",{children:S.url_pattern})]}):s.jsx("span",{className:"muted",children:"global (all URLs)"});return s.jsxs("tr",{children:[s.jsx("td",{children:b}),s.jsx("td",{children:I?s.jsxs(s.Fragment,{children:[s.jsx("textarea",{className:"field-input",style:{fontFamily:"monospace",fontSize:12,width:"100%",minHeight:60},value:I.cookiesInput,onChange:K=>p(W=>({...W,[S.rule_uid]:{...W[S.rule_uid],cookiesInput:K.target.value}}))}),I.msg&&s.jsx("div",{className:`form-msg form-msg--${I.msg.ok?"ok":"err"}`,children:I.msg.text}),s.jsxs("div",{style:{display:"flex",gap:8,marginTop:4},children:[s.jsx("button",{className:"btn-primary",style:{fontSize:12,padding:"2px 8px"},disabled:I.saving,onClick:()=>_(S),children:I.saving?"Saving…":"Save"}),s.jsx("button",{className:"btn-secondary",style:{fontSize:12,padding:"2px 8px"},onClick:()=>P(S.rule_uid),children:"Cancel"})]})]}):s.jsx("code",{style:{fontSize:12,wordBreak:"break-all"},children:S.cookies_json})}),s.jsx("td",{children:!I&&s.jsxs("div",{style:{display:"flex",gap:6},children:[s.jsx("button",{className:"btn-secondary",style:{fontSize:12,padding:"2px 8px"},onClick:()=>E(S),children:"Edit"}),s.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"2px 8px"},onClick:()=>w(S.rule_uid),children:"Del"})]})})]},S.rule_uid)})})]}):s.jsx("p",{className:"muted",style:{marginBottom:16},children:"No cookie rules defined."}),s.jsx("h3",{style:{marginBottom:8},children:"Add Rule"}),s.jsxs("form",{onSubmit:m,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:"Pattern type"}),s.jsxs("select",{className:"field-input",value:a,onChange:S=>o(S.target.value),children:[s.jsx("option",{value:"global",children:"Global (all URLs)"}),s.jsx("option",{value:"wildcard",children:"Wildcard (e.g. *.youtube.com)"}),s.jsx("option",{value:"regex",children:"Regex (matched against full URL)"})]})]}),a!=="global"&&s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:a==="wildcard"?"URL/hostname pattern":"Regex pattern"}),s.jsx("input",{className:"field-input",type:"text",value:u,onChange:S=>c(S.target.value),placeholder:a==="wildcard"?"*.youtube.com or https://example.com/*":".*\\.youtube\\.com.*",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:"Cookies (JSON object)"}),s.jsx("textarea",{className:"field-input",style:{fontFamily:"monospace",fontSize:13,minHeight:70},value:y,onChange:S=>h(S.target.value),placeholder:'{"SESSION": "abc123", "token": "xyz"}',required:!0})]}),g&&s.jsx("div",{className:`form-msg form-msg--${g.ok?"ok":"err"}`,children:g.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:v,children:v?"Adding…":"Add Rule"})]})]})})}function Wm(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(!1),[a,o]=f.useState(null);f.useEffect(()=>{(async()=>{try{t(await Ka())}catch(j){o({ok:!1,text:j.message})}finally{r(!1)}})()},[]);async function u(j){i(!0),o(null);try{await Al({ublock_enabled:j}),t(p=>({...p,ublock_enabled:j})),o({ok:!0,text:"Saved."})}catch(p){o({ok:!1,text:p.message})}finally{i(!1)}}async function c(j){i(!0),o(null);try{await Al({cookie_ext_enabled:j}),t(p=>({...p,cookie_ext_enabled:j})),o({ok:!0,text:"Saved."})}catch(p){o({ok:!1,text:p.message})}finally{i(!1)}}async function y(j){i(!0),o(null);try{await Al({modal_closer_enabled:j}),t(p=>({...p,modal_closer_enabled:j})),o({ok:!0,text:"Saved."})}catch(p){o({ok:!1,text:p.message})}finally{i(!1)}}if(n)return s.jsx("div",{className:"muted",children:"Loading\\u2026"});const h=(e==null?void 0:e.ublock_ext_available)??!1,g=(e==null?void 0:e.ublock_enabled)??!0,x=(e==null?void 0:e.cookie_ext_available)??!1,v=(e==null?void 0:e.cookie_ext_enabled)??!0,k=(e==null?void 0:e.modal_closer_enabled)??!0;return s.jsx("div",{children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Extensions"}),s.jsx("p",{className:"form-hint",style:{marginBottom:20},children:"Extensions run inside the browser during WebPage captures and can block ads, accept cookie banners, and more. Changes take effect on the next capture."}),s.jsxs("div",{className:"ext-grid",children:[s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"uBlock Origin Lite"}),s.jsx("span",{className:"ext-card-desc",children:"Blocks ads, trackers, and other page clutter during archiving via Chrome’s declarativeNetRequest API (Manifest V3)."}),!h&&s.jsxs("span",{className:"ext-card-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_UBLOCK_EXT"})," to the unpacked extension directory to enable."]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":g,className:`ext-toggle${g?" ext-toggle--on":""}`,onClick:()=>u(!g),disabled:l,"aria-label":"Toggle uBlock Origin Lite",children:s.jsx("span",{className:"ext-toggle-knob"})})]})}),s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"I Still Don’t Care About Cookies"}),s.jsx("span",{className:"ext-card-desc",children:"Dismiss cookie consent banners during archiving."}),!x&&s.jsxs("span",{className:"ext-card-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_COOKIE_EXT"})," to the unpacked extension directory to enable."]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":v,className:`ext-toggle${v?" ext-toggle--on":""}`,onClick:()=>c(!v),disabled:l,"aria-label":"Toggle I Still Don't Care About Cookies",children:s.jsx("span",{className:"ext-toggle-knob"})})]})}),s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"Modal & Dialog Closer"}),s.jsx("span",{className:"ext-card-desc",children:"Auto-dismiss cookie banners, consent overlays, and other modal dialogs before a WebPage capture is taken. Implemented as an injected browser script; no external extension required."})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":k,className:`ext-toggle${k?" ext-toggle--on":""}`,onClick:()=>y(!k),disabled:l,"aria-label":"Toggle Modal and Dialog Closer",children:s.jsx("span",{className:"ext-toggle-knob"})})]})})]}),a&&s.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text})]})})}const gu={0:"Private",1:"Public",2:"Users only",3:"Public"},Vm=()=>s.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:s.jsx("path",{d:"M7 17 17 7M9 7h8v8"})});function Qm({archiveId:e,selectedEntry:t,selectedUids:n,selectedEntries:r,detail:l,onTagFilterSet:i,tagNodes:a,onTagsRefresh:o,onEntryTitleChange:u,onEntryDeleted:c,onBulkDeleted:y,humanizeTags:h,onDetailRefresh:g,onOpenPreview:x,onPlay:v}){var il;const[k,j]=f.useState([]),[p,d]=f.useState(""),[m,w]=f.useState([]),[E,P]=f.useState(""),_=f.useRef(0),S=f.useRef(!1),[I,b]=f.useState(!1),[K,W]=f.useState(""),[q,ee]=f.useState("idle"),[me,de]=f.useState(""),te=f.useRef(null),[z,T]=f.useState(!1);f.useEffect(()=>{T(!1)},[(il=l==null?void 0:l.summary)==null?void 0:il.entry_uid]);const B=(n==null?void 0:n.size)>=2,[G,V]=f.useState(""),[fe,oe]=f.useState("idle"),[$e,Le]=f.useState(""),[He,ft]=f.useState([]),[lt,pt]=f.useState(""),[ht,Ye]=f.useState("idle"),[R,J]=f.useState(""),[Se,N]=f.useState("idle");f.useEffect(()=>{const D=++_.current;if(te.current&&(clearInterval(te.current),te.current=null),ee("idle"),de(""),!t||!e){j([]),w([]);return}b(!1),W(""),S.current=!1,j([]),Promise.all([Nl(e,t.entry_uid),om(e,t.entry_uid)]).then(([le,st])=>{D===_.current&&(j(le),w(st))}).catch(()=>{})},[t,e]),f.useEffect(()=>()=>{clearInterval(te.current)},[]),f.useEffect(()=>{if(!B||!e){ft([]);return}$d(e).then(ft).catch(()=>ft([]))},[B,e]),f.useEffect(()=>{V(""),oe("idle"),Le(""),pt(""),Ye("idle"),J(""),N("idle")},[n]);async function F(){const D=n.size;if(!window.confirm(`Delete ${D} entr${D===1?"y":"ies"}? This cannot be undone.`))return;N("running");const le=new Set;for(const st of n)try{await du(e,st),le.add(st)}catch{}N("idle"),y==null||y(le)}async function O(){const D=G.trim();if(D){oe("running"),Le("");try{for(const le of n)await cu(e,le,D);V(""),oe("done"),o==null||o(),setTimeout(()=>oe("idle"),1800)}catch(le){Le(le.message),oe("error")}}}async function H(){if(!lt)return;Ye("running"),J("");const D=[];for(const le of n)try{await Od(e,lt,le)}catch{D.push(le)}D.length>0?(J(`Failed for ${D.length} entr${D.length===1?"y":"ies"}.`),Ye("error")):(Ye("done"),setTimeout(()=>Ye("idle"),1800))}async function U(){const D=K.trim()||null;try{await Rh(e,t.entry_uid,D),u==null||u(t.entry_uid,D)}catch{}finally{b(!1)}}async function X(){const D=p.trim();if(!(!D||!t))try{await cu(e,t.entry_uid,D),d(""),P("");const le=await Nl(e,t.entry_uid);j(le),o()}catch(le){P(le.message)}}async function pe(D){try{await Ih(e,t.entry_uid,D);const le=await Nl(e,t.entry_uid);j(le),o()}catch{}}async function ze(){if(!(!t||!e)&&window.confirm("Delete this entry? This cannot be undone."))try{await du(e,t.entry_uid),c==null||c(t.entry_uid)}catch{}}async function Ue(){if(!t||!e||q==="running")return;const D=_.current,le=t.entry_uid;ee("running"),de("");try{const{job_uid:st}=await fm(e,le);if(_.current!==D)return;te.current=setInterval(async()=>{try{const mn=await Dd(e,st);if(mn.status==="completed"){if(clearInterval(te.current),te.current=null,_.current!==D)return;ee("done");const cr=await Nl(e,le);if(_.current!==D)return;j(cr),g==null||g()}else if(mn.status==="failed"){if(clearInterval(te.current),te.current=null,_.current!==D)return;ee("error"),de(mn.error_text||"Re-archive failed.")}}catch{if(clearInterval(te.current),te.current=null,_.current!==D)return;ee("error"),de("Network error while polling.")}},500)}catch(st){if(_.current!==D)return;ee("error"),de(st.message||"Failed to start re-archive.")}}const We=l?[["Added",qa(l.summary.archived_at)],["Source",l.summary.source_kind],["Type",l.summary.entity_kind],["Visibility",gu[l.summary.visibility]??l.summary.visibility],["Root",l.structured_root_relpath]]:[],Ve=new Set(["mp3","ogg","m4a","opus","wav","flac","aac"]),xe=new Set(["mp4","webm","mov","mkv","avi","m4v","ogv","pdf","html","htm","jpg","jpeg","png","gif","webp","avif","svg","bmp"]),Dt=l?l.artifacts.findIndex(D=>D.artifact_role==="primary_media"):-1,Qe=Dt>=0?l.artifacts[Dt]:null,ll=Qe?Qe.relpath.split(".").pop().toLowerCase():"",sl=Qe&&Ve.has(ll),Ts=Dt>=0&&t?`/api/archives/${e}/entries/${t.entry_uid}/artifacts/${Dt}`:null,bs=l&&!sl&&(l.summary.entity_kind==="tweet"||l.summary.entity_kind==="tweet_thread"||Qe&&xe.has(ll));return s.jsxs("aside",{className:"context-rail",children:[s.jsx("div",{className:"rail-eyebrow",children:"Context"}),B?s.jsxs("div",{className:"bulk-panel",children:[s.jsxs("p",{className:"bulk-count",children:[s.jsx("span",{className:"bulk-count-num",children:n.size})," entries selected"]}),s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Assign tag"}),$e&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:$e}),s.jsxs("div",{className:"tag-input-wrap",children:[s.jsx("span",{className:"hash",children:"/"}),s.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:G,onChange:D=>V(D.target.value),onKeyDown:D=>{D.key==="Enter"&&O()}}),s.jsx("button",{className:"tag-add-btn",onClick:O,disabled:fe==="running"||!G.trim(),children:fe==="running"?"…":fe==="done"?"✓":"Add"})]})]}),He.length>0&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Add to collection"}),s.jsxs("div",{className:"bulk-coll-row",children:[s.jsxs("select",{className:"bulk-coll-select",value:lt,onChange:D=>pt(D.target.value),children:[s.jsx("option",{value:"",children:"Pick a collection…"}),He.map(D=>s.jsx("option",{value:D.collection_uid,children:D.name},D.collection_uid))]}),s.jsx("button",{className:"tag-add-btn",onClick:H,disabled:!lt||ht==="running",children:ht==="running"?"…":ht==="done"?"✓":ht==="error"?"!":"Add"})]}),R&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"6px 0 0"},children:R})]}),s.jsx("div",{className:"rail-delete-zone",children:s.jsx("button",{className:"rail-delete-btn",onClick:F,disabled:Se==="running",children:Se==="running"?"Deleting…":`Delete ${n.size} entr${n.size===1?"y":"ies"}`})})]}):t?l?s.jsxs(s.Fragment,{children:[I?s.jsx("input",{className:"rail-title-input",autoFocus:!0,value:K,onChange:D=>W(D.target.value),onKeyDown:D=>{D.key==="Enter"&&D.currentTarget.blur(),D.key==="Escape"&&(S.current=!0,D.currentTarget.blur())},onBlur:()=>{S.current?b(!1):U(),S.current=!1}}):s.jsxs("h2",{className:"rail-title rail-title--editable",title:"Click to rename",onClick:()=>{W(l.summary.title??""),b(!0)},children:[Ct(l.summary.title)||Ct(l.summary.entry_uid),s.jsx("svg",{className:"edit-icon",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:s.jsx("path",{d:"M11.5 2.5a1.5 1.5 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"})})]}),l.summary.original_url&&s.jsxs("a",{className:"url-tile",href:l.summary.original_url,target:"_blank",rel:"noopener noreferrer",children:[s.jsx("span",{className:"ico",dangerouslySetInnerHTML:{__html:Cs(l.summary.source_kind)}}),s.jsx("span",{className:"u-text",children:l.summary.original_url}),s.jsx("span",{className:"ext",children:s.jsx(Vm,{})})]}),sl&&v&&s.jsx("button",{className:"rail-preview-btn",onClick:()=>v(Ts,t),children:"▶ Play"}),bs&&x&&s.jsx("button",{className:"rail-preview-btn",onClick:x,children:"Preview"}),s.jsx("div",{className:"meta-list",children:We.filter(([,D])=>D!=null&&D!=="").map(([D,le])=>s.jsxs("div",{className:"meta-item",children:[s.jsx("span",{className:"meta-k",children:D}),s.jsx("span",{className:`meta-v${D==="Root"?" mono":""}`,children:Ct(le)})]},D))}),l.artifacts.length>0&&(()=>{const D=l.artifacts.map((ke,$t)=>({...ke,_idx:$t})),le=D.filter(ke=>ke.artifact_role==="font"),st=D.filter(ke=>ke.artifact_role!=="font"),mn=le.reduce((ke,$t)=>ke+($t.byte_size||0),0),cr=l.summary.entry_uid,gn=ke=>s.jsx("li",{children:s.jsxs("a",{href:`/api/archives/${e}/entries/${cr}/artifacts/${ke._idx}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[s.jsx("span",{className:"artifact-name",children:ke.artifact_role==="font"?ke.relpath.split("/").pop():ke.artifact_role.replace(/_/g," ")}),s.jsx("span",{className:"artifact-size",children:ke.byte_size!=null?Gr(ke.byte_size):"—"})]})},ke._idx);return s.jsxs("div",{className:"rail-section",children:[s.jsxs("div",{className:"rail-section-heading",children:["Artifacts ",s.jsx("span",{className:"num",children:l.artifacts.length})]}),s.jsxs("ul",{className:"artifact-list",children:[st.map(gn),le.length>0&&s.jsxs("li",{className:"artifact-group",children:[s.jsxs("button",{type:"button",className:"artifact-group-header artifact-link","aria-expanded":z,onClick:()=>T(ke=>!ke),children:[s.jsxs("span",{className:"artifact-name",children:[s.jsx("span",{"aria-hidden":"true",className:`artifact-group-chevron${z?" open":""}`,children:"›"}),` fonts (${le.length})`]}),s.jsx("span",{className:"artifact-size",children:Gr(mn)})]}),z&&s.jsx("ul",{className:"artifact-list artifact-group-body",children:le.map(gn)})]})]})]})})()]}):s.jsx("p",{className:"tags-empty",children:"Loading\\u2026"}):s.jsx("p",{className:"tags-empty",children:"Select an entry."}),t&&!B&&s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Tags"}),k.length===0?s.jsx("p",{className:"tags-empty",children:"No tags yet."}):s.jsx("div",{className:"tags-wrap",children:k.map(D=>s.jsxs("span",{className:"tag-pill",title:D.full_path,children:[h?Md(D.full_path):D.full_path,s.jsx("button",{className:"remove",title:`Remove tag ${D.full_path}`,onClick:()=>pe(D.tag_uid),children:"×"})]},D.tag_uid))}),E&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:E}),s.jsxs("div",{className:"tag-input-wrap",children:[s.jsx("span",{className:"hash",children:"/"}),s.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:p,onChange:D=>d(D.target.value),onKeyDown:D=>{D.key==="Enter"&&X()}}),s.jsx("button",{className:"tag-add-btn",onClick:X,children:"Add"})]})]}),m.length>0&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Collections"}),m.map(D=>s.jsxs("div",{className:"coll-row",children:[s.jsx("span",{className:"coll-name",children:D.collection_uid}),s.jsx("span",{className:"vis-badge",children:gu[D.visibility_bits]??`bits:${D.visibility_bits}`})]},D.collection_uid))]}),l&&(l.summary.entity_kind==="tweet"||l.summary.entity_kind==="tweet_thread")&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Actions"}),s.jsx("button",{className:"rail-rearchive-btn",onClick:Ue,disabled:q==="running",children:q==="running"?"Re-archiving…":"Re-archive"}),q==="done"&&s.jsx("p",{className:"form-msg form-msg--ok",style:{marginTop:"6px"},children:"Re-archived successfully."}),q==="error"&&s.jsx("p",{className:"form-msg form-msg--err",style:{marginTop:"6px"},children:me})]}),s.jsx("div",{className:"rail-delete-zone",children:s.jsx("button",{className:"rail-delete-btn",onClick:ze,children:"Delete entry"})})]})]})}function Km({src:e}){const t=f.useRef(null);return f.useEffect(()=>{t.current&&t.current.load()},[e]),s.jsx("div",{className:"preview-video-wrap",style:{background:"#111",display:"flex",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",minHeight:"240px"},children:e?s.jsxs("video",{ref:t,controls:!0,autoPlay:!1,style:{width:"100%",maxHeight:"100%",display:"block"},children:[s.jsx("source",{src:e}),"Your browser does not support the video element."]}):s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No video available"})})}function yu({src:e,type:t,title:n,originalUrl:r}){const l=r||e,i=n||null;return s.jsxs("div",{className:"preview-iframe-wrap",style:{flex:1,minHeight:0,display:"flex",flexDirection:"column"},children:[s.jsxs("div",{className:"preview-iframe-toolbar",style:{display:"flex",flexDirection:"column",gap:"2px",padding:"6px 12px",borderBottom:"1px solid var(--line-soft)",background:"var(--paper-2)",flexShrink:0},children:[i&&s.jsx("span",{style:{fontSize:"0.85rem",fontWeight:600,color:"var(--ink)",fontFamily:"var(--sans)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:i}),s.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[s.jsx("span",{style:{flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontSize:"0.78rem",color:"var(--muted)",fontFamily:"var(--sans)"},children:l}),s.jsx("a",{href:e,target:"_blank",rel:"noreferrer noopener",style:{fontSize:"0.78rem",color:"var(--accent)",textDecoration:"none",whiteSpace:"nowrap",fontFamily:"var(--sans)",flexShrink:0},children:t==="pdf"?"Open PDF ↗":"Open in new tab ↗"})]})]}),s.jsx("iframe",{src:e,sandbox:"allow-same-origin allow-popups",allow:"autoplay 'none'",referrerPolicy:"no-referrer",style:{flex:1,border:"none",width:"100%",minHeight:0},title:i||(t==="pdf"?"PDF preview":"Page preview")})]})}function qm({src:e,alt:t}){return s.jsx("div",{className:"preview-image-wrap",style:{display:"flex",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",background:"var(--paper-2)",overflow:"hidden"},children:s.jsx("a",{href:e,target:"_blank",rel:"noreferrer noopener",style:{display:"contents"},children:s.jsx("img",{src:e,alt:t||"",style:{objectFit:"contain",maxHeight:"100%",maxWidth:"100%",display:"block",cursor:"pointer"}})})})}function Ud(e){return e?new Intl.DateTimeFormat("en-US",{month:"short",day:"numeric",year:"numeric"}).format(new Date(e*1e3)):""}function vu(e){return!e||!e.includes("&")?e:e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'")}const Y={card:{background:"var(--paper)",border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",maxWidth:"560px",margin:"0 auto",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},threadOuter:{border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",maxWidth:"560px",margin:"0 auto",background:"var(--paper)",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},tweetRow:{display:"flex",gap:"10px",padding:"10px 12px"},tweetRowThread:{display:"flex",gap:"10px",padding:"10px 12px 0"},leftCol:{display:"flex",flexDirection:"column",alignItems:"center",flexShrink:0,width:"36px"},rightCol:{flex:1,minWidth:0,paddingBottom:"8px"},avatar:{width:"36px",height:"36px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},avatarPh:{width:"36px",height:"36px",borderRadius:"50%",background:"var(--line)",flexShrink:0},threadLine:{flex:1,width:"2px",background:"var(--line-soft, var(--line))",margin:"4px 0",minHeight:"12px",borderRadius:"1px"},authorRow:{display:"flex",alignItems:"baseline",gap:"4px",flexWrap:"wrap",marginBottom:"4px",lineHeight:"1.3"},authorName:{fontWeight:"700",fontSize:"14px",color:"var(--ink)"},authorHandle:{fontSize:"13px",color:"var(--muted)"},datePart:{fontSize:"13px",color:"var(--muted)"},tweetText:{fontSize:"14px",lineHeight:"1.5",color:"var(--ink)",whiteSpace:"pre-line",marginBottom:"8px",wordBreak:"break-word"},stats:{display:"flex",gap:"12px",fontSize:"13px",color:"var(--muted)"},link:{color:"var(--accent)",textDecoration:"none"},loading:{padding:"24px 16px",textAlign:"center",color:"var(--muted)",fontSize:"14px"},error:{padding:"24px 16px",textAlign:"center",color:"var(--alert)",fontSize:"14px"},mediaGrid:{marginBottom:"8px",borderRadius:"10px",overflow:"hidden",border:"1px solid var(--line)"},mediaImg:{display:"block",width:"100%",objectFit:"cover",maxHeight:"260px"},mediaVideo:{display:"block",width:"100%",maxHeight:"260px",background:"#000"},article:{maxWidth:"560px",margin:"0 auto",border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",background:"var(--paper)",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},aCover:{width:"100%",display:"block",objectFit:"cover",maxHeight:"280px"},aMeta:{padding:"10px 14px 0"},aTweetTitle:{fontSize:"20px",fontWeight:"800",letterSpacing:"-0.3px",color:"var(--ink)",lineHeight:"1.3",marginBottom:"8px"},aAuthorRow:{display:"flex",alignItems:"center",gap:"8px",marginBottom:"8px"},aAvatar:{width:"36px",height:"36px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},aAvatarPh:{width:"36px",height:"36px",borderRadius:"50%",background:"var(--line)",flexShrink:0},aAuthorName:{fontSize:"14px",fontWeight:"700",color:"var(--ink)",lineHeight:"1.3"},aAuthorSub:{fontSize:"13px",color:"var(--muted)",lineHeight:"1.3"},aDivider:{border:"none",borderTop:"1px solid var(--line)",margin:"0"},aBody:{padding:"4px 14px 16px"},bH1:{fontSize:"22px",fontWeight:"800",letterSpacing:"-0.4px",color:"var(--ink)",lineHeight:"1.25",margin:"16px 0 6px"},bH2:{fontSize:"18px",fontWeight:"700",letterSpacing:"-0.2px",color:"var(--ink)",lineHeight:"1.3",margin:"14px 0 4px"},bP:{fontSize:"15px",color:"var(--ink)",lineHeight:"1.65",marginBottom:"12px",marginTop:"0"},bSpacer:{height:"4px",display:"block"},bQuote:{borderLeft:"3px solid var(--line)",padding:"2px 12px",margin:"12px 0",color:"var(--muted)",fontSize:"15px",lineHeight:"1.6"},bHr:{border:"none",borderTop:"1px solid var(--line)",margin:"14px 0"},bImg:{width:"100%",display:"block",borderRadius:"8px",margin:"12px 0"},bUl:{margin:"8px 0 12px",paddingLeft:"24px"},bOl:{margin:"8px 0 12px",paddingLeft:"24px"},bLi:{fontSize:"15px",color:"var(--ink)",lineHeight:"1.6",marginBottom:"4px"},bTweet:{display:"flex",alignItems:"center",gap:"10px",border:"1px solid var(--line)",borderRadius:"10px",padding:"10px 14px",margin:"12px 0",color:"var(--muted)",fontSize:"14px",textDecoration:"none"},bMdPre:{borderRadius:"8px",margin:"10px 0",overflow:"auto",background:"var(--paper-3, var(--field))",padding:"12px 14px"},bMdCode:{fontFamily:"ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, monospace",fontSize:"12px",lineHeight:"1.6",color:"var(--ink)",background:"transparent",display:"block"},iCode:{fontFamily:"ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace",fontSize:"0.875em",background:"var(--paper-3, var(--field))",padding:"1px 5px",borderRadius:"4px",color:"var(--ink)"},qtBadge:{fontSize:"11px",color:"var(--muted)",display:"inline-flex",alignItems:"center",gap:"2px",marginLeft:"4px",letterSpacing:"0.03em",flexShrink:0},lightboxBackdrop:{position:"fixed",inset:0,zIndex:500,background:"rgba(0,0,0,0.92)",display:"flex",alignItems:"center",justifyContent:"center",padding:"20px"},lightboxContent:{display:"flex",flexDirection:"column",alignItems:"center",gap:"10px",maxWidth:"90vw",maxHeight:"90vh"},lightboxToolbar:{display:"flex",alignItems:"center",gap:"10px",alignSelf:"flex-end"},lightboxImg:{maxWidth:"88vw",maxHeight:"80vh",objectFit:"contain",borderRadius:"6px",display:"block"},lightboxNav:{display:"flex",gap:"12px",alignItems:"center"},lightboxNavBtn:{background:"rgba(255,255,255,0.18)",border:"none",color:"#fff",fontSize:"20px",cursor:"pointer",borderRadius:"50%",width:"36px",height:"36px",display:"flex",alignItems:"center",justifyContent:"center",lineHeight:1,flexShrink:0},lightboxBtn:{background:"rgba(255,255,255,0.18)",border:"none",color:"#fff",fontSize:"16px",cursor:"pointer",borderRadius:"50%",width:"30px",height:"30px",display:"flex",alignItems:"center",justifyContent:"center",lineHeight:1,flexShrink:0},lightboxLink:{color:"rgba(255,255,255,0.7)",textDecoration:"none",fontSize:"14px"},lightboxCounter:{color:"rgba(255,255,255,0.6)",fontSize:"13px"}},Ce={bg:"#000000",border:"#2f3336",text:"#e7e9ea",dim:"#71767b",accent:"#1d9bf0",codeBg:"#16181c"},li={article:{background:Ce.bg,color:Ce.text,minHeight:"100%",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'},articleInner:{maxWidth:"598px",margin:"0 auto",paddingBottom:"80px"},aCover:{width:"100%",display:"block",objectFit:"cover",maxHeight:"420px"},aMeta:{padding:"20px 16px 0"},aTweetTitle:{fontSize:"34px",fontWeight:"800",letterSpacing:"-0.5px",color:Ce.text,lineHeight:"44px",marginBottom:"16px"},aAuthorRow:{display:"flex",alignItems:"center",gap:"12px",marginBottom:"16px"},aAvatar:{width:"40px",height:"40px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},aAvatarPh:{width:"40px",height:"40px",borderRadius:"50%",background:Ce.border,flexShrink:0},aAuthorName:{fontSize:"15px",fontWeight:"700",color:Ce.text,lineHeight:"1.3"},aAuthorSub:{fontSize:"15px",color:Ce.dim,lineHeight:"1.3"},aDivider:{border:"none",borderTop:`1px solid ${Ce.border}`,margin:"0"},aBody:{padding:"4px 16px 0"},bH1:{fontSize:"26px",fontWeight:"800",letterSpacing:"-0.4px",color:Ce.text,lineHeight:"36px",margin:"24px 0 10px"},bH2:{fontSize:"26px",fontWeight:"800",letterSpacing:"-0.4px",color:Ce.text,lineHeight:"36px",margin:"24px 0 10px"},bP:{fontSize:"17px",color:Ce.text,lineHeight:"1.5",marginBottom:"16px",marginTop:"0"},bSpacer:{height:"6px",display:"block"},bQuote:{borderLeft:`3px solid ${Ce.border}`,padding:"2px 14px",margin:"14px 0",color:Ce.dim,fontSize:"17px",lineHeight:"1.5"},bHr:{border:"none",borderTop:`1px solid ${Ce.border}`,margin:"28px 0"},bImg:{width:"100%",display:"block",borderRadius:"12px",margin:"16px 0"},bUl:{margin:"10px 0 16px",paddingLeft:"28px"},bOl:{margin:"10px 0 16px",paddingLeft:"28px"},bLi:{fontSize:"17px",color:Ce.text,lineHeight:"1.5",marginBottom:"6px"},bTweet:{display:"flex",alignItems:"center",gap:"12px",border:`1px solid ${Ce.border}`,borderRadius:"12px",padding:"14px 16px",margin:"14px 0",color:Ce.dim,fontSize:"15px",textDecoration:"none"},bMdPre:{borderRadius:"12px",margin:"12px 0",overflow:"auto",background:"#1e2029"},bMdCode:{fontFamily:"ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, monospace",fontSize:"13px",lineHeight:"1.65",padding:"18px 20px",display:"block",color:Ce.text,background:"transparent"},iCode:{fontFamily:"ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace",fontSize:"0.875em",background:Ce.codeBg,padding:"2px 6px",borderRadius:"4px",color:"#e6edf3"},link:{color:Ce.accent,textDecoration:"none"}};function Xm(e,t,n){const r={};return n&&n.forEach((l,i)=>{l.relpath&&(r[l.relpath]=`/api/archives/${e}/entries/${t}/artifacts/${i}`)}),r}function Zn(e,t,n){return e&&n[e]?n[e]:t||null}function Wd(){return s.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true",style:{flexShrink:0,color:"var(--ink)"},children:s.jsx("path",{d:"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.748l7.73-8.835L1.254 2.25H8.08l4.259 5.63L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"})})}function Xa(e,t,...n){var r;if(e.fromIndex!=null&&e.toIndex!=null)return{s:e.fromIndex,e:e.toIndex};if(((r=e.indices)==null?void 0:r.length)===2)return{s:e.indices[0],e:e.indices[1]};if(t)for(const l of n){if(!l)continue;const i=t.indexOf(l);if(i!==-1)return{s:i,e:i+l.length}}return null}function Vd(e,t){const n=e.expanded_url||e.url||e.text||"",r=e.display_url||e.expanded_url||e.url||e.text||"",l=Xa(e,t,e.url,e.text,e.display_url,e.expanded_url);return!l||!n?null:{...l,kind:"url",href:n,display:r}}function Qd(e,t){const n=e.screen_name||e.name||e.text||"",r=n?`@${n}`:null,l=Xa(e,t,r);return!l||!n?null:{...l,kind:"mention",screen_name:n}}const xu=/https?:\/\/[^\s<>"'\])]+/g,Jm=/[.,;:!?)()]+$/;function us(e,t){const n=[];let r=0,l;for(xu.lastIndex=0;(l=xu.exec(e))!==null;){l.index>r&&n.push(e.slice(r,l.index));let i=l[0].replace(Jm,"");n.push(s.jsx("a",{href:i,target:"_blank",rel:"noopener noreferrer",style:t,children:i},l.index));const a=l[0].slice(i.length);a&&n.push(a),r=l.index+l[0].length}return r===0?e:(rVd(o,e)).filter(Boolean),...(t.user_mentions||[]).map(o=>Qd(o,e)).filter(Boolean)];if(r.length===0&&n.length===0)return us(vu(e),Y.link);const l=new Set([0,e.length]);for(const o of r)o.s>=0&&o.s<=e.length&&l.add(o.s),o.e>=0&&o.e<=e.length&&l.add(o.e);for(const[o,u]of n)o>=0&&o<=e.length&&l.add(o),u>=0&&u<=e.length&&l.add(u);const i=(o,u)=>n.some(([c,y])=>c<=o&&y>=u),a=[...l].sort((o,u)=>o-u);return a.slice(0,-1).map((o,u)=>{const c=a[u+1];if(i(o,c))return null;const y=e.slice(o,c),h=r.filter(v=>v.s<=o&&v.e>=c),g=h.find(v=>v.kind==="url");if(g)return s.jsx("a",{href:g.href,target:"_blank",rel:"noopener noreferrer",style:Y.link,children:g.display||y},u);const x=h.find(v=>v.kind==="mention");return x?s.jsx("a",{href:`https://x.com/${x.screen_name}`,target:"_blank",rel:"noopener noreferrer",style:Y.link,children:y},u):s.jsx("span",{children:us(vu(y),Y.link)},u)}).filter(o=>o!==null)}function Gm(e,t,n,r,l=Y){if(!e)return null;t=t||[],n=n||[],r=r||[];const i=[];for(const u of t)u.length>0&&i.push({s:u.offset,e:u.offset+u.length,kind:"style",style:u.style});for(const u of n){const c=Vd(u,e);c&&i.push(c)}for(const u of r){const c=Qd(u,e);c&&i.push(c)}if(i.length===0)return us(e,l.link);const a=new Set([0,e.length]);for(const u of i)u.s>=0&&u.s<=e.length&&a.add(u.s),u.e>=0&&u.e<=e.length&&a.add(u.e);const o=[...a].sort((u,c)=>u-c);return o.slice(0,-1).map((u,c)=>{const y=o[c+1],h=i.filter(j=>j.s<=u&&j.e>=y),g=e.slice(u,y);let x;if(g.includes(` +`)){const j=g.split(` +`);x=j.flatMap((p,d)=>dj.kind==="style"&&j.style==="Code")&&(x=s.jsx("code",{style:l.iCode,children:x})),h.some(j=>j.kind==="style"&&j.style==="Bold")&&(x=s.jsx("strong",{children:x})),h.some(j=>j.kind==="style"&&j.style==="Italic")&&(x=s.jsx("em",{children:x})),h.some(j=>j.kind==="style"&&j.style==="Underline")&&(x=s.jsx("u",{children:x})),h.some(j=>j.kind==="style"&&j.style==="Strikethrough")&&(x=s.jsx("s",{children:x}));const v=h.find(j=>j.kind==="url");if(v){const j=/^https?:\/\/t\.co\//i.test(x);x=s.jsx("a",{href:v.href,target:"_blank",rel:"noopener noreferrer",style:l.link,children:j?v.display:x})}const k=h.find(j=>j.kind==="mention");return k&&(x=s.jsx("a",{href:`https://x.com/${k.screen_name}`,target:"_blank",rel:"noopener noreferrer",style:l.link,children:x})),s.jsx("span",{children:typeof x=="string"?us(x,l.link):x},c)})}function Zm(e,t,n){const r=(n==null?void 0:n.st)||Y,l=e.resolved_entities||[];return l.length===0?null:l.map((i,a)=>{var o;switch(i.type){case"divider":return s.jsx("hr",{style:r.bHr},a);case"media":{const u=Zn(i.local_path,i.url,t);return u?s.jsx("a",{href:u,target:"_blank",rel:"noopener noreferrer",style:{display:"block",cursor:"zoom-in"},onClick:c=>{var y;!c.metaKey&&!c.ctrlKey&&(c.preventDefault(),(y=n==null?void 0:n.onImgClick)==null||y.call(n,u))},children:s.jsx("img",{src:u,style:r.bImg,loading:"lazy",alt:""})},a):null}case"tweet":return i.tweet_id?s.jsxs("a",{href:`https://x.com/i/status/${i.tweet_id}`,target:"_blank",rel:"noopener noreferrer",style:r.bTweet,children:[s.jsx(Wd,{}),"View post on X"]},a):null;case"link":return i.url?s.jsx("p",{style:r.bP,children:s.jsx("a",{href:i.url,target:"_blank",rel:"noopener noreferrer",style:r.link,children:i.url})},a):null;case"markdown":{const u=i.markdown??((o=i.data)==null?void 0:o.markdown)??"";return s.jsx("pre",{style:r.bMdPre,children:s.jsx("code",{style:r.bMdCode,children:u})},a)}case"emoji":return i.url?s.jsx("img",{src:i.url,alt:"",style:{height:"1.2em",verticalAlign:"middle",margin:"0 1px"}},a):null;default:return null}})}function si(e,t,n,r){const l=(r==null?void 0:r.st)||Y,i=e.type||"",a=e.text||"",o=e.inline_style_ranges||[],u=e.data||{},c=Gm(a,o,u.urls||[],u.mentions||[],l);switch(i){case"header-one":return s.jsx("h1",{style:l.bH1,children:c},t);case"header-two":{const y=a.match(/(?:x\.com|twitter\.com)\/i\/status\/(\d+)/);return y?s.jsxs("a",{href:`https://x.com/i/status/${y[1]}`,target:"_blank",rel:"noopener noreferrer",style:l.bTweet,children:[s.jsx(Wd,{}),"View post on X"]},t):s.jsx("h2",{style:l.bH2,children:c},t)}case"unstyled":return a.trim()?s.jsx("p",{style:l.bP,children:c},t):s.jsx("span",{style:l.bSpacer},t);case"blockquote":return s.jsx("blockquote",{style:l.bQuote,children:c},t);case"unordered-list-item":case"ordered-list-item":return s.jsx("li",{style:l.bLi,children:c},t);case"atomic":return s.jsx("span",{children:Zm(e,n,r)},t);default:return a?s.jsx("p",{style:l.bP,children:c},t):null}}function eg(e,t,n){const r=(n==null?void 0:n.st)||Y,l=[];let i=0;for(;i{r&&(l==null||l(!0))},[r,l]);const o=r?li:Y,u=e.cover_media||{},c=e.author||t||{},y=e.first_published_at_secs?Ud(e.first_published_at_secs):"",g=[c.screen_name?`@${c.screen_name}`:"",y].filter(Boolean).join(" · "),x=Zn(u.local_path,u.url,n),v=Zn(c.avatar_local_path,c.avatar_url,n),k=s.jsxs(s.Fragment,{children:[i&&s.jsx(Kd,{items:[{src:i,alt:""}],startIndex:0,onClose:()=>a(null)}),x&&s.jsx("a",{href:x,target:"_blank",rel:"noopener noreferrer",style:{display:"block",cursor:"zoom-in"},onClick:j=>{!j.metaKey&&!j.ctrlKey&&(j.preventDefault(),a(x))},children:s.jsx("img",{src:x,style:o.aCover,alt:"Article cover"})}),s.jsxs("div",{style:o.aMeta,children:[e.title&&s.jsx("div",{style:o.aTweetTitle,children:e.title}),s.jsxs("div",{style:o.aAuthorRow,children:[v?s.jsx("img",{src:v,style:o.aAvatar,alt:c.name||""}):s.jsx("div",{style:o.aAvatarPh}),s.jsxs("div",{children:[s.jsx("div",{style:o.aAuthorName,children:c.name||c.screen_name||"Unknown"}),g&&s.jsx("div",{style:o.aAuthorSub,children:g})]})]})]}),s.jsx("hr",{style:o.aDivider}),s.jsx("div",{style:o.aBody,children:eg(e.blocks||[],n,{onImgClick:a,st:o})})]});return r?s.jsx("div",{style:li.article,children:s.jsx("div",{style:li.articleInner,children:k})}):s.jsx("div",{style:Y.article,children:k})}function ng({photos:e,onOpen:t}){const n=e.length;if(n===0)return null;if(n===1)return s.jsx("a",{href:e[0].src,target:"_blank",rel:"noopener noreferrer",style:{display:"block"},onClick:l=>{!l.metaKey&&!l.ctrlKey&&(l.preventDefault(),t(0))},children:s.jsx("img",{src:e[0].src,alt:e[0].alt||"",style:Y.mediaImg,loading:"lazy"})});const r=n<=2?"180px":"140px";return s.jsx("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gridTemplateRows:n===2?r:`${r} ${r}`,gap:"2px"},children:e.map((l,i)=>s.jsx("a",{href:l.src,target:"_blank",rel:"noopener noreferrer",style:{display:"block",overflow:"hidden",gridRow:n===3&&i===0?"span 2":void 0},onClick:a=>{!a.metaKey&&!a.ctrlKey&&(a.preventDefault(),t(i))},children:s.jsx("img",{src:l.src,alt:l.alt||"",loading:"lazy",style:{width:"100%",height:"100%",objectFit:"cover",display:"block"}})},i))})}function Kd({items:e,startIndex:t,onClose:n}){const[r,l]=f.useState(t);f.useEffect(()=>{const a=o=>{(o.key==="Escape"||o.key==="ArrowLeft"||o.key==="ArrowRight")&&(o.stopPropagation(),o.preventDefault()),o.key==="Escape"&&n(),o.key==="ArrowRight"&&l(u=>Math.min(u+1,e.length-1)),o.key==="ArrowLeft"&&l(u=>Math.max(u-1,0))};return document.addEventListener("keydown",a),()=>document.removeEventListener("keydown",a)},[n,e.length]);const i=e[r];return s.jsx("div",{style:Y.lightboxBackdrop,onClick:n,children:s.jsxs("div",{style:Y.lightboxContent,onClick:a=>a.stopPropagation(),children:[s.jsxs("div",{style:Y.lightboxToolbar,children:[e.length>1&&s.jsxs("span",{style:Y.lightboxCounter,children:[r+1," / ",e.length]}),s.jsx("a",{href:i.src,target:"_blank",rel:"noopener noreferrer",style:Y.lightboxLink,title:"Open in new tab",children:"↗"}),s.jsx("button",{style:Y.lightboxBtn,onClick:n,"aria-label":"Close",children:"×"})]}),s.jsx("img",{src:i.src,alt:i.alt||"",style:Y.lightboxImg}),e.length>1&&s.jsxs("div",{style:Y.lightboxNav,children:[s.jsx("button",{style:Y.lightboxNavBtn,onClick:()=>l(a=>Math.max(a-1,0)),disabled:r===0,children:"‹"}),s.jsx("button",{style:Y.lightboxNavBtn,onClick:()=>l(a=>Math.min(a+1,e.length-1)),disabled:r===e.length-1,children:"›"})]})]})})}function wu({tweet:e,isInThread:t,isLast:n,artifactMap:r}){var d,m;const[l,i]=f.useState(null),a=e.author||{},o=e.created_at_secs?Ud(e.created_at_secs):"",u=e.entities||{},c=Zn(a.avatar_local_path,a.avatar_url,r),y=t&&!n,h=e.is_quote_status===!0,g=t?Y.tweetRowThread:Y.tweetRow,x=e.full_text||"",v=((m=(d=e.extended_entities)==null?void 0:d.media)!=null&&m.length?e.extended_entities.media:u.media)||[],k=[],j=[];for(const w of v)if(w.type==="photo"){const E=Zn(w.local_path,w.media_url_https,r);E&&k.push({kind:"photo",src:E,alt:w.alt_text||""})}else if(w.type==="video"||w.type==="animated_gif"){const E=w.local_path&&r[w.local_path]||(()=>{var _,S;return(S=(((_=w.video_info)==null?void 0:_.variants)||[]).filter(I=>I.content_type==="video/mp4").sort((I,b)=>(b.bitrate||0)-(I.bitrate||0))[0])==null?void 0:S.url})();E&&j.push({kind:w.type==="animated_gif"?"gif":"video",src:E})}const p=[];for(const w of v){if(!w.url||!(w.type==="photo"?k.some(_=>_.src===Zn(w.local_path,w.media_url_https,r)):j.length>0))continue;const P=Xa(w,x,w.url);P&&p.push([P.s,P.e])}return s.jsxs(s.Fragment,{children:[l!==null&&s.jsx(Kd,{items:k,startIndex:l,onClose:()=>i(null)}),s.jsxs("div",{style:g,children:[s.jsxs("div",{style:Y.leftCol,children:[c?s.jsx("img",{src:c,style:Y.avatar,alt:a.name||""}):s.jsx("div",{style:Y.avatarPh}),y&&s.jsx("div",{style:Y.threadLine})]}),s.jsxs("div",{style:Y.rightCol,children:[s.jsxs("div",{style:Y.authorRow,children:[s.jsx("span",{style:Y.authorName,children:a.name||a.screen_name||"Unknown"}),a.screen_name&&s.jsxs("span",{style:Y.authorHandle,children:["@",a.screen_name]}),o&&s.jsxs("span",{style:Y.datePart,children:["· ",o]}),h&&s.jsx("span",{style:Y.qtBadge,title:"Quote tweet",children:"↻ QT"})]}),s.jsx("div",{style:Y.tweetText,children:Ym(x,u,p)}),k.length>0&&s.jsx("div",{style:Y.mediaGrid,children:s.jsx(ng,{photos:k,onOpen:w=>i(w)})}),j.map((w,E)=>s.jsx("div",{style:Y.mediaGrid,children:s.jsx("video",{src:w.src,style:Y.mediaVideo,controls:!0,loop:w.kind==="gif",muted:w.kind==="gif",autoPlay:w.kind==="gif"})},E)),(e.retweet_count>0||e.favorite_count>0)&&s.jsxs("div",{style:Y.stats,children:[e.favorite_count>0&&s.jsxs("span",{children:["❤️ ",e.favorite_count.toLocaleString()]}),e.retweet_count>0&&s.jsxs("span",{children:["🔁 ",e.retweet_count.toLocaleString()]})]})]})]})]})}function rg({archiveId:e,entryUid:t,artifacts:n,entityKind:r,fullPage:l,onXArticle:i}){const[a,o]=f.useState(!0),[u,c]=f.useState(null),[y,h]=f.useState([]);if(f.useEffect(()=>{if(o(!0),c(null),h([]),!n||!e||!t){o(!1);return}const v=n.map((j,p)=>({...j,index:p})).filter(j=>j.artifact_role==="raw_tweet_json");if(v.length===0){c("No tweet data found."),o(!1);return}let k=!1;return Lh(e,t,v.map(j=>j.index)).then(async j=>{if(k)return;const p=/https?:\/\/t\.co\/[A-Za-z0-9]+/g,d=_=>{var I;const S=_.full_text||"";return(((I=_.entities)==null?void 0:I.urls)||[]).map(b=>{var K;if(b.fromIndex!=null&&b.toIndex!=null)return[b.fromIndex,b.toIndex];if(((K=b.indices)==null?void 0:K.length)===2)return[b.indices[0],b.indices[1]];if(b.url){const W=S.indexOf(b.url);if(W!==-1)return[W,W+b.url.length]}return null}).filter(Boolean)},m=(_,S,I)=>_.some(([b,K])=>b<=S&&K>=I),w=new Set;for(const _ of j){const S=_.full_text||"",I=d(_);let b;for(p.lastIndex=0;(b=p.exec(S))!==null;)m(I,b.index,b.index+b[0].length)||w.add(b[0])}const E=w.size>0?await zh([...w]).catch(()=>({})):{},P=j.map(_=>{var W;const S=_.full_text||"",I=d(_),b=[];let K;for(p.lastIndex=0;(K=p.exec(S))!==null;){const q=K[0],ee=E[q];ee&&ee!==q&&!m(I,K.index,K.index+q.length)&&b.push({url:q,expanded_url:ee,display_url:(()=>{try{const me=new URL(ee);return me.hostname+(me.pathname.length>1?"/…":"")}catch{return ee}})(),fromIndex:K.index,toIndex:K.index+q.length})}return b.length===0?_:{..._,entities:{..._.entities||{},urls:[...((W=_.entities)==null?void 0:W.urls)||[],...b]}}});k||h(P)}).catch(j=>{k||c(j.message||"Failed to load tweet.")}).finally(()=>{k||o(!1)}),()=>{k=!0}},[e,t,n]),a)return s.jsx("div",{style:Y.loading,children:"Loading…"});if(u)return s.jsxs("div",{style:Y.error,children:["Error: ",u]});if(y.length===0)return null;const g=Xm(e,t,n);if(r==="tweet_thread")return s.jsx("div",{style:Y.threadOuter,children:y.map((v,k)=>s.jsx(wu,{tweet:v,isInThread:!0,isLast:k===y.length-1,artifactMap:g},v.id||k))});const x=y[0];return x.is_article&&x.article?s.jsx(tg,{article:x.article,tweetAuthor:x.author,artifactMap:g,fullPage:l,onXArticle:i}):s.jsx("div",{style:Y.card,children:s.jsx(wu,{tweet:x,isInThread:!1,isLast:!0,artifactMap:g})})}const lg=new Set(["mp4","webm","mov","mkv","avi","m4v","ogv"]),sg=new Set(["mp3","ogg","m4a","opus","wav","flac","aac"]),ig=new Set(["jpg","jpeg","png","gif","webp","avif","svg","bmp"]);function qd({archiveId:e,entry:t,detail:n,fullPage:r,onXArticle:l}){if(!t)return s.jsx("div",{className:"preview-panel preview-panel--empty",children:s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"Select an entry to preview"})});if(!n)return s.jsx("div",{className:"preview-panel preview-panel--loading",children:s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"Loading…"})});const{summary:i,artifacts:a}=n,o=i.entry_uid,u=i.entity_kind;if(u==="tweet"||u==="tweet_thread"){const x=s.jsx(rg,{archiveId:e,entryUid:o,artifacts:a,entityKind:u,fullPage:r,onXArticle:l});return r?x:s.jsx("div",{className:"preview-tweet-wrap",children:x})}const c=a.findIndex(x=>x.artifact_role==="primary_media");if(c===-1)return s.jsxs("div",{className:"preview-panel preview-panel--no-preview",children:[s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No preview available"}),a.length>0&&s.jsx("ul",{style:{marginTop:"12px",paddingLeft:"20px",fontSize:"0.8rem",color:"var(--muted-2)",fontFamily:"var(--sans)"},children:a.map((x,v)=>s.jsxs("li",{children:[x.artifact_role,": ",x.relpath]},v))})]});const y=a[c],h=`/api/archives/${e}/entries/${o}/artifacts/${c}`,g=y.relpath.split(".").pop().toLowerCase();return lg.has(g)?s.jsx("div",{className:"preview-panel",children:s.jsx(Km,{src:h})}):sg.has(g)?s.jsxs("div",{className:"preview-panel preview-panel--audio",style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:"12px",padding:"24px",fontFamily:"var(--sans)"},children:[s.jsx("span",{style:{fontSize:"2rem"},children:"🎵"}),s.jsx("span",{style:{color:"var(--ink)",fontSize:"0.95rem",fontWeight:600},children:i.title||o}),s.jsx("audio",{src:h,controls:!0,style:{marginTop:"8px",width:"100%",maxWidth:"400px"}})]}):g==="pdf"?s.jsx("div",{className:"preview-panel",style:{flex:1,minHeight:0},children:s.jsx(yu,{src:h,type:"pdf",title:i.title,originalUrl:i.original_url})}):g==="html"||g==="htm"?s.jsx("div",{className:"preview-panel",style:{flex:1,minHeight:0},children:s.jsx(yu,{src:h,type:"page",title:i.title,originalUrl:i.original_url})}):ig.has(g)?s.jsx("div",{className:"preview-panel",style:{height:"100%"},children:s.jsx(qm,{src:h,alt:i.title||"Image"})}):s.jsxs("div",{className:"preview-panel preview-panel--no-preview",children:[s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No preview available"}),s.jsx("ul",{style:{marginTop:"12px",paddingLeft:"20px",fontSize:"0.8rem",color:"var(--muted-2)",fontFamily:"var(--sans)"},children:a.map((x,v)=>s.jsxs("li",{children:[x.artifact_role,": ",x.relpath]},v))})]})}function ag({archiveId:e,entry:t,detail:n,onClose:r}){var l,i;return f.useEffect(()=>{const a=o=>{o.key==="Escape"&&r()};return window.addEventListener("keydown",a),()=>window.removeEventListener("keydown",a)},[r]),s.jsx("div",{className:"preview-modal-backdrop",onClick:r,children:s.jsxs("div",{className:`preview-modal${((l=n==null?void 0:n.summary)==null?void 0:l.entity_kind)==="tweet"||((i=n==null?void 0:n.summary)==null?void 0:i.entity_kind)==="tweet_thread"?"":" preview-modal--full"}`,onClick:a=>a.stopPropagation(),children:[s.jsxs("div",{className:"preview-modal-header",children:[s.jsx("span",{className:"preview-modal-title",children:(t==null?void 0:t.title)||(t==null?void 0:t.entry_uid)||"Preview"}),s.jsx("a",{className:"preview-modal-newtab",href:`/preview/${e}/${t==null?void 0:t.entry_uid}`,target:"_blank",rel:"noopener noreferrer",title:"Open in new tab",children:"↗"}),s.jsx("button",{className:"preview-modal-close",onClick:r,"aria-label":"Close preview",children:"×"})]}),s.jsx("div",{className:"preview-modal-body",children:s.jsx(qd,{archiveId:e,entry:t,detail:n})})]})})}function ku(e){if(!isFinite(e)||isNaN(e))return"--:--";const t=Math.floor(e/60),n=Math.floor(e%60);return`${t}:${String(n).padStart(2,"0")}`}function og({entry:e,src:t,archiveId:n,onClose:r}){const l=f.useRef(null),[i,a]=f.useState(!1),[o,u]=f.useState(0),[c,y]=f.useState(NaN),[h,g]=f.useState(1);f.useEffect(()=>{!l.current||!t||(l.current.load(),u(0),y(NaN),a(!1))},[t]),f.useEffect(()=>{l.current&&(l.current.volume=h)},[h]);function x(){const d=l.current;d&&(i?d.pause():d.play().catch(()=>{}))}function v(d){const m=l.current;if(!m||!isFinite(c))return;const w=Number(d.target.value);m.currentTime=w,u(w)}function k(d){g(Number(d.target.value))}const j=(e==null?void 0:e.title)||(e==null?void 0:e.entry_uid)||"Unknown",p=(e==null?void 0:e.source_kind)||"other";return s.jsxs(s.Fragment,{children:[s.jsx("audio",{ref:l,src:t||void 0,preload:"auto",onPlay:()=>a(!0),onPause:()=>a(!1),onEnded:()=>a(!1),onTimeUpdate:()=>{var d;return u(((d=l.current)==null?void 0:d.currentTime)??0)},onLoadedMetadata:()=>{var d;return y(((d=l.current)==null?void 0:d.duration)??NaN)},onDurationChange:()=>{var d;return y(((d=l.current)==null?void 0:d.duration)??NaN)},style:{display:"none"}}),s.jsxs("div",{className:"audio-bar",style:{position:"fixed",bottom:0,left:0,right:0,zIndex:100,background:"var(--paper-3)",borderTop:"1px solid var(--line)",display:"flex",alignItems:"center",gap:"16px",padding:"0 16px",height:"56px",fontFamily:"var(--sans)"},children:[s.jsxs("div",{className:"audio-bar-info",style:{display:"flex",alignItems:"center",gap:"8px",minWidth:0,flex:"0 1 220px",overflow:"hidden"},children:[s.jsx("span",{className:"source-icon",style:{flexShrink:0,width:"18px",height:"18px",display:"flex",alignItems:"center"},dangerouslySetInnerHTML:{__html:Cs(p)}}),s.jsx("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontSize:"0.85rem",color:"var(--ink)"},title:j,children:j})]}),s.jsxs("div",{className:"audio-bar-controls",style:{display:"flex",alignItems:"center",gap:"10px",flex:"1 1 0",minWidth:0},children:[s.jsx("button",{onClick:x,"aria-label":i?"Pause":"Play",style:{background:"none",border:"none",cursor:"pointer",padding:"4px",color:"var(--ink)",flexShrink:0,fontSize:"1.2rem",lineHeight:1},children:i?"⏸":"▶"}),s.jsx("span",{style:{fontSize:"0.75rem",color:"var(--muted)",whiteSpace:"nowrap",flexShrink:0},children:ku(o)}),s.jsx("input",{type:"range",min:0,max:isFinite(c)?c:0,step:.1,value:isFinite(o)?o:0,onChange:v,"aria-label":"Seek",style:{flex:1,minWidth:0,accentColor:"var(--accent)"}}),s.jsx("span",{style:{fontSize:"0.75rem",color:"var(--muted)",whiteSpace:"nowrap",flexShrink:0},children:ku(c)})]}),s.jsxs("div",{className:"audio-bar-right",style:{display:"flex",alignItems:"center",gap:"8px",flex:"0 1 160px"},children:[s.jsx("span",{style:{fontSize:"0.85rem",color:"var(--muted)",flexShrink:0},children:"🔊"}),s.jsx("input",{type:"range",min:0,max:1,step:.01,value:h,onChange:k,"aria-label":"Volume",style:{width:"80px",accentColor:"var(--accent)"}}),s.jsx("button",{onClick:r,"aria-label":"Close audio player",style:{background:"none",border:"none",cursor:"pointer",padding:"4px",color:"var(--muted)",fontSize:"1rem",lineHeight:1,marginLeft:"4px"},children:"✕"})]})]})]})}function ug({archiveId:e,entryUid:t}){var g,x;const[n,r]=f.useState(null),[l,i]=f.useState(!0),[a,o]=f.useState(null),[u,c]=f.useState(!1);f.useEffect(()=>{if(!u)return;const v=document.documentElement,k=document.body,j=v.style.colorScheme,p=k.style.background;return v.style.colorScheme="dark",k.style.background="#000",()=>{v.style.colorScheme=j,k.style.background=p}},[u]),f.useEffect(()=>{c(!1),Nr(e,t).then(v=>{r(v),i(!1)}).catch(v=>{o((v==null?void 0:v.message)||"Failed to load entry"),i(!1)})},[e,t]);const y=((g=n==null?void 0:n.summary)==null?void 0:g.title)||t,h=(x=n==null?void 0:n.summary)==null?void 0:x.original_url;return s.jsxs("div",{style:{minHeight:"100vh",display:"flex",flexDirection:"column",background:u?"#000":"var(--paper)",fontFamily:"var(--sans)"},children:[s.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px",padding:u?"8px 12px":"10px 16px",borderBottom:u?"none":"1px solid var(--line)",flexShrink:0,position:u?"sticky":"relative",top:0,zIndex:20,background:u?"rgba(0,0,0,0.82)":"var(--paper-2)",backdropFilter:u?"blur(12px)":"none",WebkitBackdropFilter:u?"blur(12px)":"none"},children:[s.jsx("a",{href:"/",style:{color:u?"#1d9bf0":"var(--accent)",textDecoration:"none",fontSize:"13px",flexShrink:0},children:"← Archive"}),s.jsx("span",{style:{flex:1,fontSize:"14px",fontWeight:600,color:u?"#e7e9ea":"var(--ink)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:y}),h&&s.jsx("a",{href:h,target:"_blank",rel:"noopener noreferrer",style:{color:u?"#71767b":"var(--muted)",textDecoration:"none",fontSize:"13px",flexShrink:0},children:"Original ↗"})]}),s.jsxs("div",{style:u?{flex:1}:{flex:1,minHeight:0,overflow:"auto",display:"flex",flexDirection:"column"},children:[l&&s.jsx("div",{style:{padding:"48px",textAlign:"center",color:"var(--muted)",fontSize:"14px"},children:"Loading…"}),a&&s.jsx("div",{style:{padding:"48px",textAlign:"center",color:"var(--alert)",fontSize:"14px"},children:a}),n&&s.jsx(qd,{archiveId:e,entry:n.summary,detail:n,fullPage:!0,onXArticle:c})]})]})}const cg=7e3;function dg({toasts:e,onDismiss:t,onIgnoreUblock:n}){return e.length?s.jsx("div",{className:"toast-stack",role:"log","aria-live":"polite","aria-label":"Notifications",children:e.map(r=>s.jsx(pg,{toast:r,onDismiss:t,onIgnoreUblock:n},r.id))}):null}function fg(e){if(!e)return null;if(e.length<=52)return e;try{const{hostname:t,pathname:n,search:r}=new URL(e),l=n.split("/").filter(Boolean),i=(l[l.length-1]??"")+r,a=i?`${t}/…/${i}`:t;return a.length<=56?a:a.slice(0,53)+"…"}catch{return"…"+e.slice(-51)}}function pg({toast:e,onDismiss:t,onIgnoreUblock:n}){const[r,l]=f.useState(!1),i=e.type==="warning",a=e.type==="success";f.useEffect(()=>{if(r)return;const u=setTimeout(()=>t(e.id),cg);return()=>clearTimeout(u)},[r,e.id,t]);const o=fg(e.locator);return a?s.jsx("div",{className:"toast toast--success",role:"alert","aria-atomic":"true",children:s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"✓"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Archived"}),o&&s.jsx("span",{className:"toast-locator",title:e.locator,children:o})]}),s.jsx("div",{className:"toast-btns",children:s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})})]})}):i?s.jsxs("div",{className:"toast toast--warning",role:"alert","aria-atomic":"true",children:[s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"⚠"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Archived with warnings"}),o&&s.jsx("span",{className:"toast-locator",title:e.locator,children:o})]}),s.jsxs("div",{className:"toast-btns",children:[e.text&&s.jsx("button",{type:"button",className:"toast-view-btn",onClick:()=>l(u=>!u),"aria-expanded":r,children:r?"Hide":"Details"}),e.locator&&s.jsx("button",{type:"button",className:"toast-view-btn toast-ignore-btn",onClick:()=>{n==null||n(),t(e.id)},children:"Ignore"}),s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})]})]}),r&&s.jsx("p",{className:"toast-warning-detail",children:e.text||"The page was captured but one or more browser extensions were unavailable (ad-blocking or cookie-consent). Check ARCHIVR_UBLOCK_EXT and ARCHIVR_COOKIE_EXT in your server config."})]}):s.jsxs("div",{className:"toast toast--error",role:"alert","aria-atomic":"true",children:[s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"✕"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Capture failed"}),o&&s.jsx("span",{className:"toast-locator",title:e.locator,children:o})]}),s.jsxs("div",{className:"toast-btns",children:[e.text&&s.jsx("button",{type:"button",className:"toast-view-btn",onClick:()=>l(u=>!u),children:r?"Hide":"View error"}),s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})]})]}),r&&e.text&&s.jsx("pre",{className:"toast-error-detail",children:e.text})]})}const Es=f.createContext(null),xr=(()=>{const e=window.location.pathname.match(/^\/preview\/([^/]+)\/([^/]+)/);return e?{archiveId:e[1],entryUid:e[2]}:null})(),hg=["archive","tags","collections","runs","admin","settings"],mg=["profile","tokens","instance","cookies","extensions","storage"];function yn(){const e=window.location.pathname.split("/").filter(Boolean),t=hg.includes(e[0])?e[0]:"archive",n=t==="settings"&&mg.includes(e[1])?e[1]:"profile",r=new URLSearchParams(window.location.search),l=r.get("q")??"",i=t==="archive"?r.get("tag")??null:null,a=t==="archive"?r.get("entry")??null:null;return{view:t,settingsTab:n,q:l,tag:i,entry:a}}function gg(e,t){return e==="archive"?"/":e==="settings"?`/settings/${t}`:`/${e}`}function yg(){const[e,t]=f.useState("loading"),[n,r]=f.useState(null);f.useEffect(()=>{(async()=>{if(await Hh()){t("setup");return}const $=await Qh();if(!$){t("login");return}r($),t("authenticated")})()},[]),f.useEffect(()=>{const C=()=>{r(null),t("login")};return window.addEventListener("auth:expired",C),()=>window.removeEventListener("auth:expired",C)},[]),f.useEffect(()=>{const C=()=>{const{view:$,settingsTab:A,q:Q,tag:ne,entry:ie}=yn();E($),_(A),I(Q),m(ne),x(ie),k(null),p(ie?new Set([ie]):new Set)};return window.addEventListener("popstate",C),()=>window.removeEventListener("popstate",C)},[]);const[l,i]=f.useState([]),[a,o]=f.useState(null),[u,c]=f.useState([]),[y,h]=f.useState(()=>new Set),[g,x]=f.useState(()=>yn().entry),[v,k]=f.useState(null),[j,p]=f.useState(()=>{const C=yn().entry;return C?new Set([C]):new Set}),[d,m]=f.useState(()=>yn().tag),[w,E]=f.useState(()=>yn().view),[P,_]=f.useState(()=>yn().settingsTab),[S,I]=f.useState(()=>yn().q),[b,K]=f.useState(""),[W,q]=f.useState(!1),[ee,me]=f.useState([]),[de,te]=f.useState([]),[z,T]=f.useState(()=>sessionStorage.getItem("captureDialogOpen")==="true"),[B,G]=f.useState([]),V=f.useRef(0),[fe,oe]=f.useState(()=>{try{const C=JSON.parse(sessionStorage.getItem("pendingCaptures")||"[]");if(Array.isArray(C))return C}catch{}return[]});f.useEffect(()=>{sessionStorage.setItem("pendingCaptures",JSON.stringify(fe))},[fe]);const[$e,Le]=f.useState(()=>sessionStorage.getItem("ublockWarningIgnored")==="true"),[He,ft]=f.useState(null),lt=f.useRef(0),pt=f.useRef(null),ht=f.useRef(!1),Ye=f.useRef(!0),R=f.useRef(null),J=f.useRef(0),Se=f.useRef(new Map),N=(n==null?void 0:n.humanize_slugs)??!1;f.useEffect(()=>{const C=++lt.current;ft(null),!(!v||!a)&&Nr(a,v.entry_uid).then($=>{C===lt.current&&ft($)}).catch(()=>{})},[v,a]),f.useEffect(()=>{sessionStorage.setItem("captureDialogOpen",z)},[z]);const F=f.useCallback(async(C,$,A)=>{if(C){q(!0);try{let Q;$||A?Q=await bh(C,$,A):Q=await Th(C),c(Q),p(ne=>{if(ne.size<2)return ne;const ie=new Set(Q.map(mt=>mt.entry_uid)),Oe=new Set([...ne].filter(mt=>ie.has(mt)));return Oe.size===ne.size?ne:Oe}),K(Q.length===0?"No results":`${Q.length} result${Q.length===1?"":"s"}`)}catch{c([]),K("Search failed. Try again.")}finally{q(!1)}}},[]);f.useEffect(()=>{e==="authenticated"&&Eh().then(C=>{if(i(C),C.length>0){const $=C[0].id;o($)}})},[e]),f.useEffect(()=>{if(a){if(Ye.current){Ye.current=!1,Promise.all([ni(a).then(me),_l(a).then(te)]);return}m(null),k(null),x(null),p(new Set),Promise.all([F(a,"",null),ni(a).then(me),_l(a).then(te)])}},[a]),f.useEffect(()=>{if(a===null)return;const C=setTimeout(()=>{F(a,S,d)},300);return()=>clearTimeout(C)},[S,a]),f.useEffect(()=>{a!==null&&(d!==null&&E("archive"),F(a,S,d))},[d,a]);const O=f.useCallback(C=>{o(C)},[]),H=f.useCallback(C=>{E(C),C==="tags"&&a&&_l(a).then(te)},[a]);f.useEffect(()=>{if(xr)return;const C=gg(w,P);window.location.pathname!==C&&history.pushState(null,"",C+window.location.search)},[w,P]);const U=f.useCallback(C=>{x(C?C.entry_uid:null),k(C)},[]),X=f.useCallback((C,$)=>{if(Se.current.set(C.entry_uid,C),++J.current,$.shiftKey&&R.current!==null){$.preventDefault();const A=[...document.querySelectorAll("#entries-body [data-entry-uid]")],Q=A.findIndex(Ge=>Ge.dataset.entryUid===R.current),ne=A.findIndex(Ge=>Ge.dataset.entryUid===C.entry_uid);if(Q===-1||ne===-1){R.current=C.entry_uid,p(new Set([C.entry_uid])),U(C);return}const ie=Math.min(Q,ne),Oe=Math.max(Q,ne),mt=new Set(A.slice(ie,Oe+1).map(Ge=>Ge.dataset.entryUid));p(mt),mt.size===1&&U(C)}else if($.ctrlKey||$.metaKey){R.current=C.entry_uid;const A=new Set(j);if(A.has(C.entry_uid)?A.delete(C.entry_uid):A.add(C.entry_uid),p(A),A.size===0)U(null);else if(A.size===1){const[Q]=A,ne=Se.current.get(Q)??u.find(ie=>ie.entry_uid===Q)??null;if(ne)U(ne);else{const ie=++J.current;Nr(a,Q).then(Oe=>{ie===J.current&&(Oe!=null&&Oe.summary)&&U(Oe.summary)}).catch(()=>{})}}else U(null)}else R.current=C.entry_uid,p(new Set([C.entry_uid])),U(C)},[u,j,U,a]),pe=f.useCallback(C=>{m(C)},[]),ze=f.useCallback(()=>{m(null)},[]),Ue=f.useCallback(()=>{a&&_l(a).then(te)},[a]),We=f.useCallback((C,$)=>{d===C?m($):d!=null&&d.startsWith(C+"/")&&m($+d.slice(C.length))},[d]),Ve=f.useCallback(C=>{(d===C||d!=null&&d.startsWith(C+"/"))&&m(null)},[d]),xe=f.useCallback((C,$)=>{c(A=>A.map(Q=>Q.entry_uid===C?{...Q,title:$}:Q)),k(A=>A&&A.entry_uid===C?{...A,title:$}:A),ft(A=>A&&A.summary.entry_uid===C?{...A,summary:{...A.summary,title:$}}:A)},[]),Dt=f.useCallback(()=>{if(!a||!v)return;const C=++lt.current;Nr(a,v.entry_uid).then($=>{C===lt.current&&ft($)}).catch(()=>{})},[a,v]),Qe=f.useCallback(C=>{const $=u.some(A=>A.entry_uid===C);h(A=>{const Q=new Set(A);return Q.add(C),Q}),c(A=>A.filter(Q=>Q.entry_uid!==C)),k(A=>(A==null?void 0:A.entry_uid)===C?null:A),x(A=>A===C?null:A),p(A=>{const Q=new Set(A);return Q.delete(C),Q}),$||F(a,S,d)},[u,a,S,d,F]),ll=f.useCallback(C=>{const $=new Set(u.map(Q=>Q.entry_uid)),A=[...C].some(Q=>!$.has(Q));h(Q=>{const ne=new Set(Q);return C.forEach(ie=>ne.add(ie)),ne}),c(Q=>Q.filter(ne=>!C.has(ne.entry_uid))),p(new Set),k(null),x(null),A&&F(a,S,d)},[u,a,S,d,F]);f.useEffect(()=>{if(j.size>=2)x(null),k(null);else if(j.size===1){const[C]=j;x(C)}else x(null),k(null)},[j]);const sl=f.useMemo(()=>u.filter(C=>j.has(C.entry_uid)),[u,j]);f.useEffect(()=>{if(!g||v)return;const C=u.find($=>$.entry_uid===g);C&&k(C)},[u,g,v]),f.useEffect(()=>{if(xr)return;const C=new URLSearchParams;S&&C.set("q",S),w==="archive"&&d&&C.set("tag",d),w==="archive"&&g&&C.set("entry",g);const $=C.toString(),A=window.location.pathname+($?"?"+$:"");window.location.pathname+window.location.search!==A&&history.replaceState(null,"",A)},[S,d,g,w]),f.useEffect(()=>{const C=$=>{var ie,Oe,mt,Ge;const A=$.key==="/"&&!$.metaKey&&!$.ctrlKey&&!$.altKey,Q=($.metaKey||$.ctrlKey)&&$.key==="k";if(!A&&!Q)return;const ne=(ie=document.activeElement)==null?void 0:ie.tagName;A&&(ne==="INPUT"||ne==="TEXTAREA"||ne==="SELECT")||A&&((Oe=document.activeElement)!=null&&Oe.isContentEditable)||($.preventDefault(),w==="archive"?((mt=pt.current)==null||mt.focus(),(Ge=pt.current)==null||Ge.select()):(ht.current=!0,E("archive")))};return document.addEventListener("keydown",C),()=>document.removeEventListener("keydown",C)},[w]),f.useEffect(()=>{const C=$=>{var Ga,Za;if($.metaKey||$.ctrlKey||$.altKey||$.key!=="j"&&$.key!=="k")return;const A=(Ga=document.activeElement)==null?void 0:Ga.tagName;if(A==="INPUT"||A==="TEXTAREA"||A==="SELECT"||(Za=document.activeElement)!=null&&Za.isContentEditable)return;const Q=[...document.querySelectorAll("#entries-body [data-entry-uid]")];if(Q.length===0)return;const[ne]=j.size===1?j:[null],ie=ne?Q.findIndex(Kt=>Kt.dataset.entryUid===ne):-1,Oe=$.key==="j"?Math.min(ie+1,Q.length-1):Math.max(ie-1,0);if(Oe===ie&&ie!==-1)return;const mt=Q[Oe<0?0:Oe],Ge=mt.dataset.entryUid;R.current=Ge;const Zd=++J.current;p(new Set([Ge]));const Ya=Se.current.get(Ge)??u.find(Kt=>Kt.entry_uid===Ge)??null;Ya?U(Ya):Nr(a,Ge).then(Kt=>{Zd===J.current&&(Kt!=null&&Kt.summary)&&U(Kt.summary)}).catch(()=>{}),mt.scrollIntoView({block:"nearest"}),$.preventDefault()};return document.addEventListener("keydown",C),()=>document.removeEventListener("keydown",C)},[j,u,U,a]),f.useEffect(()=>{w==="archive"&&ht.current&&(ht.current=!1,requestAnimationFrame(()=>{var C,$;(C=pt.current)==null||C.focus(),($=pt.current)==null||$.select()}))},[w]);const Ts=f.useCallback(()=>{T(!0)},[]),bs=f.useCallback(()=>{T(!1)},[]),il=f.useCallback(()=>{if(a)return Promise.allSettled([F(a,S,d),ni(a).then(me)])},[a,S,d,F]),D=f.useCallback((C,$,A="error",Q=null)=>{if(A==="warning"&&$e&&$)return;const ne=++V.current;G(ie=>[...ie,{id:ne,text:C,locator:$,type:A,headline:Q}])},[$e]),le=f.useCallback(C=>{G($=>$.filter(A=>A.id!==C))},[]),st=f.useCallback(()=>{sessionStorage.setItem("ublockWarningIgnored","true"),Le(!0),G(C=>C.filter($=>!($.type==="warning"&&$.locator)))},[]),mn=f.useCallback(C=>{oe($=>[...$,C])},[]),cr=f.useCallback(C=>{oe($=>$.filter(A=>A.id!==C))},[]),[gn,ke]=f.useState(null),[$t,Ja]=f.useState(null),Xd=f.useCallback(()=>{v&&ke(v.entry_uid)},[v]),Jd=f.useCallback(()=>ke(null),[]),Yd=f.useCallback((C,$)=>{Ja({src:C,entry:$})},[]),Gd=f.useCallback(()=>Ja(null),[]);return f.useEffect(()=>{ke(null)},[v]),f.useEffect(()=>{const C=$=>{var Q,ne,ie;if($.key!=="Escape"||z||gn)return;const A=(Q=document.activeElement)==null?void 0:Q.tagName;A==="INPUT"||A==="TEXTAREA"||A==="SELECT"||j.size>0&&($.preventDefault(),p(new Set),(ie=(ne=document.activeElement)==null?void 0:ne.blur)==null||ie.call(ne))};return window.addEventListener("keydown",C),()=>window.removeEventListener("keydown",C)},[z,gn,j]),f.useEffect(()=>(document.body.classList.toggle("has-audio-bar",!!$t),()=>document.body.classList.remove("has-audio-bar")),[$t]),e==="loading"?s.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?s.jsx(xm,{onComplete:()=>t("login")}):e==="login"?s.jsx(vm,{onLogin:C=>{r(C),t("authenticated")}}):xr?s.jsx(ug,{archiveId:xr.archiveId,entryUid:xr.entryUid}):s.jsx(Es.Provider,{value:{currentUser:n,setCurrentUser:r},children:s.jsxs(s.Fragment,{children:[s.jsx(wm,{archives:l,archiveId:a,onArchiveChange:O,view:w,onViewChange:H,onCaptureClick:Ts}),s.jsxs("main",{className:"app-shell",children:[s.jsxs("div",{className:"workspace",children:[w==="archive"&&s.jsxs("div",{className:"toolbar",children:[s.jsxs("div",{className:"search-field",children:[s.jsx("span",{className:"ico","aria-hidden":"true",children:s.jsxs("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("circle",{cx:"11",cy:"11",r:"7"}),s.jsx("line",{x1:"16.5",y1:"16.5",x2:"21",y2:"21"})]})}),s.jsx("input",{ref:pt,className:"search-input",type:"search","aria-label":"Search archive","aria-busy":W,placeholder:"Search titles, URLs, types, tags…",value:S,onChange:C=>I(C.target.value)}),s.jsx("span",{className:"kbd",children:"⌘K"})]}),s.jsxs("span",{className:"result-count",children:[b&&s.jsxs(s.Fragment,{children:[s.jsx("b",{children:b.split(" ")[0]})," ",b.split(" ").slice(1).join(" ")]}),d&&s.jsxs("button",{className:"tag-filter-badge",onClick:ze,children:["× ",N?Md(d):d]})]})]}),w==="archive"&&s.jsx(Tm,{entries:u,selectedUids:j,onRowClick:X,archiveId:a,pendingCaptures:fe,deletedUids:y}),w==="runs"&&s.jsx(Lm,{runs:ee}),w==="admin"&&s.jsx(Rm,{archives:l}),w==="tags"&&s.jsx(Im,{archiveId:a,tagNodes:de,tagFilter:d,onTagFilterSet:pe,onViewChange:H,onTagRenamed:We,onTagDeleted:Ve,onTagsRefresh:Ue,humanizeTags:N}),w==="collections"&&s.jsx($m,{archiveId:a}),w==="settings"&&s.jsx(Am,{tab:P,onTabChange:_,archiveId:a})]}),s.jsx(Qm,{archiveId:a,selectedEntry:v,selectedUids:j,selectedEntries:sl,detail:He,onTagFilterSet:pe,tagNodes:de,onTagsRefresh:Ue,onEntryTitleChange:xe,onEntryDeleted:Qe,onBulkDeleted:ll,humanizeTags:N,onDetailRefresh:Dt,onOpenPreview:Xd,onPlay:Yd})]}),gn&&v&&v.entry_uid===gn&&s.jsx(ag,{archiveId:a,entry:v,detail:He,onClose:Jd}),$t&&s.jsx(og,{entry:$t.entry,src:$t.src,archiveId:a,onClose:Gd}),s.jsx(jm,{open:z,archiveId:a,onClose:bs,onCaptured:il,onToast:D,activeJobs:fe,onJobStarted:mn,onJobSettled:cr}),s.jsx(dg,{toasts:B,onDismiss:le,onIgnoreUblock:st})]})})}Id(document.getElementById("root")).render(s.jsx(f.StrictMode,{children:s.jsx(yg,{})})); diff --git a/crates/archivr-server/static/assets/index-YmIQCrug.js b/crates/archivr-server/static/assets/index-YmIQCrug.js deleted file mode 100644 index 70daba7..0000000 --- a/crates/archivr-server/static/assets/index-YmIQCrug.js +++ /dev/null @@ -1,47 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&r(a)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var fu={exports:{}},Gl={},pu={exports:{}},Z={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Ar=Symbol.for("react.element"),Ad=Symbol.for("react.portal"),Bd=Symbol.for("react.fragment"),Ud=Symbol.for("react.strict_mode"),Hd=Symbol.for("react.profiler"),Wd=Symbol.for("react.provider"),Vd=Symbol.for("react.context"),Qd=Symbol.for("react.forward_ref"),Kd=Symbol.for("react.suspense"),Xd=Symbol.for("react.memo"),Jd=Symbol.for("react.lazy"),Wa=Symbol.iterator;function qd(e){return e===null||typeof e!="object"?null:(e=Wa&&e[Wa]||e["@@iterator"],typeof e=="function"?e:null)}var hu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},mu=Object.assign,gu={};function Vn(e,t,n){this.props=e,this.context=t,this.refs=gu,this.updater=n||hu}Vn.prototype.isReactComponent={};Vn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Vn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function vu(){}vu.prototype=Vn.prototype;function Ki(e,t,n){this.props=e,this.context=t,this.refs=gu,this.updater=n||hu}var Xi=Ki.prototype=new vu;Xi.constructor=Ki;mu(Xi,Vn.prototype);Xi.isPureReactComponent=!0;var Va=Array.isArray,yu=Object.prototype.hasOwnProperty,Ji={current:null},xu={key:!0,ref:!0,__self:!0,__source:!0};function wu(e,t,n){var r,l={},i=null,a=null;if(t!=null)for(r in t.ref!==void 0&&(a=t.ref),t.key!==void 0&&(i=""+t.key),t)yu.call(t,r)&&!xu.hasOwnProperty(r)&&(l[r]=t[r]);var o=arguments.length-2;if(o===1)l.children=n;else if(1>>1,U=D[Y];if(0>>1;Yl(N,M))Fl(H,N)?(D[Y]=H,D[F]=M,Y=F):(D[Y]=N,D[ae]=M,Y=ae);else if(Fl(H,M))D[Y]=H,D[F]=M,Y=F;else break e}}return b}function l(D,b){var M=D.sortIndex-b.sortIndex;return M!==0?M:D.id-b.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var a=Date,o=a.now();e.unstable_now=function(){return a.now()-o}}var u=[],c=[],g=1,m=null,h=3,y=!1,x=!1,k=!1,j=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,d=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v(D){for(var b=n(c);b!==null;){if(b.callback===null)r(c);else if(b.startTime<=D)r(c),b.sortIndex=b.expirationTime,t(u,b);else break;b=n(c)}}function w(D){if(k=!1,v(D),!x)if(n(u)!==null)x=!0,de(_);else{var b=n(c);b!==null&&ne(w,b.startTime-D)}}function _(D,b){x=!1,k&&(k=!1,p(S),S=-1),y=!0;var M=h;try{for(v(b),m=n(u);m!==null&&(!(m.expirationTime>b)||D&&!W());){var Y=m.callback;if(typeof Y=="function"){m.callback=null,h=m.priorityLevel;var U=Y(m.expirationTime<=b);b=e.unstable_now(),typeof U=="function"?m.callback=U:m===n(u)&&r(u),v(b)}else r(u);m=n(u)}if(m!==null)var fe=!0;else{var ae=n(c);ae!==null&&ne(w,ae.startTime-b),fe=!1}return fe}finally{m=null,h=M,y=!1}}var P=!1,C=null,S=-1,$=5,L=-1;function W(){return!(e.unstable_now()-L<$)}function A(){if(C!==null){var D=e.unstable_now();L=D;var b=!0;try{b=C(!0,D)}finally{b?Q():(P=!1,C=null)}}else P=!1}var Q;if(typeof d=="function")Q=function(){d(A)};else if(typeof MessageChannel<"u"){var ee=new MessageChannel,le=ee.port2;ee.port1.onmessage=A,Q=function(){le.postMessage(null)}}else Q=function(){j(A,0)};function de(D){C=D,P||(P=!0,Q())}function ne(D,b){S=j(function(){D(e.unstable_now())},b)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(D){D.callback=null},e.unstable_continueExecution=function(){x||y||(x=!0,de(_))},e.unstable_forceFrameRate=function(D){0>D||125Y?(D.sortIndex=M,t(c,D),n(u)===null&&D===n(c)&&(k?(p(S),S=-1):k=!0,ne(w,M-Y))):(D.sortIndex=U,t(u,D),x||y||(x=!0,de(_))),D},e.unstable_shouldYield=W,e.unstable_wrapCallback=function(D){var b=h;return function(){var M=h;h=b;try{return D.apply(this,arguments)}finally{h=M}}}})(_u);Nu.exports=_u;var of=Nu.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var uf=f,qe=of;function z(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Zs=Object.prototype.hasOwnProperty,cf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Ka={},Xa={};function df(e){return Zs.call(Xa,e)?!0:Zs.call(Ka,e)?!1:cf.test(e)?Xa[e]=!0:(Ka[e]=!0,!1)}function ff(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function pf(e,t,n,r){if(t===null||typeof t>"u"||ff(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Ae(e,t,n,r,l,i,a){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=a}var Le={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Le[e]=new Ae(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Le[t]=new Ae(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Le[e]=new Ae(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Le[e]=new Ae(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Le[e]=new Ae(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Le[e]=new Ae(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Le[e]=new Ae(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Le[e]=new Ae(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Le[e]=new Ae(e,5,!1,e.toLowerCase(),null,!1,!1)});var Yi=/[\-:]([a-z])/g;function Gi(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Yi,Gi);Le[t]=new Ae(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Yi,Gi);Le[t]=new Ae(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Yi,Gi);Le[t]=new Ae(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Le[e]=new Ae(e,1,!1,e.toLowerCase(),null,!1,!1)});Le.xlinkHref=new Ae("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Le[e]=new Ae(e,1,!1,e.toLowerCase(),null,!0,!0)});function Zi(e,t,n,r){var l=Le.hasOwnProperty(t)?Le[t]:null;(l!==null?l.type!==0:r||!(2o||l[a]!==i[o]){var u=` -`+l[a].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=a&&0<=o);break}}}finally{_s=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?ur(e):""}function hf(e){switch(e.tag){case 5:return ur(e.type);case 16:return ur("Lazy");case 13:return ur("Suspense");case 19:return ur("SuspenseList");case 0:case 2:case 15:return e=Cs(e.type,!1),e;case 11:return e=Cs(e.type.render,!1),e;case 1:return e=Cs(e.type,!0),e;default:return""}}function ri(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 wn:return"Fragment";case xn:return"Portal";case ei:return"Profiler";case ea:return"StrictMode";case ti:return"Suspense";case ni:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Tu:return(e.displayName||"Context")+".Consumer";case Eu:return(e._context.displayName||"Context")+".Provider";case ta:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case na:return t=e.displayName||null,t!==null?t:ri(e.type)||"Memo";case Lt:t=e._payload,e=e._init;try{return ri(e(t))}catch{}}return null}function mf(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 ri(t);case 8:return t===ea?"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 Vt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Pu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function gf(e){var t=Pu(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(a){r=""+a,i.call(this,a)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(a){r=""+a},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function qr(e){e._valueTracker||(e._valueTracker=gf(e))}function Lu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Pu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Cl(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 li(e,t){var n=t.checked;return ve({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function qa(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Vt(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 zu(e,t){t=t.checked,t!=null&&Zi(e,"checked",t,!1)}function si(e,t){zu(e,t);var n=Vt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ii(e,t.type,n):t.hasOwnProperty("defaultValue")&&ii(e,t.type,Vt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Ya(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ii(e,t,n){(t!=="number"||Cl(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var cr=Array.isArray;function Ln(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Yr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Nr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var hr={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},vf=["Webkit","ms","Moz","O"];Object.keys(hr).forEach(function(e){vf.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),hr[t]=hr[e]})});function Iu(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||hr.hasOwnProperty(e)&&hr[e]?(""+t).trim():t+"px"}function Ou(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Iu(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var yf=ve({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ui(e,t){if(t){if(yf[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(z(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(z(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(z(61))}if(t.style!=null&&typeof t.style!="object")throw Error(z(62))}}function ci(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 di=null;function ra(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var fi=null,zn=null,Rn=null;function eo(e){if(e=Hr(e)){if(typeof fi!="function")throw Error(z(280));var t=e.stateNode;t&&(t=rs(t),fi(e.stateNode,e.type,t))}}function Fu(e){zn?Rn?Rn.push(e):Rn=[e]:zn=e}function Mu(){if(zn){var e=zn,t=Rn;if(Rn=zn=null,eo(e),t)for(e=0;e>>=0,e===0?32:31-(bf(e)/Pf|0)|0}var Gr=64,Zr=4194304;function dr(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 Pl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,a=n&268435455;if(a!==0){var o=a&~l;o!==0?r=dr(o):(i&=a,i!==0&&(r=dr(i)))}else a=n&~l,a!==0?r=dr(a):i!==0&&(r=dr(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 Br(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-dt(t),e[t]=n}function Df(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=gr),uo=" ",co=!1;function sc(e,t){switch(e){case"keyup":return op.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ic(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var kn=!1;function cp(e,t){switch(e){case"compositionend":return ic(t);case"keypress":return t.which!==32?null:(co=!0,uo);case"textInput":return e=t.data,e===uo&&co?null:e;default:return null}}function dp(e,t){if(kn)return e==="compositionend"||!da&&sc(e,t)?(e=rc(),gl=oa=$t=null,kn=!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=mo(n)}}function cc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?cc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function dc(){for(var e=window,t=Cl();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Cl(e.document)}return t}function fa(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 wp(e){var t=dc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&cc(n.ownerDocument.documentElement,n)){if(r!==null&&fa(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=go(n,i);var a=go(n,r);l&&a&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==a.node||e.focusOffset!==a.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(a.node,a.offset)):(t.setEnd(a.node,a.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,jn=null,yi=null,yr=null,xi=!1;function vo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;xi||jn==null||jn!==Cl(r)||(r=jn,"selectionStart"in r&&fa(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}),yr&&Pr(yr,r)||(yr=r,r=Rl(yi,"onSelect"),0_n||(e.current=_i[_n],_i[_n]=null,_n--)}function oe(e,t){_n++,_i[_n]=e.current,e.current=t}var Qt={},Ie=Xt(Qt),He=Xt(!1),an=Qt;function Mn(e,t){var n=e.type.contextTypes;if(!n)return Qt;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 We(e){return e=e.childContextTypes,e!=null}function $l(){ce(He),ce(Ie)}function No(e,t,n){if(Ie.current!==Qt)throw Error(z(168));oe(Ie,t),oe(He,n)}function wc(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(z(108,mf(e)||"Unknown",l));return ve({},n,r)}function Il(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Qt,an=Ie.current,oe(Ie,e),oe(He,He.current),!0}function _o(e,t,n){var r=e.stateNode;if(!r)throw Error(z(169));n?(e=wc(e,t,an),r.__reactInternalMemoizedMergedChildContext=e,ce(He),ce(Ie),oe(Ie,e)):ce(He),oe(He,n)}var wt=null,ls=!1,As=!1;function kc(e){wt===null?wt=[e]:wt.push(e)}function zp(e){ls=!0,kc(e)}function Jt(){if(!As&&wt!==null){As=!0;var e=0,t=ie;try{var n=wt;for(ie=1;e>=a,l-=a,kt=1<<32-dt(t)+l|n<S?($=C,C=null):$=C.sibling;var L=h(p,C,v[S],w);if(L===null){C===null&&(C=$);break}e&&C&&L.alternate===null&&t(p,C),d=i(L,d,S),P===null?_=L:P.sibling=L,P=L,C=$}if(S===v.length)return n(p,C),he&&Gt(p,S),_;if(C===null){for(;SS?($=C,C=null):$=C.sibling;var W=h(p,C,L.value,w);if(W===null){C===null&&(C=$);break}e&&C&&W.alternate===null&&t(p,C),d=i(W,d,S),P===null?_=W:P.sibling=W,P=W,C=$}if(L.done)return n(p,C),he&&Gt(p,S),_;if(C===null){for(;!L.done;S++,L=v.next())L=m(p,L.value,w),L!==null&&(d=i(L,d,S),P===null?_=L:P.sibling=L,P=L);return he&&Gt(p,S),_}for(C=r(p,C);!L.done;S++,L=v.next())L=y(C,p,S,L.value,w),L!==null&&(e&&L.alternate!==null&&C.delete(L.key===null?S:L.key),d=i(L,d,S),P===null?_=L:P.sibling=L,P=L);return e&&C.forEach(function(A){return t(p,A)}),he&&Gt(p,S),_}function j(p,d,v,w){if(typeof v=="object"&&v!==null&&v.type===wn&&v.key===null&&(v=v.props.children),typeof v=="object"&&v!==null){switch(v.$$typeof){case Jr:e:{for(var _=v.key,P=d;P!==null;){if(P.key===_){if(_=v.type,_===wn){if(P.tag===7){n(p,P.sibling),d=l(P,v.props.children),d.return=p,p=d;break e}}else if(P.elementType===_||typeof _=="object"&&_!==null&&_.$$typeof===Lt&&To(_)===P.type){n(p,P.sibling),d=l(P,v.props),d.ref=lr(p,P,v),d.return=p,p=d;break e}n(p,P);break}else t(p,P);P=P.sibling}v.type===wn?(d=sn(v.props.children,p.mode,w,v.key),d.return=p,p=d):(w=Nl(v.type,v.key,v.props,null,p.mode,w),w.ref=lr(p,d,v),w.return=p,p=w)}return a(p);case xn:e:{for(P=v.key;d!==null;){if(d.key===P)if(d.tag===4&&d.stateNode.containerInfo===v.containerInfo&&d.stateNode.implementation===v.implementation){n(p,d.sibling),d=l(d,v.children||[]),d.return=p,p=d;break e}else{n(p,d);break}else t(p,d);d=d.sibling}d=Xs(v,p.mode,w),d.return=p,p=d}return a(p);case Lt:return P=v._init,j(p,d,P(v._payload),w)}if(cr(v))return x(p,d,v,w);if(Zn(v))return k(p,d,v,w);il(p,v)}return typeof v=="string"&&v!==""||typeof v=="number"?(v=""+v,d!==null&&d.tag===6?(n(p,d.sibling),d=l(d,v),d.return=p,p=d):(n(p,d),d=Ks(v,p.mode,w),d.return=p,p=d),a(p)):n(p,d)}return j}var Bn=_c(!0),Cc=_c(!1),Ml=Xt(null),Al=null,Tn=null,ga=null;function va(){ga=Tn=Al=null}function ya(e){var t=Ml.current;ce(Ml),e._currentValue=t}function Ti(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 $n(e,t){Al=e,ga=Tn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Ue=!0),e.firstContext=null)}function st(e){var t=e._currentValue;if(ga!==e)if(e={context:e,memoizedValue:t,next:null},Tn===null){if(Al===null)throw Error(z(308));Tn=e,Al.dependencies={lanes:0,firstContext:e}}else Tn=Tn.next=e;return t}var tn=null;function xa(e){tn===null?tn=[e]:tn.push(e)}function Ec(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,xa(t)):(n.next=l.next,l.next=n),t.interleaved=n,Ct(e,r)}function Ct(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 zt=!1;function wa(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Tc(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 St(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Bt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,te&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Ct(e,n)}return l=r.interleaved,l===null?(t.next=t,xa(r)):(t.next=l.next,l.next=t),r.interleaved=t,Ct(e,n)}function yl(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,sa(e,n)}}function bo(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 a={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=a:i=i.next=a,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 Bl(e,t,n,r){var l=e.updateQueue;zt=!1;var i=l.firstBaseUpdate,a=l.lastBaseUpdate,o=l.shared.pending;if(o!==null){l.shared.pending=null;var u=o,c=u.next;u.next=null,a===null?i=c:a.next=c,a=u;var g=e.alternate;g!==null&&(g=g.updateQueue,o=g.lastBaseUpdate,o!==a&&(o===null?g.firstBaseUpdate=c:o.next=c,g.lastBaseUpdate=u))}if(i!==null){var m=l.baseState;a=0,g=c=u=null,o=i;do{var h=o.lane,y=o.eventTime;if((r&h)===h){g!==null&&(g=g.next={eventTime:y,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var x=e,k=o;switch(h=t,y=n,k.tag){case 1:if(x=k.payload,typeof x=="function"){m=x.call(y,m,h);break e}m=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=k.payload,h=typeof x=="function"?x.call(y,m,h):x,h==null)break e;m=ve({},m,h);break e;case 2:zt=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[o]:h.push(o))}else y={eventTime:y,lane:h,tag:o.tag,payload:o.payload,callback:o.callback,next:null},g===null?(c=g=y,u=m):g=g.next=y,a|=h;if(o=o.next,o===null){if(o=l.shared.pending,o===null)break;h=o,o=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(g===null&&(u=m),l.baseState=u,l.firstBaseUpdate=c,l.lastBaseUpdate=g,t=l.shared.interleaved,t!==null){l=t;do a|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);cn|=a,e.lanes=a,e.memoizedState=m}}function Po(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Us.transition;Us.transition={};try{e(!1),t()}finally{ie=n,Us.transition=r}}function Vc(){return it().memoizedState}function Ip(e,t,n){var r=Ht(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Qc(e))Kc(t,n);else if(n=Ec(e,t,n,r),n!==null){var l=Fe();ft(n,e,r,l),Xc(n,t,r)}}function Op(e,t,n){var r=Ht(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Qc(e))Kc(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var a=t.lastRenderedState,o=i(a,n);if(l.hasEagerState=!0,l.eagerState=o,pt(o,a)){var u=t.interleaved;u===null?(l.next=l,xa(t)):(l.next=u.next,u.next=l),t.interleaved=l;return}}catch{}finally{}n=Ec(e,t,l,r),n!==null&&(l=Fe(),ft(n,e,r,l),Xc(n,t,r))}}function Qc(e){var t=e.alternate;return e===ge||t!==null&&t===ge}function Kc(e,t){xr=Hl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Xc(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,sa(e,n)}}var Wl={readContext:st,useCallback:Re,useContext:Re,useEffect:Re,useImperativeHandle:Re,useInsertionEffect:Re,useLayoutEffect:Re,useMemo:Re,useReducer:Re,useRef:Re,useState:Re,useDebugValue:Re,useDeferredValue:Re,useTransition:Re,useMutableSource:Re,useSyncExternalStore:Re,useId:Re,unstable_isNewReconciler:!1},Fp={readContext:st,useCallback:function(e,t){return mt().memoizedState=[e,t===void 0?null:t],e},useContext:st,useEffect:zo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,wl(4194308,4,Ac.bind(null,t,e),n)},useLayoutEffect:function(e,t){return wl(4194308,4,e,t)},useInsertionEffect:function(e,t){return wl(4,2,e,t)},useMemo:function(e,t){var n=mt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=mt();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=Ip.bind(null,ge,e),[r.memoizedState,e]},useRef:function(e){var t=mt();return e={current:e},t.memoizedState=e},useState:Lo,useDebugValue:Ta,useDeferredValue:function(e){return mt().memoizedState=e},useTransition:function(){var e=Lo(!1),t=e[0];return e=$p.bind(null,e[1]),mt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=ge,l=mt();if(he){if(n===void 0)throw Error(z(407));n=n()}else{if(n=t(),Te===null)throw Error(z(349));un&30||zc(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,zo(Dc.bind(null,r,i,e),[e]),r.flags|=2048,Fr(9,Rc.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=mt(),t=Te.identifierPrefix;if(he){var n=jt,r=kt;n=(r&~(1<<32-dt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Ir++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=a.createElement(n,{is:r.is}):(e=a.createElement(n),n==="select"&&(a=e,r.multiple?a.multiple=!0:r.size&&(a.size=r.size))):e=a.createElementNS(e,n),e[gt]=t,e[Rr]=r,ld(e,t,!1,!1),t.stateNode=e;e:{switch(a=ci(n,r),n){case"dialog":ue("cancel",e),ue("close",e),l=r;break;case"iframe":case"object":case"embed":ue("load",e),l=r;break;case"video":case"audio":for(l=0;lWn&&(t.flags|=128,r=!0,sr(i,!1),t.lanes=4194304)}else{if(!r)if(e=Ul(a),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),sr(i,!0),i.tail===null&&i.tailMode==="hidden"&&!a.alternate&&!he)return De(t),null}else 2*xe()-i.renderingStartTime>Wn&&n!==1073741824&&(t.flags|=128,r=!0,sr(i,!1),t.lanes=4194304);i.isBackwards?(a.sibling=t.child,t.child=a):(n=i.last,n!==null?n.sibling=a:t.child=a,i.last=a)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=xe(),t.sibling=null,n=me.current,oe(me,r?n&1|2:n&1),t):(De(t),null);case 22:case 23:return Da(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ke&1073741824&&(De(t),t.subtreeFlags&6&&(t.flags|=8192)):De(t),null;case 24:return null;case 25:return null}throw Error(z(156,t.tag))}function Qp(e,t){switch(ha(t),t.tag){case 1:return We(t.type)&&$l(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Un(),ce(He),ce(Ie),Sa(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return ja(t),null;case 13:if(ce(me),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(z(340));An()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ce(me),null;case 4:return Un(),null;case 10:return ya(t.type._context),null;case 22:case 23:return Da(),null;case 24:return null;default:return null}}var ol=!1,$e=!1,Kp=typeof WeakSet=="function"?WeakSet:Set,O=null;function bn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){ye(e,t,r)}else n.current=null}function Oi(e,t,n){try{n()}catch(r){ye(e,t,r)}}var Ho=!1;function Xp(e,t){if(wi=Ll,e=dc(),fa(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 a=0,o=-1,u=-1,c=0,g=0,m=e,h=null;t:for(;;){for(var y;m!==n||l!==0&&m.nodeType!==3||(o=a+l),m!==i||r!==0&&m.nodeType!==3||(u=a+r),m.nodeType===3&&(a+=m.nodeValue.length),(y=m.firstChild)!==null;)h=m,m=y;for(;;){if(m===e)break t;if(h===n&&++c===l&&(o=a),h===i&&++g===r&&(u=a),(y=m.nextSibling)!==null)break;m=h,h=m.parentNode}m=y}n=o===-1||u===-1?null:{start:o,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(ki={focusedElem:e,selectionRange:n},Ll=!1,O=t;O!==null;)if(t=O,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,O=e;else for(;O!==null;){t=O;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 k=x.memoizedProps,j=x.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?k:ot(t.type,k),j);p.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var v=t.stateNode.containerInfo;v.nodeType===1?v.textContent="":v.nodeType===9&&v.documentElement&&v.removeChild(v.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(z(163))}}catch(w){ye(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,O=e;break}O=t.return}return x=Ho,Ho=!1,x}function wr(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&&Oi(t,n,i)}l=l.next}while(l!==r)}}function as(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Fi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function ad(e){var t=e.alternate;t!==null&&(e.alternate=null,ad(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[gt],delete t[Rr],delete t[Ni],delete t[Pp],delete t[Lp])),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 od(e){return e.tag===5||e.tag===3||e.tag===4}function Wo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||od(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 Mi(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=Dl));else if(r!==4&&(e=e.child,e!==null))for(Mi(e,t,n),e=e.sibling;e!==null;)Mi(e,t,n),e=e.sibling}function Ai(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(Ai(e,t,n),e=e.sibling;e!==null;)Ai(e,t,n),e=e.sibling}var be=null,ut=!1;function Pt(e,t,n){for(n=n.child;n!==null;)ud(e,t,n),n=n.sibling}function ud(e,t,n){if(vt&&typeof vt.onCommitFiberUnmount=="function")try{vt.onCommitFiberUnmount(Zl,n)}catch{}switch(n.tag){case 5:$e||bn(n,t);case 6:var r=be,l=ut;be=null,Pt(e,t,n),be=r,ut=l,be!==null&&(ut?(e=be,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):be.removeChild(n.stateNode));break;case 18:be!==null&&(ut?(e=be,n=n.stateNode,e.nodeType===8?Ms(e.parentNode,n):e.nodeType===1&&Ms(e,n),Tr(e)):Ms(be,n.stateNode));break;case 4:r=be,l=ut,be=n.stateNode.containerInfo,ut=!0,Pt(e,t,n),be=r,ut=l;break;case 0:case 11:case 14:case 15:if(!$e&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,a=i.destroy;i=i.tag,a!==void 0&&(i&2||i&4)&&Oi(n,t,a),l=l.next}while(l!==r)}Pt(e,t,n);break;case 1:if(!$e&&(bn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){ye(n,t,o)}Pt(e,t,n);break;case 21:Pt(e,t,n);break;case 22:n.mode&1?($e=(r=$e)||n.memoizedState!==null,Pt(e,t,n),$e=r):Pt(e,t,n);break;default:Pt(e,t,n)}}function Vo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Kp),t.forEach(function(r){var l=rh.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function at(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=a),r&=~i}if(r=l,r=xe()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*qp(r/1960))-r,10e?16:e,It===null)var r=!1;else{if(e=It,It=null,Kl=0,te&6)throw Error(z(331));var l=te;for(te|=4,O=e.current;O!==null;){var i=O,a=i.child;if(O.flags&16){var o=i.deletions;if(o!==null){for(var u=0;uxe()-za?ln(e,0):La|=n),Ve(e,t)}function vd(e,t){t===0&&(e.mode&1?(t=Zr,Zr<<=1,!(Zr&130023424)&&(Zr=4194304)):t=1);var n=Fe();e=Ct(e,t),e!==null&&(Br(e,t,n),Ve(e,n))}function nh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),vd(e,n)}function rh(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(z(314))}r!==null&&r.delete(t),vd(e,n)}var yd;yd=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||He.current)Ue=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return Ue=!1,Wp(e,t,n);Ue=!!(e.flags&131072)}else Ue=!1,he&&t.flags&1048576&&jc(t,Fl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;kl(e,t),e=t.pendingProps;var l=Mn(t,Ie.current);$n(t,n),l=_a(null,t,r,e,l,n);var i=Ca();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,We(r)?(i=!0,Il(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,wa(t),l.updater=is,t.stateNode=l,l._reactInternals=t,Pi(t,r,e,n),t=Ri(null,t,r,!0,i,n)):(t.tag=0,he&&i&&pa(t),Oe(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(kl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=sh(r),e=ot(r,e),l){case 0:t=zi(null,t,r,e,n);break e;case 1:t=Ao(null,t,r,e,n);break e;case 11:t=Fo(null,t,r,e,n);break e;case 14:t=Mo(null,t,r,ot(r.type,e),n);break e}throw Error(z(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:ot(r,l),zi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:ot(r,l),Ao(e,t,r,l,n);case 3:e:{if(td(t),e===null)throw Error(z(387));r=t.pendingProps,i=t.memoizedState,l=i.element,Tc(e,t),Bl(t,r,null,n);var a=t.memoizedState;if(r=a.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:a.cache,pendingSuspenseBoundaries:a.pendingSuspenseBoundaries,transitions:a.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=Hn(Error(z(423)),t),t=Bo(e,t,r,n,l);break e}else if(r!==l){l=Hn(Error(z(424)),t),t=Bo(e,t,r,n,l);break e}else for(Xe=At(t.stateNode.containerInfo.firstChild),Je=t,he=!0,ct=null,n=Cc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(An(),r===l){t=Et(e,t,n);break e}Oe(e,t,r,n)}t=t.child}return t;case 5:return bc(t),e===null&&Ei(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,a=l.children,ji(r,l)?a=null:i!==null&&ji(r,i)&&(t.flags|=32),ed(e,t),Oe(e,t,a,n),t.child;case 6:return e===null&&Ei(t),null;case 13:return nd(e,t,n);case 4:return ka(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Bn(t,null,r,n):Oe(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:ot(r,l),Fo(e,t,r,l,n);case 7:return Oe(e,t,t.pendingProps,n),t.child;case 8:return Oe(e,t,t.pendingProps.children,n),t.child;case 12:return Oe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,a=l.value,oe(Ml,r._currentValue),r._currentValue=a,i!==null)if(pt(i.value,a)){if(i.children===l.children&&!He.current){t=Et(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var o=i.dependencies;if(o!==null){a=i.child;for(var u=o.firstContext;u!==null;){if(u.context===r){if(i.tag===1){u=St(-1,n&-n),u.tag=2;var c=i.updateQueue;if(c!==null){c=c.shared;var g=c.pending;g===null?u.next=u:(u.next=g.next,g.next=u),c.pending=u}}i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Ti(i.return,n,t),o.lanes|=n;break}u=u.next}}else if(i.tag===10)a=i.type===t.type?null:i.child;else if(i.tag===18){if(a=i.return,a===null)throw Error(z(341));a.lanes|=n,o=a.alternate,o!==null&&(o.lanes|=n),Ti(a,n,t),a=i.sibling}else a=i.child;if(a!==null)a.return=i;else for(a=i;a!==null;){if(a===t){a=null;break}if(i=a.sibling,i!==null){i.return=a.return,a=i;break}a=a.return}i=a}Oe(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,$n(t,n),l=st(l),r=r(l),t.flags|=1,Oe(e,t,r,n),t.child;case 14:return r=t.type,l=ot(r,t.pendingProps),l=ot(r.type,l),Mo(e,t,r,l,n);case 15:return Gc(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:ot(r,l),kl(e,t),t.tag=1,We(r)?(e=!0,Il(t)):e=!1,$n(t,n),Jc(t,r,l),Pi(t,r,l,n),Ri(null,t,r,!0,e,n);case 19:return rd(e,t,n);case 22:return Zc(e,t,n)}throw Error(z(156,t.tag))};function xd(e,t){return Qu(e,t)}function lh(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 rt(e,t,n,r){return new lh(e,t,n,r)}function Ia(e){return e=e.prototype,!(!e||!e.isReactComponent)}function sh(e){if(typeof e=="function")return Ia(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ta)return 11;if(e===na)return 14}return 2}function Wt(e,t){var n=e.alternate;return n===null?(n=rt(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 Nl(e,t,n,r,l,i){var a=2;if(r=e,typeof e=="function")Ia(e)&&(a=1);else if(typeof e=="string")a=5;else e:switch(e){case wn:return sn(n.children,l,i,t);case ea:a=8,l|=8;break;case ei:return e=rt(12,n,t,l|2),e.elementType=ei,e.lanes=i,e;case ti:return e=rt(13,n,t,l),e.elementType=ti,e.lanes=i,e;case ni:return e=rt(19,n,t,l),e.elementType=ni,e.lanes=i,e;case bu:return us(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Eu:a=10;break e;case Tu:a=9;break e;case ta:a=11;break e;case na:a=14;break e;case Lt:a=16,r=null;break e}throw Error(z(130,e==null?e:typeof e,""))}return t=rt(a,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function sn(e,t,n,r){return e=rt(7,e,r,t),e.lanes=n,e}function us(e,t,n,r){return e=rt(22,e,r,t),e.elementType=bu,e.lanes=n,e.stateNode={isHidden:!1},e}function Ks(e,t,n){return e=rt(6,e,null,t),e.lanes=n,e}function Xs(e,t,n){return t=rt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function ih(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=Ts(0),this.expirationTimes=Ts(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ts(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Oa(e,t,n,r,l,i,a,o,u){return e=new ih(e,t,n,o,u),t===1?(t=1,i===!0&&(t|=8)):t=0,i=rt(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},wa(i),e}function ah(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Sd)}catch(e){console.error(e)}}Sd(),Su.exports=Ye;var fh=Su.exports,Nd,Zo=fh;Nd=Zo.createRoot,Zo.hydrateRoot;async function _e(e){const t=await fetch(e);if(!t.ok)throw new Error(`${t.status} ${t.statusText}`);return t.json()}async function ph(){return _e("/api/archives")}async function hh(e){return _e(`/api/archives/${e}/entries`)}async function mh(e,t,n){const r=new URLSearchParams;return t&&r.set("q",t),n&&r.set("tag",n),_e(`/api/archives/${e}/entries/search?${r}`)}async function Vi(e,t){return _e(`/api/archives/${e}/entries/${t}`)}function gh(e,t,n){return Promise.all(n.map(r=>_e(`/api/archives/${e}/entries/${t}/artifacts/${r}`)))}async function vh(e){if(!e||e.length===0)return{};const t=await fetch("/api/util/resolve-tco",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});return t.ok?t.json():{}}async function yh(e,t,n){const r=await fetch(`/api/archives/${e}/entries/${t}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:n??null})});if(!r.ok)throw new Error(await r.text())}async function dl(e,t){return _e(`/api/archives/${e}/entries/${t}/tags`)}async function eu(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 xh(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 tu(e,t){const n=await fetch(`/api/archives/${e}/entries/${t}`,{method:"DELETE"});if(!n.ok)throw new Error(`Delete failed (${n.status})`)}async function wh(e,t,n){const r=await fetch(`/api/archives/${e}/tags/${t}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n})});if(!r.ok)throw new Error(await r.text());return r.json()}async function kh(e,t){const n=await fetch(`/api/archives/${e}/tags/${t}`,{method:"DELETE"});if(!n.ok)throw new Error(await n.text())}async function jh(e,t){const n=await fetch(`/api/archives/${e}/tags`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({path:t})});if(!n.ok)throw new Error(await n.text());return n.json()}async function Sh(e,t,n){const r=await fetch(`/api/archives/${e}/tags/${t}/move`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({parent_uid:n??null})});if(!r.ok)throw new Error(await r.text());return r.json()}async function Js(e){return _e(`/api/archives/${e}/runs`)}async function fl(e){return _e(`/api/archives/${e}/tags`)}async function Nh(e,t,n=null,r=null){const l={locator:t};n&&n!=="best"&&(l.quality=n),r&&(typeof r.ublock_enabled=="boolean"&&(l.ublock_enabled=r.ublock_enabled),typeof r.reader_mode=="boolean"&&(l.reader_mode=r.reader_mode),typeof r.cookie_ext_enabled=="boolean"&&(l.cookie_ext_enabled=r.cookie_ext_enabled),typeof r.modal_closer_enabled=="boolean"&&(l.modal_closer_enabled=r.modal_closer_enabled),typeof r.via_freedium=="boolean"&&(l.via_freedium=r.via_freedium));const i=await fetch(`/api/archives/${e}/captures`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)});if(!i.ok){const a=await i.json().catch(()=>({}));throw new Error(a.error||`HTTP ${i.status}`)}return i.json()}async function _h(e,t){return _e(`/api/archives/${e}/captures/probe?locator=${encodeURIComponent(t)}`)}async function _d(e,t){return _e(`/api/archives/${e}/capture_jobs/${t}`)}async function Ch(){return(await(await fetch("/api/auth/setup")).json()).setup_required===!0}async function Eh(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 Th(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 bh(){await fetch("/api/auth/logout",{method:"POST"})}async function Ph(){const e=await fetch("/api/auth/me");return e.status===401?null:e.json()}async function Lh(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 zh(e){const t=await fetch("/api/auth/me",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok)throw new Error(await t.text())}async function Rh(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 Dh(){return _e("/api/auth/tokens")}async function $h(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 Ih(e){const t=await fetch(`/api/auth/tokens/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(await t.text())}async function Ba(){return _e("/api/admin/instance-settings")}async function _l(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 Oh(){return _e("/api/admin/users")}async function Fh(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 Mh(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 Ah(){return _e("/api/admin/roles")}async function Bh(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 Cd(e){return _e(`/api/archives/${e}/collections`)}async function Uh(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 Hh(e,t){return _e(`/api/archives/${e}/collections/${t}`)}async function Ed(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 Wh(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 Vh(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 Qh(e,t){return _e(`/api/archives/${e}/entries/${t}/collections`)}async function nu(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 Kh(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)}}async function Xh(e){return _e(`/api/archives/${e}/blob-cleanup`)}async function Jh(e){const t=await fetch(`/api/archives/${e}/blob-cleanup`,{method:"DELETE"});if(!t.ok){const n=await t.json().catch(()=>({}));throw new Error(n.error??t.statusText)}return t.json()}async function qh(e,t){const n=await fetch(`/api/archives/${e}/entries/${t}/rearchive`,{method:"POST"});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.message||`rearchive failed: ${n.status}`)}return n.json()}async function Yh(){return _e("/api/admin/cookie-rules")}async function Gh(e,t,n){const r=await fetch("/api/admin/cookie-rules",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url_pattern:e||null,pattern_kind:t,cookies_json:n})});if(!r.ok){const l=await r.json().catch(()=>({}));throw new Error(l.message||`HTTP ${r.status}`)}return r.json()}async function Zh(e,t){const n=await fetch(`/api/admin/cookie-rules/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.message||`HTTP ${n.status}`)}}async function em(e){const t=await fetch(`/api/admin/cookie-rules/${e}`,{method:"DELETE"});if(!t.ok){const n=await t.json().catch(()=>({}));throw new Error(n.message||`HTTP ${t.status}`)}}const tm=window.fetch;window.fetch=async(...e)=>{var n;const t=await tm(...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 nm({onLogin:e}){const[t,n]=f.useState(""),[r,l]=f.useState(""),[i,a]=f.useState(null),[o,u]=f.useState(!1);async function c(g){g.preventDefault(),a(null),u(!0);try{const m=await Th(t,r);e(m)}catch(m){a(m.message)}finally{u(!1)}}return s.jsx("div",{className:"login-page",children:s.jsxs("div",{className:"login-card",children:[s.jsx("h1",{className:"login-brand",children:"Archivr"}),s.jsx("p",{className:"login-tagline",children:"Sign in to your archive"}),s.jsxs("form",{onSubmit:c,children:[s.jsxs("div",{className:"login-field",children:[s.jsx("label",{className:"login-label",htmlFor:"login-username",children:"Username"}),s.jsx("input",{className:"login-input",id:"login-username",type:"text",value:t,onChange:g=>n(g.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),s.jsxs("div",{className:"login-field",children:[s.jsx("label",{className:"login-label",htmlFor:"login-password",children:"Password"}),s.jsx("input",{className:"login-input",id:"login-password",type:"password",value:r,onChange:g=>l(g.target.value),required:!0,autoComplete:"current-password"})]}),i&&s.jsx("p",{className:"login-error",children:i}),s.jsx("button",{className:"login-submit",type:"submit",disabled:o,children:o?"Signing in…":"Sign in"})]})]})})}function rm({onComplete:e}){const[t,n]=f.useState(""),[r,l]=f.useState(""),[i,a]=f.useState(""),[o,u]=f.useState(null),[c,g]=f.useState(!1);async function m(h){if(h.preventDefault(),r!==i){u("Passwords do not match");return}if(r.length<8){u("Password must be at least 8 characters");return}u(null),g(!0);try{await Eh(t,r),e()}catch(y){u(y.message)}finally{g(!1)}}return s.jsx("div",{className:"setup-page",children:s.jsxs("div",{className:"setup-card",children:[s.jsx("h1",{className:"setup-brand",children:"Archivr"}),s.jsx("p",{className:"setup-tagline",children:"Create your owner account to get started."}),s.jsxs("form",{onSubmit:m,children:[s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-username",children:"Username"}),s.jsx("input",{className:"setup-input",id:"setup-username",type:"text",value:t,onChange:h=>n(h.target.value),autoFocus:!0,required:!0,autoComplete:"username"})]}),s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-password",children:"Password"}),s.jsx("input",{className:"setup-input",id:"setup-password",type:"password",value:r,onChange:h=>l(h.target.value),required:!0,autoComplete:"new-password"})]}),s.jsxs("div",{className:"setup-field",children:[s.jsx("label",{className:"setup-label",htmlFor:"setup-confirm",children:"Confirm password"}),s.jsx("input",{className:"setup-input",id:"setup-confirm",type:"password",value:i,onChange:h=>a(h.target.value),required:!0,autoComplete:"new-password"})]}),o&&s.jsx("p",{className:"setup-error",children:o}),s.jsx("button",{className:"setup-submit",type:"submit",disabled:c,children:c?"Creating account…":"Create account"})]})]})})}function lm({archives:e,archiveId:t,onArchiveChange:n,view:r,onViewChange:l,onCaptureClick:i}){const{currentUser:a,setCurrentUser:o}=f.useContext(hs)??{},[u,c]=f.useState(!1);async function g(){c(!0),await bh(),o(null),window.location.reload()}return s.jsxs("header",{className:"topbar",children:[s.jsx("div",{className:"brand",children:"Archivr"}),s.jsx("div",{className:"switcher",children:s.jsx("select",{"aria-label":"Select archive",value:t??"",onChange:m=>n(m.target.value),children:e.map(m=>s.jsx("option",{value:m.id,children:m.label},m.id))})}),s.jsx("nav",{className:"nav","aria-label":"Primary",children:["archive","tags","collections","runs","admin","settings"].map(m=>s.jsx("button",{className:`nav-link${r===m?" is-active":""}`,onClick:()=>l(m),children:m.charAt(0).toUpperCase()+m.slice(1)},m))}),s.jsx("button",{className:"capture-button",onClick:i,children:"Capture"}),a&&s.jsxs("div",{className:"user-menu",children:[s.jsx("span",{className:"user-name",children:a.display_name||a.username}),s.jsx("button",{className:"logout-btn",onClick:g,disabled:u,children:u?"Logging out…":"Log out"})]})]})}let Qi=1;function Td(e){const t=e.trim(),n=t.toLowerCase();for(const r of["yt:","youtube:"])if(n.startsWith(r)){const l=n.slice(r.length);return l.startsWith("video/")||l.startsWith("short/")||l.startsWith("shorts/")}if(n.startsWith("ytm:"))return!n.slice(4).startsWith("playlist/");for(const r of["x:","twitter:","tweet:"])if(n.startsWith(r))return n.slice(r.length).startsWith("media:");if(n.startsWith("spotify:"))return!1;if(n.startsWith("instagram:")||n.startsWith("facebook:")||n.startsWith("tiktok:")||n.startsWith("reddit:")||n.startsWith("snapchat:"))return!0;if(n.startsWith("http://")||n.startsWith("https://")){if(/^https?:\/\/music\.youtube\.com\/watch/.test(n)||/^https?:\/\/(?:www\.)?(?:youtu\.be\/[0-9A-Za-z_-]+|youtube\.com\/watch\?v=[0-9A-Za-z_-]+|youtube\.com\/shorts\/[0-9A-Za-z_-]+)/.test(t)||n.startsWith("https://x.com/")||n.startsWith("http://x.com/")||/^https?:\/\/(?:www\.)?instagram\.com\//.test(n)||/^https?:\/\/(?:www\.)?facebook\.com\//.test(n)||n.startsWith("https://fb.watch/")||n.startsWith("http://fb.watch/")||/^https?:\/\/(?:www\.)?tiktok\.com\//.test(n)||/^https?:\/\/(?:www\.)?reddit\.com\//.test(n)||n.startsWith("https://redd.it/")||n.startsWith("http://redd.it/")||/^https?:\/\/(?:www\.)?snapchat\.com\//.test(n))return!0;if(n.startsWith("https://open.spotify.com/")||n.startsWith("http://open.spotify.com/"))return!1}return!1}function ar(e=""){return{id:Qi++,locator:e,quality:"best",probeState:"idle",probeQualities:null,probeHasAudio:!1,status:"idle",error:null,jobUid:null,archiveId:null}}function ru(e){return e.some(t=>t.status==="submitting"||t.status==="running")}function sm({open:e,archiveId:t,onClose:n,onCaptured:r,onToast:l}){const i=f.useRef(null),a=f.useRef(!0),o=f.useRef(new Map),u=f.useRef(new Map),c=f.useRef(new Map),g=f.useRef(t);f.useEffect(()=>{g.current=t},[t]);const m=f.useRef(r),h=f.useRef(l);f.useEffect(()=>{m.current=r},[r]),f.useEffect(()=>{h.current=l},[l]);const[y,x]=f.useState(()=>{try{const N=JSON.parse(sessionStorage.getItem("captureItems")||"null");if(Array.isArray(N)&&N.length>0)return N.forEach(F=>{F.id>=Qi&&(Qi=F.id+1)}),N}catch{}return[ar()]});f.useEffect(()=>{sessionStorage.setItem("captureItems",JSON.stringify(y))},[y]);const[k,j]=f.useState(!1),[p,d]=f.useState(null),[v,w]=f.useState(null),[_,P]=f.useState(!0),[C,S]=f.useState(!0),[$,L]=f.useState(!0);f.useEffect(()=>{Ba().then(N=>{w(N),P(N.cookie_ext_enabled??!0),S(N.modal_closer_enabled??!0)}).catch(()=>w({}))},[]);const W=p!==null?p:(v==null?void 0:v.ublock_enabled)??!0,[A,Q]=f.useState(!1);f.useEffect(()=>{["captureDialogLocator","captureDialogError","captureDialogBusy","captureDialogJobStatus","captureDialogJobUid"].forEach(N=>sessionStorage.removeItem(N)),x(N=>N.map(F=>F.status==="submitting"?{...F,status:"idle",error:null}:F)),y.forEach(N=>{N.status==="running"&&N.jobUid&&N.archiveId&&!o.current.has(N.jobUid)&&ee(N.id,N.jobUid,N.locator,N.archiveId)})},[]),f.useEffect(()=>{const N=i.current;if(!N)return;const F=()=>{u.current.forEach(H=>clearTimeout(H)),u.current.clear(),n()};return N.addEventListener("close",F),()=>N.removeEventListener("close",F)},[n]),f.useEffect(()=>{const N=i.current;N&&(e?(!a.current&&!ru(y)&&x([ar()]),a.current=!1,N.open||N.showModal()):(u.current.forEach(F=>clearTimeout(F)),u.current.clear(),N.open&&N.close()))},[e]),f.useEffect(()=>()=>{o.current.forEach(N=>clearInterval(N)),u.current.forEach(N=>clearTimeout(N))},[]);function ee(N,F,H,X,K=null){if(o.current.has(F))return;const pe=setInterval(async()=>{try{const G=await _d(X,F);if(G.status==="completed"){clearInterval(o.current.get(F)),o.current.delete(F),x(q=>q.map(T=>T.id===N?{...T,status:"completed"}:T)),setTimeout(()=>{x(q=>{const T=q.filter(B=>B.id!==N);return T.length===0?[ar()]:T})},1400),m.current();try{const q=G.notes_json?JSON.parse(G.notes_json):null;if(q!=null&&q.ublock_skipped||q!=null&&q.cookie_ext_skipped){const B=q.ublock_skipped&&q.cookie_ext_skipped?"Captured without ad-blocking or cookie-consent extension. Check ARCHIVR_UBLOCK_EXT and ARCHIVR_COOKIE_EXT in your server config.":q.ublock_skipped?"Captured without ad-blocking. ARCHIVR_UBLOCK=true but ARCHIVR_UBLOCK_EXT is not set or the path is invalid.":"Captured without cookie-consent extension. ARCHIVR_COOKIE_EXT is not set or the path is invalid.";h.current(B,H,"warning"),le(K,"warning",H)}else K||h.current(null,H,"success"),le(K,"archived")}catch{K||h.current(null,H,"success"),le(K,"archived")}}else if(G.status==="failed"){clearInterval(o.current.get(F)),o.current.delete(F);const q=G.error_text||"Capture failed.";x(T=>T.map(B=>B.id===N?{...B,status:"failed",error:q}:B)),h.current(q,H),le(K,"failed",H)}}catch(G){clearInterval(o.current.get(F)),o.current.delete(F);const q=G.message||"Network error";x(T=>T.map(B=>B.id===N?{...B,status:"failed",error:q}:B)),h.current(q,H),le(K,"failed",H)}},500);o.current.set(F,pe)}function le(N,F,H=null){if(!N)return;const X=c.current.get(N);if(!X||(F==="archived"||F==="warning"?(X.archived++,F==="warning"&&(X.warnings++,H&&X.warningLocators.push(H))):(X.failed++,H&&X.failedLocators.push(H)),X.archived+X.failed0?`${K} archived (${pe} with warnings)`:`${K} archived`;B=G>0?`${Ze}, ${G} failed`:Ze}const Ce=K===0?"error":G>0||pe>0?"warning":"success",ke=[];q.length>0&&ke.push(`Failed: -${q.map(Ze=>` ${Ze}`).join(` -`)}`),T.length>0&&ke.push(`With warnings: -${T.map(Ze=>` ${Ze}`).join(` -`)}`);const Xn=ke.length>0?ke.join(` -`):null;h.current(Xn,null,Ce,B)}async function de(N,F=null){if(!N.locator.trim())return;const H=t,X=N.locator.trim(),K=N.quality||"best";x(pe=>pe.map(G=>G.id===N.id?{...G,status:"submitting",error:null}:G));try{const G=await Nh(H,X,K,{ublock_enabled:W,reader_mode:A,cookie_ext_enabled:_,modal_closer_enabled:C,via_freedium:$});x(q=>q.map(T=>T.id===N.id?{...T,status:"running",jobUid:G.job_uid,archiveId:H}:T)),ee(N.id,G.job_uid,X,H,F)}catch(pe){const G=pe.message||"Submission failed.";x(q=>q.map(T=>T.id===N.id?{...T,status:"failed",error:G}:T)),h.current(G,X),le(F,"failed",X)}}function ne(){var H,X;const N=y.filter(K=>K.status==="idle"&&K.locator.trim());if(N.length===0)return;const F=N.length>1?((H=crypto.randomUUID)==null?void 0:H.call(crypto))??`batch-${Date.now()}`:null;F&&c.current.set(F,{total:N.length,archived:0,warnings:0,failed:0,failedLocators:[],warningLocators:[]}),N.forEach(K=>de(K,F)),(X=i.current)==null||X.close()}function D(){x(N=>[...N,ar()])}function b(N){clearTimeout(u.current.get(N)),u.current.delete(N),x(F=>{const H=F.filter(X=>X.id!==N);return H.length===0?[ar()]:H})}function M(N){x(F=>F.map(H=>H.id===N?{...H,status:"idle",error:null}:H))}function Y(N,F){if(clearTimeout(u.current.get(N)),x(X=>X.map(K=>K.id===N?{...K,locator:F,probeState:"idle",probeQualities:null,probeHasAudio:!1,quality:"best"}:K)),!Td(F))return;const H=setTimeout(async()=>{u.current.delete(N),x(X=>X.map(K=>K.id===N?{...K,probeState:"probing"}:K));try{const X=await _h(g.current,F.trim());x(K=>K.map(pe=>{if(pe.id!==N||pe.locator!==F)return pe;const G=X.qualities??[],q=X.has_audio??!1,T=G.length===0&&q?"audio":"best";return{...pe,probeState:"done",probeQualities:G,probeHasAudio:q,quality:T}}))}catch{x(X=>X.map(K=>K.id===N?{...K,probeState:"idle",probeQualities:null}:K))}},600);u.current.set(N,H)}function U(N,F){x(H=>H.map(X=>X.id===N?{...X,quality:F}:X))}const fe=y.filter(N=>N.status==="idle"&&N.locator.trim()).length,ae=ru(y);return s.jsx("dialog",{ref:i,className:"capture-dialog",children:s.jsxs("div",{className:"capture-dialog-inner",children:[s.jsxs("div",{className:"capture-dialog-header",children:[s.jsx("h2",{className:"capture-dialog-title",children:"Capture"}),s.jsx("button",{type:"button",className:"capture-dialog-close",onClick:()=>{var N;return(N=i.current)==null?void 0:N.close()},"aria-label":"Close",children:s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"3",y1:"3",x2:"13",y2:"13"}),s.jsx("line",{x1:"13",y1:"3",x2:"3",y2:"13"})]})})]}),s.jsx("div",{className:"capture-rows",children:y.map((N,F)=>s.jsx(im,{item:N,autoFocus:F===y.length-1&&N.status==="idle",onLocatorChange:H=>Y(N.id,H),onQualityChange:H=>U(N.id,H),onRemove:()=>b(N.id),onReset:()=>M(N.id),onSubmit:ne},N.id))}),s.jsxs("button",{type:"button",className:"capture-add-row",onClick:D,children:[s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"8",y1:"2",x2:"8",y2:"14"}),s.jsx("line",{x1:"2",y1:"8",x2:"14",y2:"8"})]}),"Add another"]}),s.jsxs("div",{className:"capture-advanced",children:[s.jsxs("button",{type:"button",className:"capture-advanced-toggle",onClick:()=>j(N=>!N),"aria-expanded":k,children:[s.jsx("svg",{className:`capture-chevron${k?" capture-chevron--open":""}`,viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:s.jsx("polyline",{points:"4 6 8 10 12 6"})}),"Advanced options"]}),k&&s.jsxs("div",{className:"capture-advanced-panel",children:[s.jsxs("label",{className:"capture-ext-row",children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"uBlock Origin Lite"}),s.jsx("span",{className:"capture-ext-desc",children:"Block ads during this capture"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":W,className:`ext-toggle ext-toggle--sm${W?" ext-toggle--on":""}`,onClick:()=>d(N=>N===null?!W:!N),"aria-label":"Toggle uBlock for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Block cookie banners"}),s.jsx("span",{className:"capture-ext-desc",children:"Dismiss cookie consent banners during this capture"}),!(v!=null&&v.cookie_ext_available)&&s.jsxs("span",{className:"capture-ext-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_COOKIE_EXT"})]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":_,className:`ext-toggle ext-toggle--sm${_?" ext-toggle--on":""}`,onClick:()=>P(N=>!N),"aria-label":"Toggle cookie banner blocking for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Reader mode"}),s.jsx("span",{className:"capture-ext-desc",children:"Distil to article text via Readability (off by default)"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":A,className:`ext-toggle ext-toggle--sm${A?" ext-toggle--on":""}`,onClick:()=>Q(N=>!N),"aria-label":"Toggle reader mode for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Close modals and dialogs"}),s.jsx("span",{className:"capture-ext-desc",children:"Auto-dismiss cookie banners and overlays during this capture"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":C,className:`ext-toggle ext-toggle--sm${C?" ext-toggle--on":""}`,onClick:()=>S(N=>!N),"aria-label":"Toggle modal closer for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]}),s.jsxs("label",{className:"capture-ext-row",style:{marginTop:8},children:[s.jsxs("span",{className:"capture-ext-label",children:[s.jsx("span",{className:"capture-ext-name",children:"Freedium mirror"}),s.jsx("span",{className:"capture-ext-desc",children:"Route paywalled articles through a Freedium mirror (Medium, NYT, WaPo, etc.)"})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":$,className:`ext-toggle ext-toggle--sm${$?" ext-toggle--on":""}`,onClick:()=>L(N=>!N),"aria-label":"Toggle Freedium mirror for this capture",children:s.jsx("span",{className:"ext-toggle-knob"})})]})]})]}),s.jsxs("div",{className:"capture-actions",children:[s.jsx("button",{type:"button",className:"capture-submit",onClick:ne,disabled:fe===0,children:fe>1?`Archive ${fe}`:"Archive"}),s.jsx("button",{type:"button",className:"capture-cancel",onClick:()=>{var N;return(N=i.current)==null?void 0:N.close()},children:ae?"Close":"Cancel"})]})]})})}function im({item:e,autoFocus:t,onLocatorChange:n,onQualityChange:r,onRemove:l,onReset:i,onSubmit:a}){const o=f.useRef(null),u=e.status==="submitting"||e.status==="running";f.useEffect(()=>{var g;t&&e.status==="idle"&&((g=o.current)==null||g.focus())},[t]);const c=(()=>{if(e.status==="completed"||u||!Td(e.locator))return null;if(e.probeState==="probing")return s.jsx("span",{className:"capture-quality-probing","aria-label":"Checking available qualities",children:"…"});if(e.probeState==="done"){const g=e.probeQualities??[],m=e.probeHasAudio??!1;return g.length===0&&!m?s.jsx("span",{className:"capture-quality-hint",children:"No media detected"}):g.length===0&&m?s.jsx("select",{className:"capture-quality",value:"audio",onChange:h=>r(h.target.value),"aria-label":"Video quality",children:s.jsx("option",{value:"audio",children:"Audio only"})}):s.jsxs("select",{className:"capture-quality",value:e.quality||"best",onChange:h=>r(h.target.value),"aria-label":"Video quality",children:[s.jsx("option",{value:"best",children:"Best quality"}),g.map(h=>s.jsx("option",{value:h,children:h},h)),m&&s.jsx("option",{value:"audio",children:"Audio only"})]})}return null})();return s.jsxs("div",{className:`capture-row capture-row--${e.status}`,children:[s.jsxs("div",{className:"capture-row-main",children:[s.jsx(am,{status:e.status}),s.jsx("input",{ref:o,className:"capture-input",type:"text",placeholder:"https://… · yt:ID · ytm:ID · tweet:ID · x:ID",value:e.locator,onChange:g=>n(g.target.value),onKeyDown:g=>{g.key==="Enter"&&a()},disabled:u||e.status==="completed",autoComplete:"off",spellCheck:!1}),c,e.status==="failed"&&s.jsx("button",{type:"button",className:"capture-row-action",onClick:i,title:"Retry",children:s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[s.jsx("path",{d:"M13 2.5A7 7 0 1 1 6.5 1"}),s.jsx("polyline",{points:"6.5 1 4 3.5 6.5 6"})]})}),!u&&e.status!=="completed"&&e.status!=="failed"&&s.jsx("button",{type:"button",className:"capture-row-action capture-row-remove",onClick:l,"aria-label":"Remove",children:s.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("line",{x1:"3",y1:"3",x2:"13",y2:"13"}),s.jsx("line",{x1:"13",y1:"3",x2:"3",y2:"13"})]})})]}),e.error&&s.jsx("p",{className:"capture-row-error",children:e.error})]})}function am({status:e}){return e==="submitting"||e==="running"?s.jsx("span",{className:"cap-dot cap-dot--running","aria-label":"Running",children:s.jsx("span",{className:"cap-spinner"})}):e==="completed"?s.jsx("span",{className:"cap-dot cap-dot--ok","aria-label":"Done",children:"✓"}):e==="failed"?s.jsx("span",{className:"cap-dot cap-dot--err","aria-label":"Failed",children:"✕"}):s.jsx("span",{className:"cap-dot cap-dot--idle","aria-hidden":"true"})}function ql(e){if(!e)return"0 B";const t=["B","KB","MB","GB"];let n=e,r=0;for(;n>=1024&&r").replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'")}function rn(e){return om(e)??""}function bd(e){if(!e)return"";const t=new Date(e);if(isNaN(t))return e;const n=r=>String(r).padStart(2,"0");return`${t.getUTCFullYear()}-${n(t.getUTCMonth()+1)}-${n(t.getUTCDate())} ${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}const lu={youtube:'',youtube_music:'',spotify:'',x:'',instagram:'',facebook:'',tiktok:'',reddit:'',snapchat:'',local:'',web:'',other:'?'};function Ua(e){return lu[e]??lu.other}function Pd(e){return e.split("/").map(t=>t?t.split("-").map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join(" "):"").join("/")}function um({entry:e,archiveId:t,isSelected:n,isMultiSelected:r,onRowClick:l}){const[i,a]=f.useState(!1),u=e.source_kind==="web"&&e.entity_kind==="page"&&e.has_favicon&&t&&!i?s.jsx("img",{src:`/api/archives/${t}/entries/${e.entry_uid}/favicon`,width:"16",height:"16",alt:"",onError:()=>a(!0),style:{objectFit:"contain"}}):s.jsx("span",{dangerouslySetInnerHTML:{__html:Ua(e.source_kind)}}),c=n||r;function g(m){m.stopPropagation(),l(e,{ctrlKey:!0,metaKey:!1,shiftKey:!1,preventDefault(){}})}return s.jsxs("div",{className:[n&&"is-selected",r&&"is-multi-selected"].filter(Boolean).join(" ")||void 0,tabIndex:0,"data-entry-uid":e.entry_uid,onMouseDown:m=>{m.shiftKey&&m.preventDefault()},onClick:m=>l(e,m),onKeyDown:m=>{m.key==="Enter"&&l(e,m)},children:[s.jsx("div",{className:"col-check",children:s.jsx("button",{type:"button",className:`row-checkbox${c?" is-checked":""}`,"aria-pressed":c,"aria-label":c?"Deselect entry":"Select entry",onClick:g,onKeyDown:m=>m.stopPropagation()})}),s.jsx("div",{className:"col-added",children:bd(e.archived_at)}),s.jsxs("div",{className:"col-title",children:[s.jsx("span",{className:"source-icon",children:u}),s.jsx("span",{className:"entry-title",children:rn(e.title)||rn(e.entry_uid)})]}),s.jsx("div",{className:"col-type",children:s.jsx("span",{className:"type-pill",children:rn(e.entity_kind)})}),s.jsxs("div",{className:"col-size",children:[s.jsx("span",{className:"size-total",children:ql(e.total_artifact_bytes)}),e.cached_bytes>0&&e.total_artifact_bytes>0&&s.jsxs("span",{className:"size-cached-pct",title:`${ql(e.cached_bytes)} already on disk from an earlier entry`,children:[Math.round(e.cached_bytes/e.total_artifact_bytes*100),"% cached"]})]}),s.jsx("div",{className:"url-cell col-url",children:rn(e.original_url)})]})}function cm({entries:e,selectedUids:t,onRowClick:n,archiveId:r}){return s.jsx("section",{id:"archive-view",className:"view is-active",children:s.jsxs("div",{className:"entry-table",children:[s.jsxs("div",{className:"entry-header-row",children:[s.jsx("div",{className:"col-check","aria-hidden":"true"}),s.jsx("div",{className:"col-added",children:"Added"}),s.jsx("div",{className:"col-title",children:"Title"}),s.jsx("div",{className:"col-type",children:"Type"}),s.jsx("div",{className:"col-size",children:"Size"}),s.jsx("div",{className:"col-url",children:"Original URL"})]}),s.jsx("div",{id:"entries-body",children:e.map(l=>s.jsx(um,{entry:l,archiveId:r,isSelected:t.size===1&&t.has(l.entry_uid),isMultiSelected:t.size>=2&&t.has(l.entry_uid),onRowClick:n},l.entry_uid))})]})})}function dm(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 fm({status:e}){const t=e==="completed"?"run-status--completed":e==="failed"?"run-status--failed":e==="in_progress"?"run-status--in-progress":"",n=e?e.replace(/_/g," "):"—";return s.jsx("span",{className:`run-status ${t}`,children:n})}function pm({runs:e}){const[t,n]=f.useState(null);function r(l){n(i=>i===l?null:l)}return s.jsx("section",{id:"runs-view",className:"view is-active",children:s.jsxs("table",{className:"entry-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Started"}),s.jsx("th",{children:"Status"}),s.jsx("th",{children:"Requested"}),s.jsx("th",{children:"Completed"}),s.jsx("th",{children:"Failed"})]})}),s.jsx("tbody",{children:e.length===0?s.jsx("tr",{children:s.jsx("td",{colSpan:5,style:{color:"var(--muted)",padding:"24px 16px",textAlign:"center"},children:"No runs yet."})}):e.map(l=>{const i=l.status==="failed"&&l.error_summary,a=t===l.run_uid;return[s.jsxs("tr",{className:i?"run-row run-row--failed":"run-row",onClick:i?()=>r(l.run_uid):void 0,title:i?a?"Click to hide error":"Click to view error":void 0,children:[s.jsx("td",{children:dm(l.started_at)}),s.jsxs("td",{children:[s.jsx(fm,{status:l.status}),i&&s.jsx("span",{className:"run-expand-hint","aria-hidden":"true",children:a?"▴":"▾"})]}),s.jsx("td",{children:l.requested_count??"—"}),s.jsx("td",{children:l.completed_count??"—"}),s.jsx("td",{children:l.failed_count??"—"})]},l.run_uid),i&&a&&s.jsx("tr",{className:"run-error-row",children:s.jsx("td",{colSpan:5,children:s.jsx("pre",{className:"run-error-detail",children:l.error_summary})})},`${l.run_uid}-detail`)]})})]})})}const hm=4;function mm({archives:e}){const{currentUser:t}=f.useContext(hs)??{},n=t&&(t.role_bits&hm)!==0,[r,l]=f.useState("users"),[i,a]=f.useState([]),[o,u]=f.useState([]),[c,g]=f.useState(!1),[m,h]=f.useState(null),[y,x]=f.useState(""),[k,j]=f.useState(""),[p,d]=f.useState(""),[v,w]=f.useState(null),[_,P]=f.useState(!1),[C,S]=f.useState(""),[$,L]=f.useState(""),[W,A]=f.useState(null),[Q,ee]=f.useState(!1),le=f.useCallback(async()=>{if(n){g(!0),h(null);try{const[b,M]=await Promise.all([Oh(),Ah()]);a(b),u(M)}catch(b){h(b.message)}finally{g(!1)}}},[n]);f.useEffect(()=>{le()},[le]);async function de(b){const M=b.status==="active"?"disabled":"active";try{await Mh(b.user_uid,M),a(Y=>Y.map(U=>U.user_uid===b.user_uid?{...U,status:M}:U))}catch(Y){h(Y.message)}}async function ne(b){if(b.preventDefault(),!y.trim()||!k){w("Username and password required");return}P(!0),w(null);try{await Fh(y.trim(),k,p.trim()||void 0),x(""),j(""),d(""),await le()}catch(M){w(M.message)}finally{P(!1)}}async function D(b){if(b.preventDefault(),!C.trim()||!$.trim()){A("Slug and name required");return}ee(!0),A(null);try{await Bh(C.trim(),$.trim()),S(""),L(""),await le()}catch(M){A(M.message)}finally{ee(!1)}}return n?s.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[s.jsx("h1",{children:"Admin"}),s.jsxs("div",{className:"view-tabs",children:[s.jsx("button",{className:`view-tab${r==="users"?" is-active":""}`,onClick:()=>l("users"),children:"Users"}),s.jsx("button",{className:`view-tab${r==="roles"?" is-active":""}`,onClick:()=>l("roles"),children:"Roles"}),s.jsx("button",{className:`view-tab${r==="archives"?" is-active":""}`,onClick:()=>l("archives"),children:"Archives"})]}),m&&s.jsx("div",{className:"form-msg form-msg--err",children:m}),r==="users"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Users"}),c?s.jsx("p",{className:"muted",children:"Loading…"}):s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Username"}),s.jsx("th",{children:"Email"}),s.jsx("th",{children:"Roles"}),s.jsx("th",{children:"Status"}),s.jsx("th",{children:"Actions"})]})}),s.jsx("tbody",{children:i.map(b=>s.jsxs("tr",{className:b.status==="disabled"?"admin-row-disabled":"",children:[s.jsx("td",{children:b.username}),s.jsx("td",{className:"muted",children:b.email||"—"}),s.jsx("td",{children:b.role_slugs.join(", ")||"—"}),s.jsx("td",{children:s.jsx("span",{className:`status-badge status-${b.status}`,children:b.status})}),s.jsx("td",{children:s.jsx("button",{className:"admin-action-btn",onClick:()=>de(b),children:b.status==="active"?"Ban":"Unban"})})]},b.user_uid))})]}),s.jsx("h3",{children:"Create User"}),s.jsxs("form",{className:"admin-form",onSubmit:ne,children:[s.jsx("input",{className:"admin-input",placeholder:"Username",value:y,onChange:b=>x(b.target.value),required:!0}),s.jsx("input",{className:"admin-input",type:"password",placeholder:"Password (min 8 chars)",value:k,onChange:b=>j(b.target.value),required:!0}),s.jsx("input",{className:"admin-input",type:"email",placeholder:"Email (optional)",value:p,onChange:b=>d(b.target.value)}),v&&s.jsx("div",{className:"form-msg form-msg--err",children:v}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:_,children:_?"Creating…":"Create User"})]})]}),r==="roles"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Roles"}),c?s.jsx("p",{className:"muted",children:"Loading…"}):s.jsxs("table",{className:"admin-table",children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Slug"}),s.jsx("th",{children:"Name"}),s.jsx("th",{children:"Level"}),s.jsx("th",{children:"Bit"}),s.jsx("th",{children:"Built-in"})]})}),s.jsx("tbody",{children:o.map(b=>s.jsxs("tr",{children:[s.jsx("td",{children:s.jsx("code",{children:b.slug})}),s.jsx("td",{children:b.name}),s.jsx("td",{children:b.level}),s.jsx("td",{children:b.bit_position}),s.jsx("td",{children:b.is_builtin?"✓":""})]},b.role_uid))})]}),s.jsx("h3",{children:"Create Custom Role"}),s.jsxs("form",{className:"admin-form",onSubmit:D,children:[s.jsx("input",{className:"admin-input",placeholder:"Slug (e.g. moderator)",value:C,onChange:b=>S(b.target.value),required:!0}),s.jsx("input",{className:"admin-input",placeholder:"Display Name (e.g. Moderator)",value:$,onChange:b=>L(b.target.value),required:!0}),W&&s.jsx("div",{className:"form-msg form-msg--err",children:W}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:Q,children:Q?"Creating…":"Create Role"})]})]}),r==="archives"&&s.jsxs("div",{className:"admin-section",children:[s.jsx("h2",{children:"Mounted Archives"}),s.jsx("div",{className:"admin-list",children:e.map(b=>s.jsxs("div",{className:"admin-archive",children:[s.jsx("strong",{children:b.label}),s.jsx("div",{className:"muted",children:b.archive_path})]},b.id))})]})]}):s.jsxs("section",{id:"admin-view",className:"view admin-view is-active",children:[s.jsx("h1",{children:"Admin"}),s.jsx("p",{className:"muted",children:"You need admin privileges to access this panel."}),s.jsx("h2",{children:"Mounted Archives"}),s.jsx("div",{className:"admin-list",children:e.map(b=>s.jsxs("div",{className:"admin-archive",children:[s.jsx("strong",{children:b.label}),s.jsx("div",{className:"muted",children:b.archive_path})]},b.id))})]})}function Ld({node:e,onPick:t}){var n;return s.jsxs("li",{children:[s.jsx("button",{className:"tag-picker-node-btn",title:e.tag.full_path,onClick:()=>t(e.tag),children:e.tag.slug}),((n=e.children)==null?void 0:n.length)>0&&s.jsx("div",{className:"tag-children",children:s.jsx("ul",{className:"tag-tree-list",children:e.children.map(r=>s.jsx(Ld,{node:r,onPick:t},r.tag.tag_uid))})})]})}function su({title:e,tagNodes:t,excludeUid:n,onPick:r,onCancel:l}){f.useEffect(()=>{function o(u){u.key==="Escape"&&(u.preventDefault(),u.stopPropagation(),l())}return document.addEventListener("keydown",o),()=>document.removeEventListener("keydown",o)},[l]);function i(o){return o.filter(u=>u.tag.tag_uid!==n).map(u=>({...u,children:i(u.children)}))}const a=n?i(t):t;return s.jsx("div",{className:"tag-picker-backdrop",onClick:o=>{o.target===o.currentTarget&&l()},children:s.jsxs("div",{className:"tag-picker-modal",role:"dialog","aria-modal":"true",children:[s.jsxs("div",{className:"tag-picker-header",children:[s.jsx("span",{className:"tag-picker-title",children:e}),s.jsx("button",{className:"tag-picker-close",onClick:l,title:"Cancel","aria-label":"Cancel",children:"×"})]}),s.jsxs("div",{className:"tag-picker-body",children:[s.jsx("button",{className:"tag-picker-root-btn",onClick:()=>r(null),title:"Place at root level (no parent)",children:"↑ Root tag (no parent)"}),a.length>0?s.jsx("ul",{className:"tag-tree-list tag-picker-tree",children:a.map(o=>s.jsx(Ld,{node:o,onPick:r},o.tag.tag_uid))}):s.jsx("p",{className:"tag-picker-empty",children:"No other tags available."})]})]})})}function zd({parentPath:e,archiveId:t,onDone:n,onCancel:r}){const[l,i]=f.useState(""),a=f.useRef(!1);async function o(){const u=l.trim();if(!u){r();return}const c=e?`${e}/${u}`:`/${u}`;try{await jh(t,c),n()}catch(g){alert(g.message||"Create failed"),r()}}return s.jsx("input",{className:"tag-rename-input",autoFocus:!0,placeholder:"tag name",value:l,onChange:u=>i(u.target.value),onKeyDown:u=>{u.key==="Enter"&&u.currentTarget.blur(),u.key==="Escape"&&(a.current=!0,u.currentTarget.blur())},onBlur:()=>{a.current?(a.current=!1,r()):o()}})}function Rd({node:e,archiveId:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u,moveSelectMode:c,onMoveSourceSelect:g,pendingCreateParentUid:m,onCreateDone:h,onCreateCancel:y}){var W;const x=n===e.tag.full_path,[k,j]=f.useState(!1),[p,d]=f.useState(""),v=f.useRef(!1);function w(){if(k)return;if(c){g(e);return}const A=x?null:e.tag.full_path;r(A),l("archive")}function _(A){A.stopPropagation(),!c&&(d(e.tag.slug),j(!0))}async function P(){const A=p.trim();if(!A||A===e.tag.slug){j(!1);return}try{const Q=await wh(t,e.tag.tag_uid,A);i(e.tag.full_path,Q.full_path),o()}catch{}finally{j(!1)}}async function C(A){var ee;A.stopPropagation();const Q=((ee=e.children)==null?void 0:ee.length)>0?`Delete tag "${e.tag.full_path}" and all its child tags? This cannot be undone.`:`Delete tag "${e.tag.full_path}"? This cannot be undone.`;if(window.confirm(Q))try{await kh(t,e.tag.tag_uid),a(e.tag.full_path),o()}catch{}}const S={archiveId:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u,moveSelectMode:c,onMoveSourceSelect:g,pendingCreateParentUid:m,onCreateDone:h,onCreateCancel:y},$=m===e.tag.tag_uid,L=((W=e.children)==null?void 0:W.length)>0;return s.jsxs("li",{children:[s.jsxs("div",{className:`tag-node-row${c?" tag-node-row--move-select":""}`,children:[k?s.jsx("input",{className:"tag-rename-input",autoFocus:!0,value:p,onChange:A=>d(A.target.value),onKeyDown:A=>{A.key==="Enter"&&A.currentTarget.blur(),A.key==="Escape"&&(v.current=!0,A.currentTarget.blur())},onBlur:()=>{v.current?(v.current=!1,j(!1)):P()}}):s.jsxs("button",{className:`tag-node-btn${x?" is-active":""}${c?" tag-node-btn--move-select":""}`,title:c?`Select "${e.tag.full_path}" to move`:e.tag.full_path,onClick:w,onDoubleClick:c?void 0:_,children:[s.jsx("span",{className:"tag-node-label",children:u?e.tag.name:e.tag.slug}),s.jsx("span",{className:"tag-node-count",children:e.children.length===0?`(${e.entry_count})`:`(${e.entry_count}) (${e.subtree_count} Total)`}),!c&&s.jsx("svg",{className:"edit-icon",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",title:"Rename tag",onClick:A=>{A.stopPropagation(),_(A)},children:s.jsx("path",{d:"M11.5 2.5a1.5 1.5 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"})})]}),!k&&!c&&s.jsx("button",{className:"remove tag-node-delete",title:`Delete "${e.tag.full_path}"`,onClick:C,"aria-label":`Delete "${e.tag.full_path}"`,children:"×"})]}),(L||$)&&s.jsx("div",{className:"tag-children",children:s.jsxs("ul",{className:"tag-tree-list",children:[e.children.map(A=>s.jsx(Rd,{node:A,...S},A.tag.tag_uid)),$&&s.jsx("li",{children:s.jsx(zd,{parentPath:e.tag.full_path,archiveId:t,onDone:h,onCancel:y})})]})})]})}function gm({archiveId:e,tagNodes:t,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u}){const[c,g]=f.useState(null),[m,h]=f.useState(null);function y(){g("select-source"),h(null)}function x(){g(null),h(null)}f.useEffect(()=>{if(c!=="select-source")return;function Q(ee){ee.key==="Escape"&&(ee.preventDefault(),ee.stopPropagation(),x())}return document.addEventListener("keydown",Q),()=>document.removeEventListener("keydown",Q)},[c]);function k(Q){h(Q),g("select-dest")}async function j(Q){const ee=m.tag.full_path,le=m.tag.tag_uid,de=(Q==null?void 0:Q.tag_uid)??null;x();try{const ne=await Sh(e,le,de);i(ee,ne.full_path),o()}catch(ne){alert(ne.message||"Move failed")}}const[p,d]=f.useState(null),[v,w]=f.useState(void 0);function _(){d("select-parent"),w(void 0)}function P(){d(null),w(void 0)}function C(Q){w(Q??null),d("input")}function S(){d(null),w(void 0),o()}const $=c==="select-source",L=p==="input"?v?v.tag_uid:"__root__":void 0,W={archiveId:e,tagFilter:n,onTagFilterSet:r,onViewChange:l,onTagRenamed:i,onTagDeleted:a,onTagsRefresh:o,humanizeTags:u,moveSelectMode:$,onMoveSourceSelect:k,pendingCreateParentUid:L,onCreateDone:S,onCreateCancel:P},A=L==="__root__";return s.jsxs("section",{id:"tags-view",className:"view is-active",children:[s.jsxs("div",{className:"tag-tree",children:[s.jsx("div",{className:"tag-tree-header",children:$?s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"tag-tree-title tag-tree-title--move",children:"Select a tag to move"}),s.jsx("button",{className:"tag-tree-action-btn tag-tree-action-btn--cancel",onClick:x,children:"Cancel"})]}):s.jsxs(s.Fragment,{children:[s.jsx("span",{className:"tag-tree-title",children:"Tags"}),n&&s.jsxs("span",{className:"tag-tree-active",title:n,children:["Filtering: ",n]}),s.jsxs("div",{className:"tag-tree-actions",children:[s.jsx("button",{className:"tag-tree-action-btn",onClick:_,title:"Create a new tag",disabled:!!p||!!c,children:"+ New"}),s.jsx("button",{className:"tag-tree-action-btn",onClick:y,title:"Move a tag to a different parent",disabled:!!p||!!c||t.length===0,children:"Move"})]})]})}),t.length===0&&!A?s.jsx("p",{className:"muted",style:{padding:"8px 0"},children:"No tags yet."}):s.jsxs("ul",{className:"tag-tree-list",children:[t.map(Q=>s.jsx(Rd,{node:Q,...W},Q.tag.tag_uid)),A&&s.jsx("li",{children:s.jsx(zd,{parentPath:null,archiveId:e,onDone:S,onCancel:P})})]})]}),p==="select-parent"&&s.jsx(su,{title:"Create tag under…",tagNodes:t,excludeUid:null,onPick:C,onCancel:P}),c==="select-dest"&&m&&s.jsx(su,{title:`Move "${m.tag.slug}" under…`,tagNodes:t,excludeUid:m.tag.tag_uid,onPick:j,onCancel:x})]})}const pr=[{value:0,label:"Private"},{value:2,label:"Users only"},{value:3,label:"Public"}],vm=e=>{var t;return((t=pr.find(n=>n.value===e))==null?void 0:t.label)??String(e)};function ym({archiveId:e}){const[t,n]=f.useState([]),[r,l]=f.useState(!1),[i,a]=f.useState(null),[o,u]=f.useState(null),[c,g]=f.useState(null),[m,h]=f.useState(!1),[y,x]=f.useState(null),[k,j]=f.useState(""),[p,d]=f.useState(""),[v,w]=f.useState(2),[_,P]=f.useState(!1),[C,S]=f.useState(null),[$,L]=f.useState(""),[W,A]=f.useState(2),[Q,ee]=f.useState(!1),[le,de]=f.useState(null),[ne,D]=f.useState(!1),[b,M]=f.useState(""),Y=f.useRef(null),U=t.find(T=>T.collection_uid===o)??null,fe=(U==null?void 0:U.slug)==="_default_",ae=f.useCallback(async()=>{if(e){l(!0),a(null);try{const T=await Cd(e);n(T)}catch(T){a(T.message)}finally{l(!1)}}},[e]),N=f.useCallback(async T=>{if(!T){g(null);return}h(!0),x(null);try{const B=await Hh(e,T);g(B)}catch(B){x(B.message)}finally{h(!1)}},[e]);f.useEffect(()=>{ae()},[ae]),f.useEffect(()=>{N(o)},[o,N]),f.useEffect(()=>{ne&&Y.current&&Y.current.focus()},[ne]);async function F(T){T.preventDefault();const B=k.trim(),Ce=p.trim();if(!(!B||!Ce)){P(!0),S(null);try{const ke=await Uh(e,B,Ce,v);j(""),d(""),w(2),await ae(),u(ke.collection_uid)}catch(ke){S(ke.message)}finally{P(!1)}}}async function H(){const T=b.trim();if(!T||!U){D(!1);return}try{await nu(e,U.collection_uid,{name:T}),await ae(),g(B=>B&&{...B,name:T})}catch(B){a(B.message)}finally{D(!1)}}async function X(T){if(U)try{await nu(e,U.collection_uid,{default_visibility_bits:T}),await ae(),g(B=>B&&{...B,default_visibility_bits:T})}catch(B){a(B.message)}}async function K(){if(U&&window.confirm(`Delete collection "${U.name}"? Entries will not be deleted.`))try{await Kh(e,U.collection_uid),u(null),g(null),await ae()}catch(T){a(T.message)}}async function pe(T){T.preventDefault();const B=$.trim();if(!(!B||!U)){ee(!0),de(null);try{await Ed(e,U.collection_uid,B,W),L(""),await N(U.collection_uid)}catch(Ce){de(Ce.message)}finally{ee(!1)}}}async function G(T){if(U)try{await Wh(e,U.collection_uid,T),await N(U.collection_uid)}catch(B){x(B.message)}}async function q(T,B){if(U)try{await Vh(e,U.collection_uid,T,B),g(Ce=>Ce&&{...Ce,entries:Ce.entries.map(ke=>ke.entry_uid===T?{...ke,collection_visibility_bits:B}:ke)})}catch(Ce){x(Ce.message)}}return e?s.jsxs("div",{className:"collections-view",children:[s.jsx("h2",{className:"collections-heading",children:"Collections"}),r&&s.jsx("div",{className:"muted",children:"Loading…"}),i&&s.jsxs("div",{className:"collections-error",children:[i," ",s.jsx("button",{onClick:()=>a(null),className:"coll-dismiss",children:"×"})]}),s.jsxs("div",{className:"collections-layout",children:[s.jsxs("div",{className:"collections-sidebar",children:[t.map(T=>s.jsxs("button",{className:`coll-sidebar-row${o===T.collection_uid?" is-active":""}`,onClick:()=>u(T.collection_uid),children:[s.jsx("span",{className:"coll-row-name",children:T.name}),s.jsx("span",{className:"coll-row-meta",children:vm(T.default_visibility_bits)})]},T.collection_uid)),t.length===0&&!r&&s.jsx("div",{className:"muted",style:{padding:"8px 12px"},children:"No collections yet."})]}),U?s.jsxs("div",{className:"coll-detail",children:[s.jsxs("div",{className:"coll-detail-header",children:[ne?s.jsx("input",{ref:Y,className:"coll-rename-input",value:b,onChange:T=>M(T.target.value),onBlur:H,onKeyDown:T=>{T.key==="Enter"&&H(),T.key==="Escape"&&D(!1)}}):s.jsxs("h3",{className:`coll-detail-name${fe?"":" coll-detail-name--editable"}`,title:fe?void 0:"Click to rename",onClick:()=>{fe||(M(U.name),D(!0))},children:[(c==null?void 0:c.name)??U.name,!fe&&s.jsx("span",{className:"coll-edit-hint",children:" ✎"})]}),!fe&&s.jsx("button",{className:"coll-delete-btn",onClick:K,title:"Delete collection",children:"Delete"})]}),s.jsxs("div",{className:"coll-detail-vis",children:[s.jsx("span",{className:"coll-vis-label",children:"Default visibility"}),s.jsx("select",{className:"coll-vis-select",value:(c==null?void 0:c.default_visibility_bits)??U.default_visibility_bits,onChange:T=>X(Number(T.target.value)),disabled:fe,children:pr.map(T=>s.jsx("option",{value:T.value,children:T.label},T.value))})]}),s.jsxs("div",{className:"coll-entries-section",children:[s.jsx("div",{className:"coll-section-heading",children:"Entries"}),m&&s.jsx("div",{className:"muted",children:"Loading…"}),y&&s.jsx("div",{className:"collections-error",children:y}),!m&&c&&(c.entries.length===0?s.jsx("div",{className:"muted",children:"No entries in this collection."}):s.jsx("ul",{className:"coll-entries-list",children:c.entries.map(T=>s.jsxs("li",{className:"coll-entry-row",children:[s.jsxs("div",{className:"coll-entry-info",children:[s.jsx("span",{className:"coll-entry-title",children:T.title||T.entry_uid}),s.jsx("span",{className:"coll-entry-kind muted",children:T.source_kind})]}),s.jsxs("div",{className:"coll-entry-actions",children:[s.jsx("select",{className:"coll-entry-vis-select",value:T.collection_visibility_bits,onChange:B=>q(T.entry_uid,Number(B.target.value)),children:pr.map(B=>s.jsx("option",{value:B.value,children:B.label},B.value))}),!fe&&s.jsx("button",{className:"coll-entry-remove",onClick:()=>G(T.entry_uid),title:"Remove from collection",children:"×"})]})]},T.entry_uid))}))]}),!fe&&s.jsxs("form",{className:"coll-add-entry-form",onSubmit:pe,children:[s.jsx("div",{className:"coll-section-heading",children:"Add entry"}),s.jsxs("div",{className:"coll-add-entry-row",children:[s.jsx("input",{className:"coll-add-entry-input",type:"text",value:$,onChange:T=>L(T.target.value),placeholder:"entry_uid",required:!0}),s.jsx("select",{className:"coll-vis-select",value:W,onChange:T=>A(Number(T.target.value)),children:pr.map(T=>s.jsx("option",{value:T.value,children:T.label},T.value))}),s.jsx("button",{className:"coll-add-btn",type:"submit",disabled:Q,children:Q?"…":"Add"})]}),le&&s.jsx("div",{className:"collections-error",style:{marginTop:4},children:le})]})]}):s.jsx("div",{className:"coll-detail coll-detail--empty",children:s.jsx("div",{className:"muted",children:"Select a collection to view details."})})]}),s.jsxs("details",{className:"coll-create-details",children:[s.jsx("summary",{children:"+ Create collection"}),s.jsxs("form",{className:"coll-create-form",onSubmit:F,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-name",children:"Name"}),s.jsx("input",{className:"capture-input",id:"coll-name",type:"text",value:k,onChange:T=>{j(T.target.value),p||d(T.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""))},placeholder:"My Collection",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-slug",children:"Slug"}),s.jsx("input",{className:"capture-input",id:"coll-slug",type:"text",value:p,onChange:T=>d(T.target.value),placeholder:"my-collection",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"coll-vis",children:"Default visibility"}),s.jsx("select",{className:"capture-input",id:"coll-vis",style:{height:42},value:v,onChange:T=>w(Number(T.target.value)),children:pr.map(T=>s.jsx("option",{value:T.value,children:T.label},T.value))})]}),C&&s.jsx("div",{className:"collections-error",children:C}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:_,children:_?"Creating…":"Create collection"})]})]})]}):s.jsx("div",{className:"view-placeholder",children:"Select an archive."})}const xm=4;function wm({tab:e,onTabChange:t,archiveId:n}){const{currentUser:r,setCurrentUser:l}=f.useContext(hs)??{},i=r&&(r.role_bits&xm)!==0,a=["profile","tokens",...i?["instance","cookies","extensions","storage"]:[]],o={profile:"Profile",tokens:"API Tokens",instance:"Instance",cookies:"Cookies",extensions:"Extensions",storage:"Storage"};return s.jsxs("section",{className:"admin-view",children:[s.jsx("h1",{children:"Settings"}),s.jsx("div",{className:"view-tabs",children:a.map(u=>s.jsx("button",{className:`view-tab${e===u?" is-active":""}`,onClick:()=>t(u),children:o[u]},u))}),e==="profile"&&s.jsx(km,{currentUser:r,setCurrentUser:l}),e==="tokens"&&s.jsx(jm,{}),e==="instance"&&i&&s.jsx(Sm,{}),e==="cookies"&&i&&s.jsx(_m,{}),e==="extensions"&&i&&s.jsx(Cm,{}),e==="storage"&&i&&s.jsx(Nm,{archiveId:n})]})}function km({currentUser:e,setCurrentUser:t}){const[n,r]=f.useState((e==null?void 0:e.display_name)??""),[l,i]=f.useState(!1),[a,o]=f.useState(null),[u,c]=f.useState(""),[g,m]=f.useState(""),[h,y]=f.useState(""),[x,k]=f.useState(!1),[j,p]=f.useState(null);async function d(w){w.preventDefault(),i(!0),o(null);try{await Lh(n),t(_=>({..._,display_name:n||null})),o({ok:!0,text:"Saved."})}catch(_){o({ok:!1,text:_.message})}finally{i(!1)}}async function v(w){if(w.preventDefault(),g!==h){p({ok:!1,text:"Passwords do not match."});return}k(!0),p(null);try{await Rh(u,g),c(""),m(""),y(""),p({ok:!0,text:"Password changed."})}catch(_){p({ok:!1,text:_.message})}finally{k(!1)}}return s.jsxs("div",{style:{maxWidth:440},children:[s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Display Name"}),s.jsxs("form",{onSubmit:d,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"display-name",children:"Name shown in the UI"}),s.jsx("input",{className:"field-input",id:"display-name",placeholder:(e==null?void 0:e.username)??"",value:n,onChange:w=>r(w.target.value)})]}),a&&s.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:l,children:l?"Saving…":"Save"})]})]}),s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Display Preferences"}),s.jsxs("label",{className:"checkbox-row",children:[s.jsx("input",{type:"checkbox",checked:(e==null?void 0:e.humanize_slugs)??!1,onChange:async w=>{const _=w.target.checked;try{await zh({humanize_slugs:_}),t(P=>({...P,humanize_slugs:_}))}catch{}}}),s.jsx("span",{className:"form-label",style:{margin:0},children:"Humanize tag display"})]}),s.jsx("p",{className:"muted",style:{fontSize:13,margin:"4px 0 0"},children:'When on, tag paths show as "X / Articles" instead of "x/articles".'})]}),s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Change Password"}),s.jsxs("form",{onSubmit:v,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"cur-pw",children:"Current password"}),s.jsx("input",{className:"field-input",id:"cur-pw",type:"password",value:u,onChange:w=>c(w.target.value),required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"new-pw",children:"New password"}),s.jsx("input",{className:"field-input",id:"new-pw",type:"password",value:g,onChange:w=>m(w.target.value),required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",htmlFor:"confirm-pw",children:"Confirm new password"}),s.jsx("input",{className:"field-input",id:"confirm-pw",type:"password",value:h,onChange:w=>y(w.target.value),required:!0})]}),j&&s.jsx("div",{className:`form-msg form-msg--${j.ok?"ok":"err"}`,children:j.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:x,children:x?"Changing…":"Change Password"})]})]})]})}function jm(){const[e,t]=f.useState([]),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,o]=f.useState(""),[u,c]=f.useState(!1),[g,m]=f.useState(null),h=f.useCallback(async()=>{r(!0),i(null);try{t(await Dh())}catch(k){i(k.message)}finally{r(!1)}},[]);f.useEffect(()=>{h()},[h]);async function y(k){if(k.preventDefault(),!!a.trim()){c(!0);try{const j=await $h(a.trim());m(j),o(""),h()}catch(j){i(j.message)}finally{c(!1)}}}async function x(k){try{await Ih(k),t(j=>j.filter(p=>p.token_uid!==k))}catch(j){i(j.message)}}return s.jsx("div",{style:{maxWidth:600},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"API Tokens"}),g&&s.jsxs("div",{className:"token-banner",children:[s.jsx("strong",{children:"Token created."})," Copy it now — it won't be shown again.",s.jsx("code",{children:g.raw_token}),s.jsx("button",{className:"token-dismiss",onClick:()=>m(null),children:"Dismiss"})]}),s.jsxs("form",{className:"token-create-row",onSubmit:y,children:[s.jsx("input",{className:"field-input field-input--flex",placeholder:"Token name",value:a,onChange:k=>o(k.target.value),required:!0}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:u,children:u?"Creating…":"Create token"})]}),l&&s.jsx("div",{className:"form-msg form-msg--err",children:l}),n?s.jsx("div",{className:"muted",children:"Loading…"}):s.jsxs("div",{children:[e.length===0&&s.jsx("div",{className:"muted",children:"No tokens yet."}),e.map(k=>s.jsxs("div",{className:"token-row",children:[s.jsxs("div",{className:"token-row-info",children:[s.jsx("strong",{children:k.name}),s.jsxs("div",{className:"muted",children:["Created ",k.created_at.slice(0,10),k.last_used_at&&` · Last used ${k.last_used_at.slice(0,10)}`]})]}),s.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"4px 10px"},onClick:()=>x(k.token_uid),children:"Revoke"})]},k.token_uid))]})]})})}function Sm(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,o]=f.useState(!1),[u,c]=f.useState(null);f.useEffect(()=>{(async()=>{try{t(await Ba())}catch(m){i(m.message)}finally{r(!1)}})()},[]);async function g(m){m.preventDefault(),o(!0),c(null);try{await _l(e),c({ok:!0,text:"Saved."})}catch(h){c({ok:!1,text:h.message})}finally{o(!1)}}return n?s.jsx("div",{className:"muted",children:"Loading…"}):l?s.jsx("div",{className:"form-msg form-msg--err",children:l}):e?s.jsx("div",{style:{maxWidth:440},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Instance Settings"}),s.jsxs("form",{onSubmit:g,children:[[["public_index_enabled","Public index (unauthenticated browsing)"],["public_entry_content_enabled","Public entry content"],["open_registration_enabled","Open registration"]].map(([m,h])=>s.jsxs("label",{className:"checkbox-row",children:[s.jsx("input",{type:"checkbox",checked:!!e[m],onChange:y=>t(x=>({...x,[m]:y.target.checked}))}),h]},m)),s.jsxs("div",{className:"form-field",style:{marginTop:4},children:[s.jsx("label",{className:"form-label",children:"Default entry visibility"}),s.jsxs("select",{className:"field-input",value:e.default_entry_visibility,onChange:m=>t(h=>({...h,default_entry_visibility:Number(m.target.value)})),children:[s.jsx("option",{value:0,children:"Private"}),s.jsx("option",{value:2,children:"Unlisted"}),s.jsx("option",{value:3,children:"Public"})]})]}),u&&s.jsx("div",{className:`form-msg form-msg--${u.ok?"ok":"err"}`,children:u.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:a,children:a?"Saving…":"Save Settings"})]})]})}):null}function qs(e){if(e===0)return"0 B";const t=["B","KB","MB","GB","TB"],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/Math.pow(1024,n)).toFixed(n===0?0:1)} ${t[n]}`}function Nm({archiveId:e}){const[t,n]=f.useState("idle"),[r,l]=f.useState(null),[i,a]=f.useState(null),[o,u]=f.useState(null);function c(){n("idle"),l(null),a(null),u(null)}async function g(){n("scanning"),u(null),l(null);try{const y=await Xh(e);l(y),n("scanned")}catch(y){u(y.message),n("error")}}async function m(){n("deleting"),u(null);try{const y=await Jh(e);a(y),n("done")}catch(y){u(y.message),n("error")}}const h=r&&r.deletable_files===0&&r.orphaned_blob_rows===0;return s.jsx("div",{style:{maxWidth:440},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Orphan Cleanup"}),s.jsxs("p",{className:"muted",style:{marginBottom:16},children:["Scan for blob files and database records that are no longer referenced by any archive entry and safely delete them."," ",s.jsx("strong",{children:"Cleanup is blocked while captures are running."})]}),!e&&s.jsx("div",{className:"muted",children:"No archive selected."}),e&&t==="idle"&&s.jsx("button",{className:"btn-ghost",onClick:g,children:"Scan for orphaned blobs"}),t==="scanning"&&s.jsx("div",{className:"muted",children:"Scanning…"}),t==="scanned"&&r&&h&&s.jsxs(s.Fragment,{children:[s.jsx("div",{className:"form-msg form-msg--ok",children:"Archive is clean — nothing to remove."}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Done"})]}),t==="scanned"&&r&&!h&&s.jsxs("div",{children:[s.jsxs("div",{style:{marginBottom:14,lineHeight:1.6},children:["Found ",s.jsx("strong",{children:r.deletable_files})," unreferenced file",r.deletable_files!==1?"s":""," ","and ",s.jsx("strong",{children:r.orphaned_blob_rows})," orphaned DB record",r.orphaned_blob_rows!==1?"s":""," ","— ",s.jsx("strong",{children:qs(r.total_bytes)})," recoverable."]}),s.jsxs("div",{style:{display:"flex",gap:8},children:[s.jsxs("button",{className:"btn-danger",onClick:m,children:["Delete (",qs(r.total_bytes),")"]}),s.jsx("button",{className:"btn-ghost",onClick:c,children:"Cancel"})]})]}),t==="deleting"&&s.jsx("div",{className:"muted",children:"Deleting…"}),t==="done"&&i&&s.jsxs("div",{children:[s.jsxs("div",{className:"form-msg form-msg--ok",children:["Freed ",s.jsx("strong",{children:qs(i.freed_bytes)})," ","— removed ",i.deleted_files," file",i.deleted_files!==1?"s":""," ","and ",i.deleted_blob_rows," DB record",i.deleted_blob_rows!==1?"s":"","."]}),i.errors&&i.errors.length>0&&s.jsxs("div",{className:"form-msg form-msg--err",style:{marginTop:6},children:[i.errors.length," file",i.errors.length!==1?"s":""," could not be deleted."]}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Scan again"})]}),t==="error"&&s.jsxs("div",{children:[s.jsx("div",{className:"form-msg form-msg--err",children:o}),s.jsx("button",{className:"btn-ghost",style:{marginTop:10},onClick:c,children:"Try again"})]})]})})}function _m(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(null),[a,o]=f.useState("global"),[u,c]=f.useState(""),[g,m]=f.useState("{}"),[h,y]=f.useState(null),[x,k]=f.useState(!1),[j,p]=f.useState({});f.useEffect(()=>{d()},[]);async function d(){r(!0),i(null);try{t(await Yh())}catch(S){i(S.message)}finally{r(!1)}}async function v(S){S.preventDefault();try{JSON.parse(g)}catch{y({ok:!1,text:'cookies must be valid JSON, e.g. {"session": "abc"}'});return}k(!0),y(null);try{await Gh(a==="global"?null:u.trim(),a,g),c(""),m("{}"),o("global"),y({ok:!0,text:"Rule added."}),await d()}catch($){y({ok:!1,text:$.message})}finally{k(!1)}}async function w(S){try{await em(S),await d()}catch($){i($.message)}}function _(S){p($=>({...$,[S.rule_uid]:{cookiesInput:S.cookies_json,saving:!1,msg:null}}))}function P(S){p($=>{const L={...$};return delete L[S],L})}async function C(S){const $=j[S.rule_uid];try{JSON.parse($.cookiesInput)}catch{p(L=>({...L,[S.rule_uid]:{...L[S.rule_uid],msg:{ok:!1,text:"Invalid JSON"}}}));return}p(L=>({...L,[S.rule_uid]:{...L[S.rule_uid],saving:!0,msg:null}}));try{await Zh(S.rule_uid,{cookies_json:$.cookiesInput}),P(S.rule_uid),await d()}catch(L){p(W=>({...W,[S.rule_uid]:{...W[S.rule_uid],saving:!1,msg:{ok:!1,text:L.message}}}))}}return n?s.jsx("div",{className:"muted",children:"Loading…"}):l?s.jsx("div",{className:"form-msg form-msg--err",children:l}):s.jsx("div",{style:{maxWidth:560},children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Cookie Rules"}),s.jsx("p",{className:"muted",style:{marginBottom:12},children:"Cookies are injected into every capture network request (yt-dlp, HTTP downloads, web-page snapshots). Global rules apply to all URLs; wildcard and regex rules apply only to matching URLs."}),e&&e.length>0?s.jsxs("table",{className:"data-table",style:{width:"100%",marginBottom:16},children:[s.jsx("thead",{children:s.jsxs("tr",{children:[s.jsx("th",{children:"Pattern"}),s.jsx("th",{children:"Cookies"}),s.jsx("th",{style:{width:100},children:"Actions"})]})}),s.jsx("tbody",{children:e.map(S=>{const $=j[S.rule_uid],L=S.url_pattern?s.jsxs(s.Fragment,{children:[s.jsxs("span",{className:"muted",children:[S.pattern_kind,":"]})," ",s.jsx("code",{children:S.url_pattern})]}):s.jsx("span",{className:"muted",children:"global (all URLs)"});return s.jsxs("tr",{children:[s.jsx("td",{children:L}),s.jsx("td",{children:$?s.jsxs(s.Fragment,{children:[s.jsx("textarea",{className:"field-input",style:{fontFamily:"monospace",fontSize:12,width:"100%",minHeight:60},value:$.cookiesInput,onChange:W=>p(A=>({...A,[S.rule_uid]:{...A[S.rule_uid],cookiesInput:W.target.value}}))}),$.msg&&s.jsx("div",{className:`form-msg form-msg--${$.msg.ok?"ok":"err"}`,children:$.msg.text}),s.jsxs("div",{style:{display:"flex",gap:8,marginTop:4},children:[s.jsx("button",{className:"btn-primary",style:{fontSize:12,padding:"2px 8px"},disabled:$.saving,onClick:()=>C(S),children:$.saving?"Saving…":"Save"}),s.jsx("button",{className:"btn-secondary",style:{fontSize:12,padding:"2px 8px"},onClick:()=>P(S.rule_uid),children:"Cancel"})]})]}):s.jsx("code",{style:{fontSize:12,wordBreak:"break-all"},children:S.cookies_json})}),s.jsx("td",{children:!$&&s.jsxs("div",{style:{display:"flex",gap:6},children:[s.jsx("button",{className:"btn-secondary",style:{fontSize:12,padding:"2px 8px"},onClick:()=>_(S),children:"Edit"}),s.jsx("button",{className:"btn-danger",style:{fontSize:12,padding:"2px 8px"},onClick:()=>w(S.rule_uid),children:"Del"})]})})]},S.rule_uid)})})]}):s.jsx("p",{className:"muted",style:{marginBottom:16},children:"No cookie rules defined."}),s.jsx("h3",{style:{marginBottom:8},children:"Add Rule"}),s.jsxs("form",{onSubmit:v,children:[s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:"Pattern type"}),s.jsxs("select",{className:"field-input",value:a,onChange:S=>o(S.target.value),children:[s.jsx("option",{value:"global",children:"Global (all URLs)"}),s.jsx("option",{value:"wildcard",children:"Wildcard (e.g. *.youtube.com)"}),s.jsx("option",{value:"regex",children:"Regex (matched against full URL)"})]})]}),a!=="global"&&s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:a==="wildcard"?"URL/hostname pattern":"Regex pattern"}),s.jsx("input",{className:"field-input",type:"text",value:u,onChange:S=>c(S.target.value),placeholder:a==="wildcard"?"*.youtube.com or https://example.com/*":".*\\.youtube\\.com.*",required:!0})]}),s.jsxs("div",{className:"form-field",children:[s.jsx("label",{className:"form-label",children:"Cookies (JSON object)"}),s.jsx("textarea",{className:"field-input",style:{fontFamily:"monospace",fontSize:13,minHeight:70},value:g,onChange:S=>m(S.target.value),placeholder:'{"SESSION": "abc123", "token": "xyz"}',required:!0})]}),h&&s.jsx("div",{className:`form-msg form-msg--${h.ok?"ok":"err"}`,children:h.text}),s.jsx("button",{className:"btn-primary",type:"submit",disabled:x,children:x?"Adding…":"Add Rule"})]})]})})}function Cm(){const[e,t]=f.useState(null),[n,r]=f.useState(!0),[l,i]=f.useState(!1),[a,o]=f.useState(null);f.useEffect(()=>{(async()=>{try{t(await Ba())}catch(j){o({ok:!1,text:j.message})}finally{r(!1)}})()},[]);async function u(j){i(!0),o(null);try{await _l({ublock_enabled:j}),t(p=>({...p,ublock_enabled:j})),o({ok:!0,text:"Saved."})}catch(p){o({ok:!1,text:p.message})}finally{i(!1)}}async function c(j){i(!0),o(null);try{await _l({cookie_ext_enabled:j}),t(p=>({...p,cookie_ext_enabled:j})),o({ok:!0,text:"Saved."})}catch(p){o({ok:!1,text:p.message})}finally{i(!1)}}async function g(j){i(!0),o(null);try{await _l({modal_closer_enabled:j}),t(p=>({...p,modal_closer_enabled:j})),o({ok:!0,text:"Saved."})}catch(p){o({ok:!1,text:p.message})}finally{i(!1)}}if(n)return s.jsx("div",{className:"muted",children:"Loading\\u2026"});const m=(e==null?void 0:e.ublock_ext_available)??!1,h=(e==null?void 0:e.ublock_enabled)??!0,y=(e==null?void 0:e.cookie_ext_available)??!1,x=(e==null?void 0:e.cookie_ext_enabled)??!0,k=(e==null?void 0:e.modal_closer_enabled)??!0;return s.jsx("div",{children:s.jsxs("div",{className:"form-section",children:[s.jsx("h2",{children:"Extensions"}),s.jsx("p",{className:"form-hint",style:{marginBottom:20},children:"Extensions run inside the browser during WebPage captures and can block ads, accept cookie banners, and more. Changes take effect on the next capture."}),s.jsxs("div",{className:"ext-grid",children:[s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"uBlock Origin Lite"}),s.jsx("span",{className:"ext-card-desc",children:"Blocks ads, trackers, and other page clutter during archiving via Chrome’s declarativeNetRequest API (Manifest V3)."}),!m&&s.jsxs("span",{className:"ext-card-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_UBLOCK_EXT"})," to the unpacked extension directory to enable."]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":h,className:`ext-toggle${h?" ext-toggle--on":""}`,onClick:()=>u(!h),disabled:l,"aria-label":"Toggle uBlock Origin Lite",children:s.jsx("span",{className:"ext-toggle-knob"})})]})}),s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"I Still Don’t Care About Cookies"}),s.jsx("span",{className:"ext-card-desc",children:"Dismiss cookie consent banners during archiving."}),!y&&s.jsxs("span",{className:"ext-card-hint",children:["Not configured — set ",s.jsx("code",{children:"ARCHIVR_COOKIE_EXT"})," to the unpacked extension directory to enable."]})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":x,className:`ext-toggle${x?" ext-toggle--on":""}`,onClick:()=>c(!x),disabled:l,"aria-label":"Toggle I Still Don't Care About Cookies",children:s.jsx("span",{className:"ext-toggle-knob"})})]})}),s.jsx("div",{className:"ext-card",children:s.jsxs("div",{className:"ext-card-header",children:[s.jsxs("div",{className:"ext-card-info",children:[s.jsx("span",{className:"ext-card-name",children:"Modal & Dialog Closer"}),s.jsx("span",{className:"ext-card-desc",children:"Auto-dismiss cookie banners, consent overlays, and other modal dialogs before a WebPage capture is taken. Implemented as an injected browser script; no external extension required."})]}),s.jsx("button",{type:"button",role:"switch","aria-checked":k,className:`ext-toggle${k?" ext-toggle--on":""}`,onClick:()=>g(!k),disabled:l,"aria-label":"Toggle Modal and Dialog Closer",children:s.jsx("span",{className:"ext-toggle-knob"})})]})})]}),a&&s.jsx("div",{className:`form-msg form-msg--${a.ok?"ok":"err"}`,children:a.text})]})})}const iu={0:"Private",1:"Public",2:"Users only",3:"Public"},Em=()=>s.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:s.jsx("path",{d:"M7 17 17 7M9 7h8v8"})});function Tm({archiveId:e,selectedEntry:t,selectedUids:n,selectedEntries:r,detail:l,onTagFilterSet:i,tagNodes:a,onTagsRefresh:o,onEntryTitleChange:u,onEntryDeleted:c,onBulkDeleted:g,humanizeTags:m,onDetailRefresh:h,onOpenPreview:y,onPlay:x}){var bt;const[k,j]=f.useState([]),[p,d]=f.useState(""),[v,w]=f.useState([]),[_,P]=f.useState(""),C=f.useRef(0),S=f.useRef(!1),[$,L]=f.useState(!1),[W,A]=f.useState(""),[Q,ee]=f.useState("idle"),[le,de]=f.useState(""),ne=f.useRef(null),[D,b]=f.useState(!1);f.useEffect(()=>{b(!1)},[(bt=l==null?void 0:l.summary)==null?void 0:bt.entry_uid]);const M=(n==null?void 0:n.size)>=2,[Y,U]=f.useState(""),[fe,ae]=f.useState("idle"),[N,F]=f.useState(""),[H,X]=f.useState([]),[K,pe]=f.useState(""),[G,q]=f.useState("idle"),[T,B]=f.useState(""),[Ce,ke]=f.useState("idle");f.useEffect(()=>{const I=++C.current;if(ne.current&&(clearInterval(ne.current),ne.current=null),ee("idle"),de(""),!t||!e){j([]),w([]);return}L(!1),A(""),S.current=!1,j([]),Promise.all([dl(e,t.entry_uid),Qh(e,t.entry_uid)]).then(([se,Qe])=>{I===C.current&&(j(se),w(Qe))}).catch(()=>{})},[t,e]),f.useEffect(()=>()=>{clearInterval(ne.current)},[]),f.useEffect(()=>{if(!M||!e){X([]);return}Cd(e).then(X).catch(()=>X([]))},[M,e]),f.useEffect(()=>{U(""),ae("idle"),F(""),pe(""),q("idle"),B(""),ke("idle")},[n]);async function Xn(){const I=n.size;if(!window.confirm(`Delete ${I} entr${I===1?"y":"ies"}? This cannot be undone.`))return;ke("running");const se=new Set;for(const Qe of n)try{await tu(e,Qe),se.add(Qe)}catch{}ke("idle"),g==null||g(se)}async function Ze(){const I=Y.trim();if(I){ae("running"),F("");try{for(const se of n)await eu(e,se,I);U(""),ae("done"),o==null||o(),setTimeout(()=>ae("idle"),1800)}catch(se){F(se.message),ae("error")}}}async function ms(){if(!K)return;q("running"),B("");const I=[];for(const se of n)try{await Ed(e,K,se)}catch{I.push(se)}I.length>0?(B(`Failed for ${I.length} entr${I.length===1?"y":"ies"}.`),q("error")):(q("done"),setTimeout(()=>q("idle"),1800))}async function gs(){const I=W.trim()||null;try{await yh(e,t.entry_uid,I),u==null||u(t.entry_uid,I)}catch{}finally{L(!1)}}async function Vr(){const I=p.trim();if(!(!I||!t))try{await eu(e,t.entry_uid,I),d(""),P("");const se=await dl(e,t.entry_uid);j(se),o()}catch(se){P(se.message)}}async function vs(I){try{await xh(e,t.entry_uid,I);const se=await dl(e,t.entry_uid);j(se),o()}catch{}}async function ys(){if(!(!t||!e)&&window.confirm("Delete this entry? This cannot be undone."))try{await tu(e,t.entry_uid),c==null||c(t.entry_uid)}catch{}}async function xs(){if(!t||!e||Q==="running")return;const I=C.current,se=t.entry_uid;ee("running"),de("");try{const{job_uid:Qe}=await qh(e,se);if(C.current!==I)return;ne.current=setInterval(async()=>{try{const qt=await _d(e,Qe);if(qt.status==="completed"){if(clearInterval(ne.current),ne.current=null,C.current!==I)return;ee("done");const qn=await dl(e,se);if(C.current!==I)return;j(qn),h==null||h()}else if(qt.status==="failed"){if(clearInterval(ne.current),ne.current=null,C.current!==I)return;ee("error"),de(qt.error_text||"Re-archive failed.")}}catch{if(clearInterval(ne.current),ne.current=null,C.current!==I)return;ee("error"),de("Network error while polling.")}},500)}catch(Qe){if(C.current!==I)return;ee("error"),de(Qe.message||"Failed to start re-archive.")}}const ws=l?[["Added",bd(l.summary.archived_at)],["Source",l.summary.source_kind],["Type",l.summary.entity_kind],["Visibility",iu[l.summary.visibility]??l.summary.visibility],["Root",l.structured_root_relpath]]:[],ks=new Set(["mp3","ogg","m4a","opus","wav","flac","aac"]),js=new Set(["mp4","webm","mov","mkv","avi","m4v","ogv","pdf","html","htm","jpg","jpeg","png","gif","webp","avif","svg","bmp"]),hn=l?l.artifacts.findIndex(I=>I.artifact_role==="primary_media"):-1,mn=hn>=0?l.artifacts[hn]:null,Qr=mn?mn.relpath.split(".").pop().toLowerCase():"",Kr=mn&&ks.has(Qr),gn=hn>=0&&t?`/api/archives/${e}/entries/${t.entry_uid}/artifacts/${hn}`:null,Jn=l&&!Kr&&(l.summary.entity_kind==="tweet"||l.summary.entity_kind==="tweet_thread"||mn&&js.has(Qr));return s.jsxs("aside",{className:"context-rail",children:[s.jsx("div",{className:"rail-eyebrow",children:"Context"}),M?s.jsxs("div",{className:"bulk-panel",children:[s.jsxs("p",{className:"bulk-count",children:[s.jsx("span",{className:"bulk-count-num",children:n.size})," entries selected"]}),s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Assign tag"}),N&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:N}),s.jsxs("div",{className:"tag-input-wrap",children:[s.jsx("span",{className:"hash",children:"/"}),s.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:Y,onChange:I=>U(I.target.value),onKeyDown:I=>{I.key==="Enter"&&Ze()}}),s.jsx("button",{className:"tag-add-btn",onClick:Ze,disabled:fe==="running"||!Y.trim(),children:fe==="running"?"…":fe==="done"?"✓":"Add"})]})]}),H.length>0&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Add to collection"}),s.jsxs("div",{className:"bulk-coll-row",children:[s.jsxs("select",{className:"bulk-coll-select",value:K,onChange:I=>pe(I.target.value),children:[s.jsx("option",{value:"",children:"Pick a collection…"}),H.map(I=>s.jsx("option",{value:I.collection_uid,children:I.name},I.collection_uid))]}),s.jsx("button",{className:"tag-add-btn",onClick:ms,disabled:!K||G==="running",children:G==="running"?"…":G==="done"?"✓":G==="error"?"!":"Add"})]}),T&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"6px 0 0"},children:T})]}),s.jsx("div",{className:"rail-delete-zone",children:s.jsx("button",{className:"rail-delete-btn",onClick:Xn,disabled:Ce==="running",children:Ce==="running"?"Deleting…":`Delete ${n.size} entr${n.size===1?"y":"ies"}`})})]}):t?l?s.jsxs(s.Fragment,{children:[$?s.jsx("input",{className:"rail-title-input",autoFocus:!0,value:W,onChange:I=>A(I.target.value),onKeyDown:I=>{I.key==="Enter"&&I.currentTarget.blur(),I.key==="Escape"&&(S.current=!0,I.currentTarget.blur())},onBlur:()=>{S.current?L(!1):gs(),S.current=!1}}):s.jsxs("h2",{className:"rail-title rail-title--editable",title:"Click to rename",onClick:()=>{A(l.summary.title??""),L(!0)},children:[rn(l.summary.title)||rn(l.summary.entry_uid),s.jsx("svg",{className:"edit-icon",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:s.jsx("path",{d:"M11.5 2.5a1.5 1.5 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"})})]}),l.summary.original_url&&s.jsxs("a",{className:"url-tile",href:l.summary.original_url,target:"_blank",rel:"noopener noreferrer",children:[s.jsx("span",{className:"ico",dangerouslySetInnerHTML:{__html:Ua(l.summary.source_kind)}}),s.jsx("span",{className:"u-text",children:l.summary.original_url}),s.jsx("span",{className:"ext",children:s.jsx(Em,{})})]}),Kr&&x&&s.jsx("button",{className:"rail-preview-btn",onClick:()=>x(gn,t),children:"▶ Play"}),Jn&&y&&s.jsx("button",{className:"rail-preview-btn",onClick:y,children:"Preview"}),s.jsx("div",{className:"meta-list",children:ws.filter(([,I])=>I!=null&&I!=="").map(([I,se])=>s.jsxs("div",{className:"meta-item",children:[s.jsx("span",{className:"meta-k",children:I}),s.jsx("span",{className:`meta-v${I==="Root"?" mono":""}`,children:rn(se)})]},I))}),l.artifacts.length>0&&(()=>{const I=l.artifacts.map((R,V)=>({...R,_idx:V})),se=I.filter(R=>R.artifact_role==="font"),Qe=I.filter(R=>R.artifact_role!=="font"),qt=se.reduce((R,V)=>R+(V.byte_size||0),0),qn=l.summary.entry_uid,E=R=>s.jsx("li",{children:s.jsxs("a",{href:`/api/archives/${e}/entries/${qn}/artifacts/${R._idx}`,target:"_blank",rel:"noopener noreferrer",className:"artifact-link",children:[s.jsx("span",{className:"artifact-name",children:R.artifact_role==="font"?R.relpath.split("/").pop():R.artifact_role.replace(/_/g," ")}),s.jsx("span",{className:"artifact-size",children:R.byte_size!=null?ql(R.byte_size):"—"})]})},R._idx);return s.jsxs("div",{className:"rail-section",children:[s.jsxs("div",{className:"rail-section-heading",children:["Artifacts ",s.jsx("span",{className:"num",children:l.artifacts.length})]}),s.jsxs("ul",{className:"artifact-list",children:[Qe.map(E),se.length>0&&s.jsxs("li",{className:"artifact-group",children:[s.jsxs("button",{type:"button",className:"artifact-group-header artifact-link","aria-expanded":D,onClick:()=>b(R=>!R),children:[s.jsxs("span",{className:"artifact-name",children:[s.jsx("span",{"aria-hidden":"true",className:`artifact-group-chevron${D?" open":""}`,children:"›"}),` fonts (${se.length})`]}),s.jsx("span",{className:"artifact-size",children:ql(qt)})]}),D&&s.jsx("ul",{className:"artifact-list artifact-group-body",children:se.map(E)})]})]})]})})()]}):s.jsx("p",{className:"tags-empty",children:"Loading\\u2026"}):s.jsx("p",{className:"tags-empty",children:"Select an entry."}),t&&!M&&s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Tags"}),k.length===0?s.jsx("p",{className:"tags-empty",children:"No tags yet."}):s.jsx("div",{className:"tags-wrap",children:k.map(I=>s.jsxs("span",{className:"tag-pill",title:I.full_path,children:[m?Pd(I.full_path):I.full_path,s.jsx("button",{className:"remove",title:`Remove tag ${I.full_path}`,onClick:()=>vs(I.tag_uid),children:"×"})]},I.tag_uid))}),_&&s.jsx("p",{className:"form-msg form-msg--err",style:{margin:"0 0 8px"},children:_}),s.jsxs("div",{className:"tag-input-wrap",children:[s.jsx("span",{className:"hash",children:"/"}),s.jsx("input",{className:"tag-input",type:"text",placeholder:"science/cs",autoComplete:"off",value:p,onChange:I=>d(I.target.value),onKeyDown:I=>{I.key==="Enter"&&Vr()}}),s.jsx("button",{className:"tag-add-btn",onClick:Vr,children:"Add"})]})]}),v.length>0&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Collections"}),v.map(I=>s.jsxs("div",{className:"coll-row",children:[s.jsx("span",{className:"coll-name",children:I.collection_uid}),s.jsx("span",{className:"vis-badge",children:iu[I.visibility_bits]??`bits:${I.visibility_bits}`})]},I.collection_uid))]}),l&&(l.summary.entity_kind==="tweet"||l.summary.entity_kind==="tweet_thread")&&s.jsxs("div",{className:"rail-section",children:[s.jsx("div",{className:"rail-section-heading",children:"Actions"}),s.jsx("button",{className:"rail-rearchive-btn",onClick:xs,disabled:Q==="running",children:Q==="running"?"Re-archiving…":"Re-archive"}),Q==="done"&&s.jsx("p",{className:"form-msg form-msg--ok",style:{marginTop:"6px"},children:"Re-archived successfully."}),Q==="error"&&s.jsx("p",{className:"form-msg form-msg--err",style:{marginTop:"6px"},children:le})]}),s.jsx("div",{className:"rail-delete-zone",children:s.jsx("button",{className:"rail-delete-btn",onClick:ys,children:"Delete entry"})})]})]})}function bm({src:e}){const t=f.useRef(null);return f.useEffect(()=>{t.current&&t.current.load()},[e]),s.jsx("div",{className:"preview-video-wrap",style:{background:"#111",display:"flex",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",minHeight:"240px"},children:e?s.jsxs("video",{ref:t,controls:!0,autoPlay:!1,style:{width:"100%",maxHeight:"100%",display:"block"},children:[s.jsx("source",{src:e}),"Your browser does not support the video element."]}):s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No video available"})})}function au({src:e,type:t,title:n,originalUrl:r}){const l=r||e,i=n||null;return s.jsxs("div",{className:"preview-iframe-wrap",style:{flex:1,minHeight:0,display:"flex",flexDirection:"column"},children:[s.jsxs("div",{className:"preview-iframe-toolbar",style:{display:"flex",flexDirection:"column",gap:"2px",padding:"6px 12px",borderBottom:"1px solid var(--line-soft)",background:"var(--paper-2)",flexShrink:0},children:[i&&s.jsx("span",{style:{fontSize:"0.85rem",fontWeight:600,color:"var(--ink)",fontFamily:"var(--sans)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:i}),s.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[s.jsx("span",{style:{flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontSize:"0.78rem",color:"var(--muted)",fontFamily:"var(--sans)"},children:l}),s.jsx("a",{href:e,target:"_blank",rel:"noreferrer noopener",style:{fontSize:"0.78rem",color:"var(--accent)",textDecoration:"none",whiteSpace:"nowrap",fontFamily:"var(--sans)",flexShrink:0},children:t==="pdf"?"Open PDF ↗":"Open in new tab ↗"})]})]}),s.jsx("iframe",{src:e,sandbox:"allow-same-origin allow-popups",allow:"autoplay 'none'",referrerPolicy:"no-referrer",style:{flex:1,border:"none",width:"100%",minHeight:0},title:i||(t==="pdf"?"PDF preview":"Page preview")})]})}function Pm({src:e,alt:t}){return s.jsx("div",{className:"preview-image-wrap",style:{display:"flex",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",background:"var(--paper-2)",overflow:"hidden"},children:s.jsx("a",{href:e,target:"_blank",rel:"noreferrer noopener",style:{display:"contents"},children:s.jsx("img",{src:e,alt:t||"",style:{objectFit:"contain",maxHeight:"100%",maxWidth:"100%",display:"block",cursor:"pointer"}})})})}function Dd(e){return e?new Intl.DateTimeFormat("en-US",{month:"short",day:"numeric",year:"numeric"}).format(new Date(e*1e3)):""}function ou(e){return!e||!e.includes("&")?e:e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'")}const J={card:{background:"var(--paper)",border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",maxWidth:"560px",margin:"0 auto",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},threadOuter:{border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",maxWidth:"560px",margin:"0 auto",background:"var(--paper)",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},tweetRow:{display:"flex",gap:"10px",padding:"10px 12px"},tweetRowThread:{display:"flex",gap:"10px",padding:"10px 12px 0"},leftCol:{display:"flex",flexDirection:"column",alignItems:"center",flexShrink:0,width:"36px"},rightCol:{flex:1,minWidth:0,paddingBottom:"8px"},avatar:{width:"36px",height:"36px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},avatarPh:{width:"36px",height:"36px",borderRadius:"50%",background:"var(--line)",flexShrink:0},threadLine:{flex:1,width:"2px",background:"var(--line-soft, var(--line))",margin:"4px 0",minHeight:"12px",borderRadius:"1px"},authorRow:{display:"flex",alignItems:"baseline",gap:"4px",flexWrap:"wrap",marginBottom:"4px",lineHeight:"1.3"},authorName:{fontWeight:"700",fontSize:"14px",color:"var(--ink)"},authorHandle:{fontSize:"13px",color:"var(--muted)"},datePart:{fontSize:"13px",color:"var(--muted)"},tweetText:{fontSize:"14px",lineHeight:"1.5",color:"var(--ink)",whiteSpace:"pre-line",marginBottom:"8px",wordBreak:"break-word"},stats:{display:"flex",gap:"12px",fontSize:"13px",color:"var(--muted)"},link:{color:"var(--accent)",textDecoration:"none"},loading:{padding:"24px 16px",textAlign:"center",color:"var(--muted)",fontSize:"14px"},error:{padding:"24px 16px",textAlign:"center",color:"var(--alert)",fontSize:"14px"},mediaGrid:{marginBottom:"8px",borderRadius:"10px",overflow:"hidden",border:"1px solid var(--line)"},mediaImg:{display:"block",width:"100%",objectFit:"cover",maxHeight:"260px"},mediaVideo:{display:"block",width:"100%",maxHeight:"260px",background:"#000"},article:{maxWidth:"560px",margin:"0 auto",border:"1px solid var(--line)",borderRadius:"12px",overflow:"hidden",background:"var(--paper)",fontFamily:'var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)'},aCover:{width:"100%",display:"block",objectFit:"cover",maxHeight:"280px"},aMeta:{padding:"10px 14px 0"},aTweetTitle:{fontSize:"20px",fontWeight:"800",letterSpacing:"-0.3px",color:"var(--ink)",lineHeight:"1.3",marginBottom:"8px"},aAuthorRow:{display:"flex",alignItems:"center",gap:"8px",marginBottom:"8px"},aAvatar:{width:"36px",height:"36px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},aAvatarPh:{width:"36px",height:"36px",borderRadius:"50%",background:"var(--line)",flexShrink:0},aAuthorName:{fontSize:"14px",fontWeight:"700",color:"var(--ink)",lineHeight:"1.3"},aAuthorSub:{fontSize:"13px",color:"var(--muted)",lineHeight:"1.3"},aDivider:{border:"none",borderTop:"1px solid var(--line)",margin:"0"},aBody:{padding:"4px 14px 16px"},bH1:{fontSize:"22px",fontWeight:"800",letterSpacing:"-0.4px",color:"var(--ink)",lineHeight:"1.25",margin:"16px 0 6px"},bH2:{fontSize:"18px",fontWeight:"700",letterSpacing:"-0.2px",color:"var(--ink)",lineHeight:"1.3",margin:"14px 0 4px"},bP:{fontSize:"15px",color:"var(--ink)",lineHeight:"1.65",marginBottom:"12px",marginTop:"0"},bSpacer:{height:"4px",display:"block"},bQuote:{borderLeft:"3px solid var(--line)",padding:"2px 12px",margin:"12px 0",color:"var(--muted)",fontSize:"15px",lineHeight:"1.6"},bHr:{border:"none",borderTop:"1px solid var(--line)",margin:"14px 0"},bImg:{width:"100%",display:"block",borderRadius:"8px",margin:"12px 0"},bUl:{margin:"8px 0 12px",paddingLeft:"24px"},bOl:{margin:"8px 0 12px",paddingLeft:"24px"},bLi:{fontSize:"15px",color:"var(--ink)",lineHeight:"1.6",marginBottom:"4px"},bTweet:{display:"flex",alignItems:"center",gap:"10px",border:"1px solid var(--line)",borderRadius:"10px",padding:"10px 14px",margin:"12px 0",color:"var(--muted)",fontSize:"14px",textDecoration:"none"},bMdPre:{borderRadius:"8px",margin:"10px 0",overflow:"auto",background:"var(--paper-3, var(--field))",padding:"12px 14px"},bMdCode:{fontFamily:"ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, monospace",fontSize:"12px",lineHeight:"1.6",color:"var(--ink)",background:"transparent",display:"block"},iCode:{fontFamily:"ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace",fontSize:"0.875em",background:"var(--paper-3, var(--field))",padding:"1px 5px",borderRadius:"4px",color:"var(--ink)"},qtBadge:{fontSize:"11px",color:"var(--muted)",display:"inline-flex",alignItems:"center",gap:"2px",marginLeft:"4px",letterSpacing:"0.03em",flexShrink:0},lightboxBackdrop:{position:"fixed",inset:0,zIndex:500,background:"rgba(0,0,0,0.92)",display:"flex",alignItems:"center",justifyContent:"center",padding:"20px"},lightboxContent:{display:"flex",flexDirection:"column",alignItems:"center",gap:"10px",maxWidth:"90vw",maxHeight:"90vh"},lightboxToolbar:{display:"flex",alignItems:"center",gap:"10px",alignSelf:"flex-end"},lightboxImg:{maxWidth:"88vw",maxHeight:"80vh",objectFit:"contain",borderRadius:"6px",display:"block"},lightboxNav:{display:"flex",gap:"12px",alignItems:"center"},lightboxNavBtn:{background:"rgba(255,255,255,0.18)",border:"none",color:"#fff",fontSize:"20px",cursor:"pointer",borderRadius:"50%",width:"36px",height:"36px",display:"flex",alignItems:"center",justifyContent:"center",lineHeight:1,flexShrink:0},lightboxBtn:{background:"rgba(255,255,255,0.18)",border:"none",color:"#fff",fontSize:"16px",cursor:"pointer",borderRadius:"50%",width:"30px",height:"30px",display:"flex",alignItems:"center",justifyContent:"center",lineHeight:1,flexShrink:0},lightboxLink:{color:"rgba(255,255,255,0.7)",textDecoration:"none",fontSize:"14px"},lightboxCounter:{color:"rgba(255,255,255,0.6)",fontSize:"13px"}},je={bg:"#000000",border:"#2f3336",text:"#e7e9ea",dim:"#71767b",accent:"#1d9bf0",codeBg:"#16181c"},Ys={article:{background:je.bg,color:je.text,minHeight:"100%",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'},articleInner:{maxWidth:"598px",margin:"0 auto",paddingBottom:"80px"},aCover:{width:"100%",display:"block",objectFit:"cover",maxHeight:"420px"},aMeta:{padding:"20px 16px 0"},aTweetTitle:{fontSize:"34px",fontWeight:"800",letterSpacing:"-0.5px",color:je.text,lineHeight:"44px",marginBottom:"16px"},aAuthorRow:{display:"flex",alignItems:"center",gap:"12px",marginBottom:"16px"},aAvatar:{width:"40px",height:"40px",borderRadius:"50%",objectFit:"cover",flexShrink:0,display:"block"},aAvatarPh:{width:"40px",height:"40px",borderRadius:"50%",background:je.border,flexShrink:0},aAuthorName:{fontSize:"15px",fontWeight:"700",color:je.text,lineHeight:"1.3"},aAuthorSub:{fontSize:"15px",color:je.dim,lineHeight:"1.3"},aDivider:{border:"none",borderTop:`1px solid ${je.border}`,margin:"0"},aBody:{padding:"4px 16px 0"},bH1:{fontSize:"26px",fontWeight:"800",letterSpacing:"-0.4px",color:je.text,lineHeight:"36px",margin:"24px 0 10px"},bH2:{fontSize:"26px",fontWeight:"800",letterSpacing:"-0.4px",color:je.text,lineHeight:"36px",margin:"24px 0 10px"},bP:{fontSize:"17px",color:je.text,lineHeight:"1.5",marginBottom:"16px",marginTop:"0"},bSpacer:{height:"6px",display:"block"},bQuote:{borderLeft:`3px solid ${je.border}`,padding:"2px 14px",margin:"14px 0",color:je.dim,fontSize:"17px",lineHeight:"1.5"},bHr:{border:"none",borderTop:`1px solid ${je.border}`,margin:"28px 0"},bImg:{width:"100%",display:"block",borderRadius:"12px",margin:"16px 0"},bUl:{margin:"10px 0 16px",paddingLeft:"28px"},bOl:{margin:"10px 0 16px",paddingLeft:"28px"},bLi:{fontSize:"17px",color:je.text,lineHeight:"1.5",marginBottom:"6px"},bTweet:{display:"flex",alignItems:"center",gap:"12px",border:`1px solid ${je.border}`,borderRadius:"12px",padding:"14px 16px",margin:"14px 0",color:je.dim,fontSize:"15px",textDecoration:"none"},bMdPre:{borderRadius:"12px",margin:"12px 0",overflow:"auto",background:"#1e2029"},bMdCode:{fontFamily:"ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, monospace",fontSize:"13px",lineHeight:"1.65",padding:"18px 20px",display:"block",color:je.text,background:"transparent"},iCode:{fontFamily:"ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace",fontSize:"0.875em",background:je.codeBg,padding:"2px 6px",borderRadius:"4px",color:"#e6edf3"},link:{color:je.accent,textDecoration:"none"}};function Lm(e,t,n){const r={};return n&&n.forEach((l,i)=>{l.relpath&&(r[l.relpath]=`/api/archives/${e}/entries/${t}/artifacts/${i}`)}),r}function On(e,t,n){return e&&n[e]?n[e]:t||null}function $d(){return s.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true",style:{flexShrink:0,color:"var(--ink)"},children:s.jsx("path",{d:"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.748l7.73-8.835L1.254 2.25H8.08l4.259 5.63L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"})})}function Ha(e,t,...n){var r;if(e.fromIndex!=null&&e.toIndex!=null)return{s:e.fromIndex,e:e.toIndex};if(((r=e.indices)==null?void 0:r.length)===2)return{s:e.indices[0],e:e.indices[1]};if(t)for(const l of n){if(!l)continue;const i=t.indexOf(l);if(i!==-1)return{s:i,e:i+l.length}}return null}function Id(e,t){const n=e.expanded_url||e.url||e.text||"",r=e.display_url||e.expanded_url||e.url||e.text||"",l=Ha(e,t,e.url,e.text,e.display_url,e.expanded_url);return!l||!n?null:{...l,kind:"url",href:n,display:r}}function Od(e,t){const n=e.screen_name||e.name||e.text||"",r=n?`@${n}`:null,l=Ha(e,t,r);return!l||!n?null:{...l,kind:"mention",screen_name:n}}const uu=/https?:\/\/[^\s<>"'\])]+/g,zm=/[.,;:!?)()]+$/;function Yl(e,t){const n=[];let r=0,l;for(uu.lastIndex=0;(l=uu.exec(e))!==null;){l.index>r&&n.push(e.slice(r,l.index));let i=l[0].replace(zm,"");n.push(s.jsx("a",{href:i,target:"_blank",rel:"noopener noreferrer",style:t,children:i},l.index));const a=l[0].slice(i.length);a&&n.push(a),r=l.index+l[0].length}return r===0?e:(rId(o,e)).filter(Boolean),...(t.user_mentions||[]).map(o=>Od(o,e)).filter(Boolean)];if(r.length===0&&n.length===0)return Yl(ou(e),J.link);const l=new Set([0,e.length]);for(const o of r)o.s>=0&&o.s<=e.length&&l.add(o.s),o.e>=0&&o.e<=e.length&&l.add(o.e);for(const[o,u]of n)o>=0&&o<=e.length&&l.add(o),u>=0&&u<=e.length&&l.add(u);const i=(o,u)=>n.some(([c,g])=>c<=o&&g>=u),a=[...l].sort((o,u)=>o-u);return a.slice(0,-1).map((o,u)=>{const c=a[u+1];if(i(o,c))return null;const g=e.slice(o,c),m=r.filter(x=>x.s<=o&&x.e>=c),h=m.find(x=>x.kind==="url");if(h)return s.jsx("a",{href:h.href,target:"_blank",rel:"noopener noreferrer",style:J.link,children:h.display||g},u);const y=m.find(x=>x.kind==="mention");return y?s.jsx("a",{href:`https://x.com/${y.screen_name}`,target:"_blank",rel:"noopener noreferrer",style:J.link,children:g},u):s.jsx("span",{children:Yl(ou(g),J.link)},u)}).filter(o=>o!==null)}function Dm(e,t,n,r,l=J){if(!e)return null;t=t||[],n=n||[],r=r||[];const i=[];for(const u of t)u.length>0&&i.push({s:u.offset,e:u.offset+u.length,kind:"style",style:u.style});for(const u of n){const c=Id(u,e);c&&i.push(c)}for(const u of r){const c=Od(u,e);c&&i.push(c)}if(i.length===0)return Yl(e,l.link);const a=new Set([0,e.length]);for(const u of i)u.s>=0&&u.s<=e.length&&a.add(u.s),u.e>=0&&u.e<=e.length&&a.add(u.e);const o=[...a].sort((u,c)=>u-c);return o.slice(0,-1).map((u,c)=>{const g=o[c+1],m=i.filter(j=>j.s<=u&&j.e>=g),h=e.slice(u,g);let y;if(h.includes(` -`)){const j=h.split(` -`);y=j.flatMap((p,d)=>dj.kind==="style"&&j.style==="Code")&&(y=s.jsx("code",{style:l.iCode,children:y})),m.some(j=>j.kind==="style"&&j.style==="Bold")&&(y=s.jsx("strong",{children:y})),m.some(j=>j.kind==="style"&&j.style==="Italic")&&(y=s.jsx("em",{children:y})),m.some(j=>j.kind==="style"&&j.style==="Underline")&&(y=s.jsx("u",{children:y})),m.some(j=>j.kind==="style"&&j.style==="Strikethrough")&&(y=s.jsx("s",{children:y}));const x=m.find(j=>j.kind==="url");if(x){const j=/^https?:\/\/t\.co\//i.test(y);y=s.jsx("a",{href:x.href,target:"_blank",rel:"noopener noreferrer",style:l.link,children:j?x.display:y})}const k=m.find(j=>j.kind==="mention");return k&&(y=s.jsx("a",{href:`https://x.com/${k.screen_name}`,target:"_blank",rel:"noopener noreferrer",style:l.link,children:y})),s.jsx("span",{children:typeof y=="string"?Yl(y,l.link):y},c)})}function $m(e,t,n){const r=(n==null?void 0:n.st)||J,l=e.resolved_entities||[];return l.length===0?null:l.map((i,a)=>{var o;switch(i.type){case"divider":return s.jsx("hr",{style:r.bHr},a);case"media":{const u=On(i.local_path,i.url,t);return u?s.jsx("a",{href:u,target:"_blank",rel:"noopener noreferrer",style:{display:"block",cursor:"zoom-in"},onClick:c=>{var g;!c.metaKey&&!c.ctrlKey&&(c.preventDefault(),(g=n==null?void 0:n.onImgClick)==null||g.call(n,u))},children:s.jsx("img",{src:u,style:r.bImg,loading:"lazy",alt:""})},a):null}case"tweet":return i.tweet_id?s.jsxs("a",{href:`https://x.com/i/status/${i.tweet_id}`,target:"_blank",rel:"noopener noreferrer",style:r.bTweet,children:[s.jsx($d,{}),"View post on X"]},a):null;case"link":return i.url?s.jsx("p",{style:r.bP,children:s.jsx("a",{href:i.url,target:"_blank",rel:"noopener noreferrer",style:r.link,children:i.url})},a):null;case"markdown":{const u=i.markdown??((o=i.data)==null?void 0:o.markdown)??"";return s.jsx("pre",{style:r.bMdPre,children:s.jsx("code",{style:r.bMdCode,children:u})},a)}case"emoji":return i.url?s.jsx("img",{src:i.url,alt:"",style:{height:"1.2em",verticalAlign:"middle",margin:"0 1px"}},a):null;default:return null}})}function Gs(e,t,n,r){const l=(r==null?void 0:r.st)||J,i=e.type||"",a=e.text||"",o=e.inline_style_ranges||[],u=e.data||{},c=Dm(a,o,u.urls||[],u.mentions||[],l);switch(i){case"header-one":return s.jsx("h1",{style:l.bH1,children:c},t);case"header-two":{const g=a.match(/(?:x\.com|twitter\.com)\/i\/status\/(\d+)/);return g?s.jsxs("a",{href:`https://x.com/i/status/${g[1]}`,target:"_blank",rel:"noopener noreferrer",style:l.bTweet,children:[s.jsx($d,{}),"View post on X"]},t):s.jsx("h2",{style:l.bH2,children:c},t)}case"unstyled":return a.trim()?s.jsx("p",{style:l.bP,children:c},t):s.jsx("span",{style:l.bSpacer},t);case"blockquote":return s.jsx("blockquote",{style:l.bQuote,children:c},t);case"unordered-list-item":case"ordered-list-item":return s.jsx("li",{style:l.bLi,children:c},t);case"atomic":return s.jsx("span",{children:$m(e,n,r)},t);default:return a?s.jsx("p",{style:l.bP,children:c},t):null}}function Im(e,t,n){const r=(n==null?void 0:n.st)||J,l=[];let i=0;for(;i{r&&(l==null||l(!0))},[r,l]);const o=r?Ys:J,u=e.cover_media||{},c=e.author||t||{},g=e.first_published_at_secs?Dd(e.first_published_at_secs):"",h=[c.screen_name?`@${c.screen_name}`:"",g].filter(Boolean).join(" · "),y=On(u.local_path,u.url,n),x=On(c.avatar_local_path,c.avatar_url,n),k=s.jsxs(s.Fragment,{children:[i&&s.jsx(Fd,{items:[{src:i,alt:""}],startIndex:0,onClose:()=>a(null)}),y&&s.jsx("a",{href:y,target:"_blank",rel:"noopener noreferrer",style:{display:"block",cursor:"zoom-in"},onClick:j=>{!j.metaKey&&!j.ctrlKey&&(j.preventDefault(),a(y))},children:s.jsx("img",{src:y,style:o.aCover,alt:"Article cover"})}),s.jsxs("div",{style:o.aMeta,children:[e.title&&s.jsx("div",{style:o.aTweetTitle,children:e.title}),s.jsxs("div",{style:o.aAuthorRow,children:[x?s.jsx("img",{src:x,style:o.aAvatar,alt:c.name||""}):s.jsx("div",{style:o.aAvatarPh}),s.jsxs("div",{children:[s.jsx("div",{style:o.aAuthorName,children:c.name||c.screen_name||"Unknown"}),h&&s.jsx("div",{style:o.aAuthorSub,children:h})]})]})]}),s.jsx("hr",{style:o.aDivider}),s.jsx("div",{style:o.aBody,children:Im(e.blocks||[],n,{onImgClick:a,st:o})})]});return r?s.jsx("div",{style:Ys.article,children:s.jsx("div",{style:Ys.articleInner,children:k})}):s.jsx("div",{style:J.article,children:k})}function Fm({photos:e,onOpen:t}){const n=e.length;if(n===0)return null;if(n===1)return s.jsx("a",{href:e[0].src,target:"_blank",rel:"noopener noreferrer",style:{display:"block"},onClick:l=>{!l.metaKey&&!l.ctrlKey&&(l.preventDefault(),t(0))},children:s.jsx("img",{src:e[0].src,alt:e[0].alt||"",style:J.mediaImg,loading:"lazy"})});const r=n<=2?"180px":"140px";return s.jsx("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gridTemplateRows:n===2?r:`${r} ${r}`,gap:"2px"},children:e.map((l,i)=>s.jsx("a",{href:l.src,target:"_blank",rel:"noopener noreferrer",style:{display:"block",overflow:"hidden",gridRow:n===3&&i===0?"span 2":void 0},onClick:a=>{!a.metaKey&&!a.ctrlKey&&(a.preventDefault(),t(i))},children:s.jsx("img",{src:l.src,alt:l.alt||"",loading:"lazy",style:{width:"100%",height:"100%",objectFit:"cover",display:"block"}})},i))})}function Fd({items:e,startIndex:t,onClose:n}){const[r,l]=f.useState(t);f.useEffect(()=>{const a=o=>{(o.key==="Escape"||o.key==="ArrowLeft"||o.key==="ArrowRight")&&(o.stopPropagation(),o.preventDefault()),o.key==="Escape"&&n(),o.key==="ArrowRight"&&l(u=>Math.min(u+1,e.length-1)),o.key==="ArrowLeft"&&l(u=>Math.max(u-1,0))};return document.addEventListener("keydown",a),()=>document.removeEventListener("keydown",a)},[n,e.length]);const i=e[r];return s.jsx("div",{style:J.lightboxBackdrop,onClick:n,children:s.jsxs("div",{style:J.lightboxContent,onClick:a=>a.stopPropagation(),children:[s.jsxs("div",{style:J.lightboxToolbar,children:[e.length>1&&s.jsxs("span",{style:J.lightboxCounter,children:[r+1," / ",e.length]}),s.jsx("a",{href:i.src,target:"_blank",rel:"noopener noreferrer",style:J.lightboxLink,title:"Open in new tab",children:"↗"}),s.jsx("button",{style:J.lightboxBtn,onClick:n,"aria-label":"Close",children:"×"})]}),s.jsx("img",{src:i.src,alt:i.alt||"",style:J.lightboxImg}),e.length>1&&s.jsxs("div",{style:J.lightboxNav,children:[s.jsx("button",{style:J.lightboxNavBtn,onClick:()=>l(a=>Math.max(a-1,0)),disabled:r===0,children:"‹"}),s.jsx("button",{style:J.lightboxNavBtn,onClick:()=>l(a=>Math.min(a+1,e.length-1)),disabled:r===e.length-1,children:"›"})]})]})})}function cu({tweet:e,isInThread:t,isLast:n,artifactMap:r}){var d,v;const[l,i]=f.useState(null),a=e.author||{},o=e.created_at_secs?Dd(e.created_at_secs):"",u=e.entities||{},c=On(a.avatar_local_path,a.avatar_url,r),g=t&&!n,m=e.is_quote_status===!0,h=t?J.tweetRowThread:J.tweetRow,y=e.full_text||"",x=((v=(d=e.extended_entities)==null?void 0:d.media)!=null&&v.length?e.extended_entities.media:u.media)||[],k=[],j=[];for(const w of x)if(w.type==="photo"){const _=On(w.local_path,w.media_url_https,r);_&&k.push({kind:"photo",src:_,alt:w.alt_text||""})}else if(w.type==="video"||w.type==="animated_gif"){const _=w.local_path&&r[w.local_path]||(()=>{var C,S;return(S=(((C=w.video_info)==null?void 0:C.variants)||[]).filter($=>$.content_type==="video/mp4").sort(($,L)=>(L.bitrate||0)-($.bitrate||0))[0])==null?void 0:S.url})();_&&j.push({kind:w.type==="animated_gif"?"gif":"video",src:_})}const p=[];for(const w of x){if(!w.url||!(w.type==="photo"?k.some(C=>C.src===On(w.local_path,w.media_url_https,r)):j.length>0))continue;const P=Ha(w,y,w.url);P&&p.push([P.s,P.e])}return s.jsxs(s.Fragment,{children:[l!==null&&s.jsx(Fd,{items:k,startIndex:l,onClose:()=>i(null)}),s.jsxs("div",{style:h,children:[s.jsxs("div",{style:J.leftCol,children:[c?s.jsx("img",{src:c,style:J.avatar,alt:a.name||""}):s.jsx("div",{style:J.avatarPh}),g&&s.jsx("div",{style:J.threadLine})]}),s.jsxs("div",{style:J.rightCol,children:[s.jsxs("div",{style:J.authorRow,children:[s.jsx("span",{style:J.authorName,children:a.name||a.screen_name||"Unknown"}),a.screen_name&&s.jsxs("span",{style:J.authorHandle,children:["@",a.screen_name]}),o&&s.jsxs("span",{style:J.datePart,children:["· ",o]}),m&&s.jsx("span",{style:J.qtBadge,title:"Quote tweet",children:"↻ QT"})]}),s.jsx("div",{style:J.tweetText,children:Rm(y,u,p)}),k.length>0&&s.jsx("div",{style:J.mediaGrid,children:s.jsx(Fm,{photos:k,onOpen:w=>i(w)})}),j.map((w,_)=>s.jsx("div",{style:J.mediaGrid,children:s.jsx("video",{src:w.src,style:J.mediaVideo,controls:!0,loop:w.kind==="gif",muted:w.kind==="gif",autoPlay:w.kind==="gif"})},_)),(e.retweet_count>0||e.favorite_count>0)&&s.jsxs("div",{style:J.stats,children:[e.favorite_count>0&&s.jsxs("span",{children:["❤️ ",e.favorite_count.toLocaleString()]}),e.retweet_count>0&&s.jsxs("span",{children:["🔁 ",e.retweet_count.toLocaleString()]})]})]})]})]})}function Mm({archiveId:e,entryUid:t,artifacts:n,entityKind:r,fullPage:l,onXArticle:i}){const[a,o]=f.useState(!0),[u,c]=f.useState(null),[g,m]=f.useState([]);if(f.useEffect(()=>{if(o(!0),c(null),m([]),!n||!e||!t){o(!1);return}const x=n.map((j,p)=>({...j,index:p})).filter(j=>j.artifact_role==="raw_tweet_json");if(x.length===0){c("No tweet data found."),o(!1);return}let k=!1;return gh(e,t,x.map(j=>j.index)).then(async j=>{if(k)return;const p=/https?:\/\/t\.co\/[A-Za-z0-9]+/g,d=C=>{var $;const S=C.full_text||"";return((($=C.entities)==null?void 0:$.urls)||[]).map(L=>{var W;if(L.fromIndex!=null&&L.toIndex!=null)return[L.fromIndex,L.toIndex];if(((W=L.indices)==null?void 0:W.length)===2)return[L.indices[0],L.indices[1]];if(L.url){const A=S.indexOf(L.url);if(A!==-1)return[A,A+L.url.length]}return null}).filter(Boolean)},v=(C,S,$)=>C.some(([L,W])=>L<=S&&W>=$),w=new Set;for(const C of j){const S=C.full_text||"",$=d(C);let L;for(p.lastIndex=0;(L=p.exec(S))!==null;)v($,L.index,L.index+L[0].length)||w.add(L[0])}const _=w.size>0?await vh([...w]).catch(()=>({})):{},P=j.map(C=>{var A;const S=C.full_text||"",$=d(C),L=[];let W;for(p.lastIndex=0;(W=p.exec(S))!==null;){const Q=W[0],ee=_[Q];ee&&ee!==Q&&!v($,W.index,W.index+Q.length)&&L.push({url:Q,expanded_url:ee,display_url:(()=>{try{const le=new URL(ee);return le.hostname+(le.pathname.length>1?"/…":"")}catch{return ee}})(),fromIndex:W.index,toIndex:W.index+Q.length})}return L.length===0?C:{...C,entities:{...C.entities||{},urls:[...((A=C.entities)==null?void 0:A.urls)||[],...L]}}});k||m(P)}).catch(j=>{k||c(j.message||"Failed to load tweet.")}).finally(()=>{k||o(!1)}),()=>{k=!0}},[e,t,n]),a)return s.jsx("div",{style:J.loading,children:"Loading…"});if(u)return s.jsxs("div",{style:J.error,children:["Error: ",u]});if(g.length===0)return null;const h=Lm(e,t,n);if(r==="tweet_thread")return s.jsx("div",{style:J.threadOuter,children:g.map((x,k)=>s.jsx(cu,{tweet:x,isInThread:!0,isLast:k===g.length-1,artifactMap:h},x.id||k))});const y=g[0];return y.is_article&&y.article?s.jsx(Om,{article:y.article,tweetAuthor:y.author,artifactMap:h,fullPage:l,onXArticle:i}):s.jsx("div",{style:J.card,children:s.jsx(cu,{tweet:y,isInThread:!1,isLast:!0,artifactMap:h})})}const Am=new Set(["mp4","webm","mov","mkv","avi","m4v","ogv"]),Bm=new Set(["mp3","ogg","m4a","opus","wav","flac","aac"]),Um=new Set(["jpg","jpeg","png","gif","webp","avif","svg","bmp"]);function Md({archiveId:e,entry:t,detail:n,fullPage:r,onXArticle:l}){if(!t)return s.jsx("div",{className:"preview-panel preview-panel--empty",children:s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"Select an entry to preview"})});if(!n)return s.jsx("div",{className:"preview-panel preview-panel--loading",children:s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"Loading…"})});const{summary:i,artifacts:a}=n,o=i.entry_uid,u=i.entity_kind;if(u==="tweet"||u==="tweet_thread"){const y=s.jsx(Mm,{archiveId:e,entryUid:o,artifacts:a,entityKind:u,fullPage:r,onXArticle:l});return r?y:s.jsx("div",{className:"preview-tweet-wrap",children:y})}const c=a.findIndex(y=>y.artifact_role==="primary_media");if(c===-1)return s.jsxs("div",{className:"preview-panel preview-panel--no-preview",children:[s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No preview available"}),a.length>0&&s.jsx("ul",{style:{marginTop:"12px",paddingLeft:"20px",fontSize:"0.8rem",color:"var(--muted-2)",fontFamily:"var(--sans)"},children:a.map((y,x)=>s.jsxs("li",{children:[y.artifact_role,": ",y.relpath]},x))})]});const g=a[c],m=`/api/archives/${e}/entries/${o}/artifacts/${c}`,h=g.relpath.split(".").pop().toLowerCase();return Am.has(h)?s.jsx("div",{className:"preview-panel",children:s.jsx(bm,{src:m})}):Bm.has(h)?s.jsxs("div",{className:"preview-panel preview-panel--audio",style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:"12px",padding:"24px",fontFamily:"var(--sans)"},children:[s.jsx("span",{style:{fontSize:"2rem"},children:"🎵"}),s.jsx("span",{style:{color:"var(--ink)",fontSize:"0.95rem",fontWeight:600},children:i.title||o}),s.jsx("audio",{src:m,controls:!0,style:{marginTop:"8px",width:"100%",maxWidth:"400px"}})]}):h==="pdf"?s.jsx("div",{className:"preview-panel",style:{flex:1,minHeight:0},children:s.jsx(au,{src:m,type:"pdf",title:i.title,originalUrl:i.original_url})}):h==="html"||h==="htm"?s.jsx("div",{className:"preview-panel",style:{flex:1,minHeight:0},children:s.jsx(au,{src:m,type:"page",title:i.title,originalUrl:i.original_url})}):Um.has(h)?s.jsx("div",{className:"preview-panel",style:{height:"100%"},children:s.jsx(Pm,{src:m,alt:i.title||"Image"})}):s.jsxs("div",{className:"preview-panel preview-panel--no-preview",children:[s.jsx("span",{style:{color:"var(--muted)",fontFamily:"var(--sans)",fontSize:"0.9rem"},children:"No preview available"}),s.jsx("ul",{style:{marginTop:"12px",paddingLeft:"20px",fontSize:"0.8rem",color:"var(--muted-2)",fontFamily:"var(--sans)"},children:a.map((y,x)=>s.jsxs("li",{children:[y.artifact_role,": ",y.relpath]},x))})]})}function Hm({archiveId:e,entry:t,detail:n,onClose:r}){var l,i;return f.useEffect(()=>{const a=o=>{o.key==="Escape"&&r()};return window.addEventListener("keydown",a),()=>window.removeEventListener("keydown",a)},[r]),s.jsx("div",{className:"preview-modal-backdrop",onClick:r,children:s.jsxs("div",{className:`preview-modal${((l=n==null?void 0:n.summary)==null?void 0:l.entity_kind)==="tweet"||((i=n==null?void 0:n.summary)==null?void 0:i.entity_kind)==="tweet_thread"?"":" preview-modal--full"}`,onClick:a=>a.stopPropagation(),children:[s.jsxs("div",{className:"preview-modal-header",children:[s.jsx("span",{className:"preview-modal-title",children:(t==null?void 0:t.title)||(t==null?void 0:t.entry_uid)||"Preview"}),s.jsx("a",{className:"preview-modal-newtab",href:`/preview/${e}/${t==null?void 0:t.entry_uid}`,target:"_blank",rel:"noopener noreferrer",title:"Open in new tab",children:"↗"}),s.jsx("button",{className:"preview-modal-close",onClick:r,"aria-label":"Close preview",children:"×"})]}),s.jsx("div",{className:"preview-modal-body",children:s.jsx(Md,{archiveId:e,entry:t,detail:n})})]})})}function du(e){if(!isFinite(e)||isNaN(e))return"--:--";const t=Math.floor(e/60),n=Math.floor(e%60);return`${t}:${String(n).padStart(2,"0")}`}function Wm({entry:e,src:t,archiveId:n,onClose:r}){const l=f.useRef(null),[i,a]=f.useState(!1),[o,u]=f.useState(0),[c,g]=f.useState(NaN),[m,h]=f.useState(1);f.useEffect(()=>{!l.current||!t||(l.current.load(),u(0),g(NaN),a(!1))},[t]),f.useEffect(()=>{l.current&&(l.current.volume=m)},[m]);function y(){const d=l.current;d&&(i?d.pause():d.play().catch(()=>{}))}function x(d){const v=l.current;if(!v||!isFinite(c))return;const w=Number(d.target.value);v.currentTime=w,u(w)}function k(d){h(Number(d.target.value))}const j=(e==null?void 0:e.title)||(e==null?void 0:e.entry_uid)||"Unknown",p=(e==null?void 0:e.source_kind)||"other";return s.jsxs(s.Fragment,{children:[s.jsx("audio",{ref:l,src:t||void 0,preload:"auto",onPlay:()=>a(!0),onPause:()=>a(!1),onEnded:()=>a(!1),onTimeUpdate:()=>{var d;return u(((d=l.current)==null?void 0:d.currentTime)??0)},onLoadedMetadata:()=>{var d;return g(((d=l.current)==null?void 0:d.duration)??NaN)},onDurationChange:()=>{var d;return g(((d=l.current)==null?void 0:d.duration)??NaN)},style:{display:"none"}}),s.jsxs("div",{className:"audio-bar",style:{position:"fixed",bottom:0,left:0,right:0,zIndex:100,background:"var(--paper-3)",borderTop:"1px solid var(--line)",display:"flex",alignItems:"center",gap:"16px",padding:"0 16px",height:"56px",fontFamily:"var(--sans)"},children:[s.jsxs("div",{className:"audio-bar-info",style:{display:"flex",alignItems:"center",gap:"8px",minWidth:0,flex:"0 1 220px",overflow:"hidden"},children:[s.jsx("span",{className:"source-icon",style:{flexShrink:0,width:"18px",height:"18px",display:"flex",alignItems:"center"},dangerouslySetInnerHTML:{__html:Ua(p)}}),s.jsx("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontSize:"0.85rem",color:"var(--ink)"},title:j,children:j})]}),s.jsxs("div",{className:"audio-bar-controls",style:{display:"flex",alignItems:"center",gap:"10px",flex:"1 1 0",minWidth:0},children:[s.jsx("button",{onClick:y,"aria-label":i?"Pause":"Play",style:{background:"none",border:"none",cursor:"pointer",padding:"4px",color:"var(--ink)",flexShrink:0,fontSize:"1.2rem",lineHeight:1},children:i?"⏸":"▶"}),s.jsx("span",{style:{fontSize:"0.75rem",color:"var(--muted)",whiteSpace:"nowrap",flexShrink:0},children:du(o)}),s.jsx("input",{type:"range",min:0,max:isFinite(c)?c:0,step:.1,value:isFinite(o)?o:0,onChange:x,"aria-label":"Seek",style:{flex:1,minWidth:0,accentColor:"var(--accent)"}}),s.jsx("span",{style:{fontSize:"0.75rem",color:"var(--muted)",whiteSpace:"nowrap",flexShrink:0},children:du(c)})]}),s.jsxs("div",{className:"audio-bar-right",style:{display:"flex",alignItems:"center",gap:"8px",flex:"0 1 160px"},children:[s.jsx("span",{style:{fontSize:"0.85rem",color:"var(--muted)",flexShrink:0},children:"🔊"}),s.jsx("input",{type:"range",min:0,max:1,step:.01,value:m,onChange:k,"aria-label":"Volume",style:{width:"80px",accentColor:"var(--accent)"}}),s.jsx("button",{onClick:r,"aria-label":"Close audio player",style:{background:"none",border:"none",cursor:"pointer",padding:"4px",color:"var(--muted)",fontSize:"1rem",lineHeight:1,marginLeft:"4px"},children:"✕"})]})]})]})}function Vm({archiveId:e,entryUid:t}){var h,y;const[n,r]=f.useState(null),[l,i]=f.useState(!0),[a,o]=f.useState(null),[u,c]=f.useState(!1);f.useEffect(()=>{if(!u)return;const x=document.documentElement,k=document.body,j=x.style.colorScheme,p=k.style.background;return x.style.colorScheme="dark",k.style.background="#000",()=>{x.style.colorScheme=j,k.style.background=p}},[u]),f.useEffect(()=>{c(!1),Vi(e,t).then(x=>{r(x),i(!1)}).catch(x=>{o((x==null?void 0:x.message)||"Failed to load entry"),i(!1)})},[e,t]);const g=((h=n==null?void 0:n.summary)==null?void 0:h.title)||t,m=(y=n==null?void 0:n.summary)==null?void 0:y.original_url;return s.jsxs("div",{style:{minHeight:"100vh",display:"flex",flexDirection:"column",background:u?"#000":"var(--paper)",fontFamily:"var(--sans)"},children:[s.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px",padding:u?"8px 12px":"10px 16px",borderBottom:u?"none":"1px solid var(--line)",flexShrink:0,position:u?"sticky":"relative",top:0,zIndex:20,background:u?"rgba(0,0,0,0.82)":"var(--paper-2)",backdropFilter:u?"blur(12px)":"none",WebkitBackdropFilter:u?"blur(12px)":"none"},children:[s.jsx("a",{href:"/",style:{color:u?"#1d9bf0":"var(--accent)",textDecoration:"none",fontSize:"13px",flexShrink:0},children:"← Archive"}),s.jsx("span",{style:{flex:1,fontSize:"14px",fontWeight:600,color:u?"#e7e9ea":"var(--ink)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:g}),m&&s.jsx("a",{href:m,target:"_blank",rel:"noopener noreferrer",style:{color:u?"#71767b":"var(--muted)",textDecoration:"none",fontSize:"13px",flexShrink:0},children:"Original ↗"})]}),s.jsxs("div",{style:u?{flex:1}:{flex:1,minHeight:0,overflow:"auto",display:"flex",flexDirection:"column"},children:[l&&s.jsx("div",{style:{padding:"48px",textAlign:"center",color:"var(--muted)",fontSize:"14px"},children:"Loading…"}),a&&s.jsx("div",{style:{padding:"48px",textAlign:"center",color:"var(--alert)",fontSize:"14px"},children:a}),n&&s.jsx(Md,{archiveId:e,entry:n.summary,detail:n,fullPage:!0,onXArticle:c})]})]})}const Qm=7e3;function Km({toasts:e,onDismiss:t,onIgnoreUblock:n}){return e.length?s.jsx("div",{className:"toast-stack",role:"log","aria-live":"polite","aria-label":"Notifications",children:e.map(r=>s.jsx(Jm,{toast:r,onDismiss:t,onIgnoreUblock:n},r.id))}):null}function Xm(e){if(!e)return null;if(e.length<=52)return e;try{const{hostname:t,pathname:n,search:r}=new URL(e),l=n.split("/").filter(Boolean),i=(l[l.length-1]??"")+r,a=i?`${t}/…/${i}`:t;return a.length<=56?a:a.slice(0,53)+"…"}catch{return"…"+e.slice(-51)}}function Jm({toast:e,onDismiss:t,onIgnoreUblock:n}){const[r,l]=f.useState(!1),i=e.type==="warning",a=e.type==="success";f.useEffect(()=>{if(r)return;const u=setTimeout(()=>t(e.id),Qm);return()=>clearTimeout(u)},[r,e.id,t]);const o=Xm(e.locator);return a?s.jsx("div",{className:"toast toast--success",role:"alert","aria-atomic":"true",children:s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"✓"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Archived"}),o&&s.jsx("span",{className:"toast-locator",title:e.locator,children:o})]}),s.jsx("div",{className:"toast-btns",children:s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})})]})}):i?s.jsxs("div",{className:"toast toast--warning",role:"alert","aria-atomic":"true",children:[s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"⚠"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Archived with warnings"}),o&&s.jsx("span",{className:"toast-locator",title:e.locator,children:o})]}),s.jsxs("div",{className:"toast-btns",children:[e.text&&s.jsx("button",{type:"button",className:"toast-view-btn",onClick:()=>l(u=>!u),"aria-expanded":r,children:r?"Hide":"Details"}),e.locator&&s.jsx("button",{type:"button",className:"toast-view-btn toast-ignore-btn",onClick:()=>{n==null||n(),t(e.id)},children:"Ignore"}),s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})]})]}),r&&s.jsx("p",{className:"toast-warning-detail",children:e.text||"The page was captured but one or more browser extensions were unavailable (ad-blocking or cookie-consent). Check ARCHIVR_UBLOCK_EXT and ARCHIVR_COOKIE_EXT in your server config."})]}):s.jsxs("div",{className:"toast toast--error",role:"alert","aria-atomic":"true",children:[s.jsxs("div",{className:"toast-top",children:[s.jsx("span",{className:"toast-icon","aria-hidden":"true",children:"✕"}),s.jsxs("div",{className:"toast-body",children:[s.jsx("span",{className:"toast-headline",children:e.headline||"Capture failed"}),o&&s.jsx("span",{className:"toast-locator",title:e.locator,children:o})]}),s.jsxs("div",{className:"toast-btns",children:[e.text&&s.jsx("button",{type:"button",className:"toast-view-btn",onClick:()=>l(u=>!u),children:r?"Hide":"View error"}),s.jsx("button",{type:"button",className:"toast-dismiss",onClick:()=>t(e.id),"aria-label":"Dismiss",children:"×"})]})]}),r&&e.text&&s.jsx("pre",{className:"toast-error-detail",children:e.text})]})}const hs=f.createContext(null),or=(()=>{const e=window.location.pathname.match(/^\/preview\/([^/]+)\/([^/]+)/);return e?{archiveId:e[1],entryUid:e[2]}:null})(),qm=["archive","tags","collections","runs","admin","settings"],Ym=["profile","tokens","instance","cookies","extensions","storage"];function Yt(){const e=window.location.pathname.split("/").filter(Boolean),t=qm.includes(e[0])?e[0]:"archive",n=t==="settings"&&Ym.includes(e[1])?e[1]:"profile",r=new URLSearchParams(window.location.search),l=r.get("q")??"",i=t==="archive"?r.get("tag")??null:null,a=t==="archive"?r.get("entry")??null:null;return{view:t,settingsTab:n,q:l,tag:i,entry:a}}function Gm(e,t){return e==="archive"?"/":e==="settings"?`/settings/${t}`:`/${e}`}function Zm(){const[e,t]=f.useState("loading"),[n,r]=f.useState(null);f.useEffect(()=>{(async()=>{if(await Ch()){t("setup");return}const R=await Ph();if(!R){t("login");return}r(R),t("authenticated")})()},[]),f.useEffect(()=>{const E=()=>{r(null),t("login")};return window.addEventListener("auth:expired",E),()=>window.removeEventListener("auth:expired",E)},[]),f.useEffect(()=>{const E=()=>{const{view:R,settingsTab:V,q:re,tag:ze,entry:et}=Yt();v(R),_(V),C(re),p(ze),m(et),y(null),k(et?new Set([et]):new Set)};return window.addEventListener("popstate",E),()=>window.removeEventListener("popstate",E)},[]);const[l,i]=f.useState([]),[a,o]=f.useState(null),[u,c]=f.useState([]),[g,m]=f.useState(()=>Yt().entry),[h,y]=f.useState(null),[x,k]=f.useState(()=>{const E=Yt().entry;return E?new Set([E]):new Set}),[j,p]=f.useState(()=>Yt().tag),[d,v]=f.useState(()=>Yt().view),[w,_]=f.useState(()=>Yt().settingsTab),[P,C]=f.useState(()=>Yt().q),[S,$]=f.useState(""),[L,W]=f.useState(!1),[A,Q]=f.useState([]),[ee,le]=f.useState([]),[de,ne]=f.useState(()=>sessionStorage.getItem("captureDialogOpen")==="true"),[D,b]=f.useState([]),M=f.useRef(0),[Y,U]=f.useState(()=>sessionStorage.getItem("ublockWarningIgnored")==="true"),[fe,ae]=f.useState(null),N=f.useRef(0),F=f.useRef(null),H=f.useRef(!1),X=f.useRef(!0),K=f.useRef(null),pe=(n==null?void 0:n.humanize_slugs)??!1;f.useEffect(()=>{const E=++N.current;ae(null),!(!h||!a)&&Vi(a,h.entry_uid).then(R=>{E===N.current&&ae(R)}).catch(()=>{})},[h,a]),f.useEffect(()=>{sessionStorage.setItem("captureDialogOpen",de)},[de]);const G=f.useCallback(async(E,R,V)=>{if(E){W(!0);try{let re;R||V?re=await mh(E,R,V):re=await hh(E),c(re),k(ze=>{if(ze.size<2)return ze;const et=new Set(re.map(vn=>vn.entry_uid)),Yn=new Set([...ze].filter(vn=>et.has(vn)));return Yn.size===ze.size?ze:Yn}),$(re.length===0?"No results":`${re.length} result${re.length===1?"":"s"}`)}catch{c([]),$("Search failed. Try again.")}finally{W(!1)}}},[]);f.useEffect(()=>{e==="authenticated"&&ph().then(E=>{if(i(E),E.length>0){const R=E[0].id;o(R)}})},[e]),f.useEffect(()=>{if(a){if(X.current){X.current=!1,Promise.all([Js(a).then(Q),fl(a).then(le)]);return}p(null),y(null),m(null),k(new Set),Promise.all([G(a,"",null),Js(a).then(Q),fl(a).then(le)])}},[a]),f.useEffect(()=>{if(a===null)return;const E=setTimeout(()=>{G(a,P,j)},300);return()=>clearTimeout(E)},[P,a]),f.useEffect(()=>{a!==null&&(j!==null&&v("archive"),G(a,P,j))},[j,a]);const q=f.useCallback(E=>{o(E)},[]),T=f.useCallback(E=>{v(E),E==="tags"&&a&&fl(a).then(le)},[a]);f.useEffect(()=>{if(or)return;const E=Gm(d,w);window.location.pathname!==E&&history.pushState(null,"",E+window.location.search)},[d,w]);const B=f.useCallback(E=>{m(E?E.entry_uid:null),y(E)},[]),Ce=f.useCallback((E,R)=>{if(R.shiftKey&&K.current!==null){R.preventDefault();const V=u.findIndex(Gn=>Gn.entry_uid===K.current),re=u.findIndex(Gn=>Gn.entry_uid===E.entry_uid);if(V===-1||re===-1){K.current=E.entry_uid,k(new Set([E.entry_uid])),B(E);return}const ze=Math.min(V,re),et=Math.max(V,re),Yn=u.slice(ze,et+1),vn=new Set(Yn.map(Gn=>Gn.entry_uid));k(vn),vn.size===1&&B(Yn[0])}else R.ctrlKey||R.metaKey?(K.current=E.entry_uid,k(V=>{const re=new Set(V);return re.has(E.entry_uid)?re.delete(E.entry_uid):re.add(E.entry_uid),re})):(K.current=E.entry_uid,k(new Set([E.entry_uid])),B(E))},[u,B]),ke=f.useCallback(E=>{p(E)},[]),Xn=f.useCallback(()=>{p(null)},[]),Ze=f.useCallback(()=>{a&&fl(a).then(le)},[a]),ms=f.useCallback((E,R)=>{j===E?p(R):j!=null&&j.startsWith(E+"/")&&p(R+j.slice(E.length))},[j]),gs=f.useCallback(E=>{(j===E||j!=null&&j.startsWith(E+"/"))&&p(null)},[j]),Vr=f.useCallback((E,R)=>{c(V=>V.map(re=>re.entry_uid===E?{...re,title:R}:re)),y(V=>V&&V.entry_uid===E?{...V,title:R}:V),ae(V=>V&&V.summary.entry_uid===E?{...V,summary:{...V.summary,title:R}}:V)},[]),vs=f.useCallback(()=>{if(!a||!h)return;const E=++N.current;Vi(a,h.entry_uid).then(R=>{E===N.current&&ae(R)}).catch(()=>{})},[a,h]),ys=f.useCallback(E=>{c(R=>R.filter(V=>V.entry_uid!==E)),y(R=>(R==null?void 0:R.entry_uid)===E?null:R),m(R=>R===E?null:R),k(R=>{const V=new Set(R);return V.delete(E),V})},[]),xs=f.useCallback(E=>{c(R=>R.filter(V=>!E.has(V.entry_uid))),k(new Set),y(null),m(null)},[]);f.useEffect(()=>{if(x.size>=2)m(null),y(null);else if(x.size===1){const[E]=x;m(E)}else m(null),y(null)},[x]);const ws=f.useMemo(()=>u.filter(E=>x.has(E.entry_uid)),[u,x]);f.useEffect(()=>{if(!g||h)return;const E=u.find(R=>R.entry_uid===g);E&&y(E)},[u,g,h]),f.useEffect(()=>{if(or)return;const E=new URLSearchParams;P&&E.set("q",P),d==="archive"&&j&&E.set("tag",j),d==="archive"&&g&&E.set("entry",g);const R=E.toString(),V=window.location.pathname+(R?"?"+R:"");window.location.pathname+window.location.search!==V&&history.replaceState(null,"",V)},[P,j,g,d]),f.useEffect(()=>{const E=R=>{var V,re;(R.metaKey||R.ctrlKey)&&R.key==="k"&&(R.preventDefault(),d==="archive"?((V=F.current)==null||V.focus(),(re=F.current)==null||re.select()):(H.current=!0,v("archive")))};return document.addEventListener("keydown",E),()=>document.removeEventListener("keydown",E)},[d]),f.useEffect(()=>{d==="archive"&&H.current&&(H.current=!1,requestAnimationFrame(()=>{var E,R;(E=F.current)==null||E.focus(),(R=F.current)==null||R.select()}))},[d]);const ks=f.useCallback(()=>{ne(!0)},[]),js=f.useCallback(()=>{ne(!1)},[]),hn=f.useCallback(()=>{a&&Promise.all([G(a,P,j),Js(a).then(Q)])},[a,P,j,G]),mn=f.useCallback((E,R,V="error",re=null)=>{if(V==="warning"&&Y&&R)return;const ze=++M.current;b(et=>[...et,{id:ze,text:E,locator:R,type:V,headline:re}])},[Y]),Qr=f.useCallback(E=>{b(R=>R.filter(V=>V.id!==E))},[]),Kr=f.useCallback(()=>{sessionStorage.setItem("ublockWarningIgnored","true"),U(!0),b(E=>E.filter(R=>!(R.type==="warning"&&R.locator)))},[]),[gn,Jn]=f.useState(null),[bt,I]=f.useState(null),se=f.useCallback(()=>{h&&Jn(h.entry_uid)},[h]),Qe=f.useCallback(()=>Jn(null),[]),qt=f.useCallback((E,R)=>{I({src:E,entry:R})},[]),qn=f.useCallback(()=>I(null),[]);return f.useEffect(()=>{Jn(null)},[h]),f.useEffect(()=>{const E=R=>{var re,ze,et;if(R.key!=="Escape"||de||gn)return;const V=(re=document.activeElement)==null?void 0:re.tagName;V==="INPUT"||V==="TEXTAREA"||V==="SELECT"||x.size>0&&(R.preventDefault(),k(new Set),(et=(ze=document.activeElement)==null?void 0:ze.blur)==null||et.call(ze))};return window.addEventListener("keydown",E),()=>window.removeEventListener("keydown",E)},[de,gn,x]),f.useEffect(()=>(document.body.classList.toggle("has-audio-bar",!!bt),()=>document.body.classList.remove("has-audio-bar")),[bt]),e==="loading"?s.jsx("div",{className:"auth-loading",children:"Loading\\u2026"}):e==="setup"?s.jsx(rm,{onComplete:()=>t("login")}):e==="login"?s.jsx(nm,{onLogin:E=>{r(E),t("authenticated")}}):or?s.jsx(Vm,{archiveId:or.archiveId,entryUid:or.entryUid}):s.jsx(hs.Provider,{value:{currentUser:n,setCurrentUser:r},children:s.jsxs(s.Fragment,{children:[s.jsx(lm,{archives:l,archiveId:a,onArchiveChange:q,view:d,onViewChange:T,onCaptureClick:ks}),s.jsxs("main",{className:"app-shell",children:[s.jsxs("div",{className:"workspace",children:[d==="archive"&&s.jsxs("div",{className:"toolbar",children:[s.jsxs("div",{className:"search-field",children:[s.jsx("span",{className:"ico","aria-hidden":"true",children:s.jsxs("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",children:[s.jsx("circle",{cx:"11",cy:"11",r:"7"}),s.jsx("line",{x1:"16.5",y1:"16.5",x2:"21",y2:"21"})]})}),s.jsx("input",{ref:F,className:"search-input",type:"search","aria-label":"Search archive","aria-busy":L,placeholder:"Search titles, URLs, types, tags…",value:P,onChange:E=>C(E.target.value)}),s.jsx("span",{className:"kbd",children:"⌘K"})]}),s.jsxs("span",{className:"result-count",children:[S&&s.jsxs(s.Fragment,{children:[s.jsx("b",{children:S.split(" ")[0]})," ",S.split(" ").slice(1).join(" ")]}),j&&s.jsxs("button",{className:"tag-filter-badge",onClick:Xn,children:["× ",pe?Pd(j):j]})]})]}),d==="archive"&&s.jsx(cm,{entries:u,selectedUids:x,onRowClick:Ce,archiveId:a}),d==="runs"&&s.jsx(pm,{runs:A}),d==="admin"&&s.jsx(mm,{archives:l}),d==="tags"&&s.jsx(gm,{archiveId:a,tagNodes:ee,tagFilter:j,onTagFilterSet:ke,onViewChange:T,onTagRenamed:ms,onTagDeleted:gs,onTagsRefresh:Ze,humanizeTags:pe}),d==="collections"&&s.jsx(ym,{archiveId:a}),d==="settings"&&s.jsx(wm,{tab:w,onTabChange:_,archiveId:a})]}),s.jsx(Tm,{archiveId:a,selectedEntry:h,selectedUids:x,selectedEntries:ws,detail:fe,onTagFilterSet:ke,tagNodes:ee,onTagsRefresh:Ze,onEntryTitleChange:Vr,onEntryDeleted:ys,onBulkDeleted:xs,humanizeTags:pe,onDetailRefresh:vs,onOpenPreview:se,onPlay:qt})]}),gn&&h&&h.entry_uid===gn&&s.jsx(Hm,{archiveId:a,entry:h,detail:fe,onClose:Qe}),bt&&s.jsx(Wm,{entry:bt.entry,src:bt.src,archiveId:a,onClose:qn}),s.jsx(sm,{open:de,archiveId:a,onClose:js,onCaptured:hn,onToast:mn}),s.jsx(Km,{toasts:D,onDismiss:Qr,onIgnoreUblock:Kr})]})})}Nd(document.getElementById("root")).render(s.jsx(f.StrictMode,{children:s.jsx(Zm,{})})); diff --git a/crates/archivr-server/static/index.html b/crates/archivr-server/static/index.html index 395784e..0825258 100644 --- a/crates/archivr-server/static/index.html +++ b/crates/archivr-server/static/index.html @@ -4,8 +4,8 @@ Archivr - - + +
diff --git a/docs/README.md b/docs/README.md index b421947..add643c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -141,7 +141,7 @@ Auth and session handling will be designed when remote or public hosting becomes ### Supported Platforms - Local files: `file:///absolute/path/to/file.ext` -- YouTube media: standard video/short URLs, plus [shorthand video inputs](#supported-shorthand-inputs) +- YouTube media: individual videos/shorts, playlists, and channels; standard URLs or [shorthand video inputs](#supported-shorthand-inputs). Playlists and channels archive as a container entry with each video stored as a child entry beneath it. - X/Twitter media from Tweets: normal Tweet URLs or the `tweet:media:ID` shorthand - 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` @@ -175,6 +175,14 @@ The probe endpoint (`GET /api/archives/:id/captures/probe?locator=…`) requires ``` `has_video: false, has_audio: false` means yt-dlp found no downloadable tracks (e.g. a tweet with no media). A 502 means yt-dlp itself failed (transient network error, rate-limit, unsupported extractor) — treat as inconclusive, not "no media." +#### YouTube playlists and channels + +Capturing a YouTube playlist or channel URL creates a **container entry** for the playlist or channel, with each video archived as a child entry beneath it. Before downloading, the capture UI probes each video to fetch available quality options, letting you set quality per-video or apply a single quality to the whole playlist. + +**Incremental sync:** When re-archiving a playlist or channel, enable **sync mode** in the capture dialog to skip videos that are already in the archive. Only new videos are downloaded; the existing container entry is reused. + +**Excluding individual videos:** In the expanded per-video list, each video has a remove button (×) to exclude it from the current capture. Removed videos are not downloaded; the rest proceed normally. + ### Hosting on NixOS The flake exposes a `nixosModules.default` output. Add it to your system flake and diff --git a/docs/superpowers/plans/2026-06-25-auth-foundation.md b/docs/superpowers/plans/2026-06-25-auth-foundation.md deleted file mode 100644 index 3f9da52..0000000 --- a/docs/superpowers/plans/2026-06-25-auth-foundation.md +++ /dev/null @@ -1,1977 +0,0 @@ -# Auth Foundation Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add cookie-session + API-token authentication, a role bitmask system, a first-run setup wizard, and auth-protected routes to the Archivr server. - -**Architecture:** Auth state lives in a dedicated `archivr-auth.sqlite` file next to the server config, separate from the per-archive store DBs. `AppState` gains an `auth_db_path`. Route handlers call `database::open_auth_db()` to get a connection; a new `AuthUser` Axum extractor validates session cookies and Bearer tokens for every protected handler. - -**Tech Stack:** Rust/Axum, rusqlite (existing), argon2 (Argon2id hashing), rand (token generation), axum-extra (cookie extraction), React/Vite (existing frontend) - -**Spec:** `docs/superpowers/specs/2026-06-25-auth-foundation-design.md` - ---- - -## File Map - -| File | Action | What changes | -|---|---|---| -| `Cargo.toml` | Modify | Add `argon2`, `rand`, `axum-extra` to workspace deps | -| `crates/archivr-server/Cargo.toml` | Modify | Pull `argon2`, `rand`, `axum-extra` from workspace | -| `crates/archivr-core/src/database.rs` | Modify | `initialize_auth_schema`, auth CRUD helpers, new record types | -| `crates/archivr-server/src/auth.rs` | **Create** | `AuthUser` extractor, password helpers, token generation, role constants | -| `crates/archivr-server/src/routes.rs` | Modify | `AppState` + `app()` gain `auth_db_path`; auth endpoints; route protection; `ApiError` gets JSON body + `unauthorized`/`forbidden` constructors | -| `crates/archivr-server/src/registry.rs` | Modify | `ServerRegistry` gains optional `auth_db_path` | -| `crates/archivr-server/src/main.rs` | Modify | Compute auth DB path; pass to `app()`; session cleanup task; remove non-loopback auth warning | -| `frontend/src/App.jsx` | Modify | Setup check on mount; `AuthContext`; 401 handling | -| `frontend/src/api.js` | Modify | 401 interceptor; auth helper calls | -| `frontend/src/components/LoginPage.jsx` | **Create** | Login form | -| `frontend/src/components/SetupPage.jsx` | **Create** | First-run owner creation wizard | -| `frontend/src/components/Topbar.jsx` | Modify | User menu + logout button | - ---- - -## Task 1: Add dependencies - -**Files:** -- Modify: `Cargo.toml` -- Modify: `crates/archivr-server/Cargo.toml` - -- [ ] **Step 1: Add workspace dependencies** - -Open `Cargo.toml`. In `[workspace.dependencies]`, add after the `base64` line: - -```toml -argon2 = { version = "0.5", features = ["std"] } -rand = { version = "0.8", features = ["std"] } -axum-extra = { version = "0.9", features = ["cookie"] } -``` - -- [ ] **Step 2: Pull into server crate** - -Open `crates/archivr-server/Cargo.toml`. Add to `[dependencies]`: - -```toml -argon2.workspace = true -rand.workspace = true -axum-extra.workspace = true -``` - -- [ ] **Step 3: Verify compilation** - -```bash -cd ~/personal/archivr && cargo check -p archivr-server -``` - -Expected: compiles with no errors. - -- [ ] **Step 4: Commit** - -```bash -git add Cargo.toml crates/archivr-server/Cargo.toml Cargo.lock -git commit -m "feat(auth): add argon2, rand, axum-extra dependencies" -``` - ---- - -## Task 2: Auth schema in `database.rs` - -Add `initialize_auth_schema` and update `instance_settings`. - -**Files:** -- Modify: `crates/archivr-core/src/database.rs` - -- [ ] **Step 1: Write failing test for role seeding** - -At the bottom of the `#[cfg(test)]` block in `database.rs`, add: - -```rust -#[test] -fn auth_schema_seeds_builtin_roles() { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - let count: i64 = conn - .query_row("SELECT COUNT(*) FROM roles WHERE is_builtin = 1", [], |r| r.get(0)) - .unwrap(); - assert_eq!(count, 4); - let owner_bits: i64 = conn - .query_row("SELECT bit_position FROM roles WHERE slug = 'owner'", [], |r| r.get(0)) - .unwrap(); - assert_eq!(owner_bits, 3); -} - -#[test] -fn auth_schema_is_idempotent() { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - initialize_auth_schema(&conn).unwrap(); // must not panic -} -``` - -- [ ] **Step 2: Run to confirm they fail** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core auth_schema 2>&1 | tail -5 -``` - -Expected: FAILED — `initialize_auth_schema` not found. - -- [ ] **Step 3: Add `initialize_auth_schema`** - -After the closing `}` of `initialize_schema` in `database.rs`, add: - -```rust -pub fn initialize_auth_schema(conn: &Connection) -> Result<()> { - conn.pragma_update(None, "journal_mode", "WAL")?; - conn.pragma_update(None, "foreign_keys", "ON")?; - conn.execute_batch( - r#" - CREATE TABLE IF NOT EXISTS roles ( - id INTEGER PRIMARY KEY, - role_uid TEXT NOT NULL UNIQUE, - slug TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - level INTEGER NOT NULL, - bit_position INTEGER NOT NULL UNIQUE, - is_builtin INTEGER NOT NULL DEFAULT 0 CHECK (is_builtin IN (0, 1)) - ); - - INSERT OR IGNORE INTO roles (role_uid, slug, name, level, bit_position, is_builtin) VALUES - ('role-guest', 'guest', 'Guest', 0, 0, 1), - ('role-user', 'user', 'User', 1, 1, 1), - ('role-admin', 'admin', 'Admin', 3, 2, 1), - ('role-owner', 'owner', 'Owner', 4, 3, 1); - - CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_id INTEGER NOT NULL REFERENCES roles(id), - assigned_at TEXT NOT NULL, - assigned_by_user_id INTEGER REFERENCES users(id), - PRIMARY KEY (user_id, role_id) - ); - - CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY, - session_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_bits INTEGER NOT NULL, - created_at TEXT NOT NULL, - last_seen_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - user_agent TEXT - ); - CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); - - CREATE TABLE IF NOT EXISTS api_tokens ( - id INTEGER PRIMARY KEY, - token_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - token_hash TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - created_at TEXT NOT NULL, - last_used_at TEXT, - expires_at TEXT - ); - CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id ON api_tokens(user_id); - - CREATE TABLE IF NOT EXISTS instance_settings ( - id INTEGER PRIMARY KEY CHECK (id = 1), - public_index_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_index_enabled IN (0, 1)), - public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)), - public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)), - default_entry_visibility INTEGER NOT NULL DEFAULT 2 - ); - - INSERT OR IGNORE INTO instance_settings - (id, public_index_enabled, public_entry_content_enabled, - public_archive_submission_enabled, default_entry_visibility) - VALUES (1, 0, 0, 0, 2); - - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - user_uid TEXT NOT NULL UNIQUE, - username TEXT NOT NULL UNIQUE, - email TEXT UNIQUE, - password_hash TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('active', 'disabled')), - role TEXT NOT NULL CHECK (role IN ('admin', 'user')), - created_at TEXT NOT NULL, - last_login_at TEXT - ); - "#, - )?; - Ok(()) -} -``` - -Note: `users` is duplicated here with `CREATE TABLE IF NOT EXISTS` so the auth DB is self-contained without needing `initialize_schema`. - -- [ ] **Step 4: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core auth_schema 2>&1 | tail -5 -``` - -Expected: 2 tests pass. - -- [ ] **Step 5: Add `open_auth_db` function** - -After `open_or_initialize`, add: - -```rust -pub fn open_auth_db(auth_db_path: &Path) -> Result { - if let Some(parent) = auth_db_path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!("failed to create auth DB directory {}", parent.display()) - })?; - } - let conn = Connection::open(auth_db_path).with_context(|| { - format!("failed to open auth database at {}", auth_db_path.display()) - })?; - initialize_auth_schema(&conn)?; - Ok(conn) -} -``` - -- [ ] **Step 6: Verify compilation and commit** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core 2>&1 | tail -3 -``` - -Expected: all existing tests pass. - -```bash -git add crates/archivr-core/src/database.rs -git commit -m "feat(auth): add initialize_auth_schema and open_auth_db" -``` - ---- - -## Task 3: User and role DB helpers - -**Files:** -- Modify: `crates/archivr-core/src/database.rs` - -- [ ] **Step 1: Add record types** - -After the existing struct definitions (before `pub fn database_path`), add: - -```rust -#[derive(Debug, Clone)] -pub struct AuthUserRecord { - pub id: i64, - pub user_uid: String, - pub username: String, - pub password_hash: String, - pub status: String, -} - -#[derive(Debug, Clone)] -pub struct SessionRecord { - pub user_id: i64, - pub role_bits: u32, - pub last_seen_at: String, - pub session_uid: String, -} - -#[derive(Debug, Clone)] -pub struct ApiTokenRecord { - pub token_uid: String, - pub name: String, - pub created_at: String, - pub last_used_at: Option, -} -``` - -- [ ] **Step 2: Write failing tests for user helpers** - -In the `#[cfg(test)]` block, add: - -```rust -fn make_auth_conn() -> Connection { - let conn = Connection::open_in_memory().unwrap(); - initialize_auth_schema(&conn).unwrap(); - conn -} - -#[test] -fn ensure_owner_exists_returns_false_when_no_owner() { - let conn = make_auth_conn(); - assert!(!ensure_owner_exists(&conn).unwrap()); -} - -#[test] -fn create_owner_then_ensure_returns_true() { - let conn = make_auth_conn(); - create_owner(&conn, "alice", "hashed_pw").unwrap(); - assert!(ensure_owner_exists(&conn).unwrap()); -} - -#[test] -fn create_owner_assigns_cumulative_roles() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "hashed_pw").unwrap(); - let bits = compute_role_bits(&conn, user_id).unwrap(); - // guest=1, user=2, admin=4, owner=8 → 15 - assert_eq!(bits, 15u32); -} - -#[test] -fn get_user_by_username_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_user_by_username(&conn, "nobody").unwrap().is_none()); -} -``` - -- [ ] **Step 3: Run to confirm they fail** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core ensure_owner 2>&1 | tail -5 -``` - -Expected: FAILED. - -- [ ] **Step 4: Implement user/role helpers** - -After `open_auth_db`, add: - -```rust -/// Returns true if an owner account exists. -pub fn ensure_owner_exists(conn: &Connection) -> Result { - let count: i64 = conn.query_row( - "SELECT COUNT(*) FROM user_roles ur - JOIN roles r ON r.id = ur.role_id - WHERE r.slug = 'owner'", - [], - |row| row.get(0), - )?; - Ok(count > 0) -} - -/// Creates a user and assigns all roles from `user` up to `owner` (cumulative). -/// `password_hash` must already be hashed by the caller. -pub fn create_owner(conn: &Connection, username: &str, password_hash: &str) -> Result { - let user_uid = public_id("usr"); - conn.execute( - "INSERT INTO users (user_uid, username, email, password_hash, status, role, created_at) - VALUES (?1, ?2, NULL, ?3, 'active', 'admin', ?4)", - params![user_uid, username, password_hash, now_timestamp()], - )?; - let user_id = conn.last_insert_rowid(); - // Assign user, admin, owner (cumulative) - for slug in &["user", "admin", "owner"] { - let role_id: i64 = conn.query_row( - "SELECT id FROM roles WHERE slug = ?1", - [slug], - |row| row.get(0), - )?; - conn.execute( - "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at) - VALUES (?1, ?2, ?3)", - params![user_id, role_id, now_timestamp()], - )?; - } - Ok(user_id) -} - -pub fn get_user_by_username(conn: &Connection, username: &str) -> Result> { - conn.query_row( - "SELECT id, user_uid, username, password_hash, status FROM users WHERE username = ?1", - [username], - |row| { - Ok(AuthUserRecord { - id: row.get(0)?, - user_uid: row.get(1)?, - username: row.get(2)?, - password_hash: row.get(3)?, - status: row.get(4)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -/// Computes role_bits = ROLE_GUEST | OR(assigned role bit values). -/// ROLE_GUEST (bit 0, value 1) is always included as the implicit floor. -pub fn compute_role_bits(conn: &Connection, user_id: i64) -> Result { - let mut stmt = conn.prepare( - "SELECT (1 << r.bit_position) FROM user_roles ur - JOIN roles r ON r.id = ur.role_id - WHERE ur.user_id = ?1", - )?; - let bits: u32 = stmt - .query_map([user_id], |row| row.get::<_, i64>(0))? - .try_fold(1u32, |acc, val| val.map(|v| acc | v as u32))?; - Ok(bits) -} -``` - -- [ ] **Step 5: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core ensure_owner create_owner get_user compute_role 2>&1 | tail -5 -``` - -Expected: all 4 new tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-core/src/database.rs -git commit -m "feat(auth): user and role DB helpers (create_owner, compute_role_bits)" -``` - ---- - -## Task 4: Session and token DB helpers - -**Files:** -- Modify: `crates/archivr-core/src/database.rs` - -- [ ] **Step 1: Write failing session tests** - -```rust -#[test] -fn create_and_get_session() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - let uid = create_session(&conn, user_id, 15, None).unwrap(); - let sess = get_session(&conn, &uid).unwrap().unwrap(); - assert_eq!(sess.user_id, user_id); - assert_eq!(sess.role_bits, 15); -} - -#[test] -fn get_session_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_session(&conn, "nonexistent").unwrap().is_none()); -} - -#[test] -fn delete_session_removes_it() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - let uid = create_session(&conn, user_id, 15, None).unwrap(); - delete_session(&conn, &uid).unwrap(); - assert!(get_session(&conn, &uid).unwrap().is_none()); -} - -#[test] -fn token_hash_round_trips() { - let conn = make_auth_conn(); - let user_id = create_owner(&conn, "alice", "pw").unwrap(); - create_api_token(&conn, user_id, "hash_abc", "My Token").unwrap(); - let found_id = get_user_for_token(&conn, "hash_abc").unwrap(); - assert_eq!(found_id, Some(user_id)); -} - -#[test] -fn get_user_for_token_returns_none_for_unknown() { - let conn = make_auth_conn(); - assert!(get_user_for_token(&conn, "unknown").unwrap().is_none()); -} -``` - -- [ ] **Step 2: Run to confirm failure** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core create_and_get_session token_hash 2>&1 | tail -5 -``` - -- [ ] **Step 3: Implement session helpers** - -After `compute_role_bits`, add: - -```rust -/// Returns a new session_uid (UUID). -pub fn create_session( - conn: &Connection, - user_id: i64, - role_bits: u32, - user_agent: Option<&str>, -) -> Result { - let session_uid = public_id("sess"); - let now = now_timestamp(); - // expires_at = 30 days from now (approximate via string arithmetic is fragile; - // compute with chrono instead) - let expires_at = chrono::Utc::now() - .checked_add_signed(chrono::Duration::days(30)) - .unwrap() - .format("%Y-%m-%dT%H-%M-%S%.3f") - .to_string(); - conn.execute( - "INSERT INTO sessions (session_uid, user_id, role_bits, created_at, last_seen_at, expires_at, user_agent) - VALUES (?1, ?2, ?3, ?4, ?4, ?5, ?6)", - params![session_uid, user_id, role_bits as i64, now, expires_at, user_agent], - )?; - Ok(session_uid) -} - -/// Returns session if it exists, the user is active, and it has not expired. -pub fn get_session(conn: &Connection, session_uid: &str) -> Result> { - let now = now_timestamp(); - conn.query_row( - "SELECT s.user_id, s.role_bits, s.last_seen_at, s.session_uid - FROM sessions s - JOIN users u ON u.id = s.user_id - WHERE s.session_uid = ?1 - AND u.status = 'active' - AND s.expires_at > ?2", - params![session_uid, now], - |row| { - Ok(SessionRecord { - user_id: row.get(0)?, - role_bits: row.get::<_, i64>(1)? as u32, - last_seen_at: row.get(2)?, - session_uid: row.get(3)?, - }) - }, - ) - .optional() - .map_err(Into::into) -} - -pub fn delete_session(conn: &Connection, session_uid: &str) -> Result<()> { - conn.execute("DELETE FROM sessions WHERE session_uid = ?1", [session_uid])?; - Ok(()) -} - -/// Updates last_seen_at and extends expires_at by 30 days. -pub fn touch_session(conn: &Connection, session_uid: &str) -> Result<()> { - let now = now_timestamp(); - let new_expires = chrono::Utc::now() - .checked_add_signed(chrono::Duration::days(30)) - .unwrap() - .format("%Y-%m-%dT%H-%M-%S%.3f") - .to_string(); - conn.execute( - "UPDATE sessions SET last_seen_at = ?1, expires_at = ?2 WHERE session_uid = ?3", - params![now, new_expires, session_uid], - )?; - Ok(()) -} - -pub fn delete_expired_sessions(conn: &Connection) -> Result { - let now = now_timestamp(); - let n = conn.execute("DELETE FROM sessions WHERE expires_at <= ?1", [now])?; - Ok(n) -} -``` - -- [ ] **Step 4: Implement token helpers** - -```rust -/// Creates an API token. `token_hash` is SHA3-256 hex of the raw token. -/// Returns the token_uid. -pub fn create_api_token( - conn: &Connection, - user_id: i64, - token_hash: &str, - name: &str, -) -> Result { - let token_uid = public_id("tok"); - conn.execute( - "INSERT INTO api_tokens (token_uid, user_id, token_hash, name, created_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![token_uid, user_id, token_hash, name, now_timestamp()], - )?; - Ok(token_uid) -} - -/// Returns the user_id for a given token hash, if the token is valid and user is active. -pub fn get_user_for_token(conn: &Connection, token_hash: &str) -> Result> { - let now = now_timestamp(); - conn.query_row( - "SELECT t.user_id FROM api_tokens t - JOIN users u ON u.id = t.user_id - WHERE t.token_hash = ?1 - AND u.status = 'active' - AND (t.expires_at IS NULL OR t.expires_at > ?2)", - params![token_hash, now], - |row| row.get(0), - ) - .optional() - .map_err(Into::into) -} - -pub fn touch_token(conn: &Connection, token_uid: &str) -> Result<()> { - conn.execute( - "UPDATE api_tokens SET last_used_at = ?1 WHERE token_uid = ?2", - params![now_timestamp(), token_uid], - )?; - Ok(()) -} - -/// Returns true if the token was found and deleted (user_id must match). -pub fn delete_api_token(conn: &Connection, token_uid: &str, user_id: i64) -> Result { - let n = conn.execute( - "DELETE FROM api_tokens WHERE token_uid = ?1 AND user_id = ?2", - params![token_uid, user_id], - )?; - Ok(n > 0) -} - -pub fn list_user_tokens(conn: &Connection, user_id: i64) -> Result> { - let mut stmt = conn.prepare( - "SELECT token_uid, name, created_at, last_used_at - FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC", - )?; - let records = stmt - .query_map([user_id], |row| { - Ok(ApiTokenRecord { - token_uid: row.get(0)?, - name: row.get(1)?, - created_at: row.get(2)?, - last_used_at: row.get(3)?, - }) - })? - .collect::, _>>()?; - Ok(records) -} -``` - -- [ ] **Step 5: Run all new tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-core 2>&1 | tail -5 -``` - -Expected: all tests pass (including all previously passing ones). - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-core/src/database.rs -git commit -m "feat(auth): session and token DB helpers" -``` - ---- - -## Task 5: Auth DB path in AppState, registry, and main.rs - -**Files:** -- Modify: `crates/archivr-server/src/registry.rs` -- Modify: `crates/archivr-server/src/routes.rs` -- Modify: `crates/archivr-server/src/main.rs` - -- [ ] **Step 1: Add `auth_db_path` to `ServerRegistry`** - -In `registry.rs`, update the `ServerRegistry` struct: - -```rust -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct ServerRegistry { - #[serde(default)] - pub archives: Vec, - /// Optional bind address. Defaults to `127.0.0.1:8080`. - #[serde(default)] - pub bind: Option, - /// Path to the server-level auth database. - /// Defaults to `archivr-auth.sqlite` in the same directory as the config file. - #[serde(default)] - pub auth_db_path: Option, -} -``` - -- [ ] **Step 2: Update `AppState` in `routes.rs`** - -Change `AppState`: - -```rust -#[derive(Clone)] -pub struct AppState { - registry: Arc, - pub auth_db_path: Arc, -} -``` - -Update `app()` signature and body: - -```rust -pub fn app(registry: ServerRegistry, auth_db_path: std::path::PathBuf) -> Router { - let state = AppState { - registry: Arc::new(registry), - auth_db_path: Arc::new(auth_db_path), - }; - // ... rest unchanged -``` - -- [ ] **Step 3: Update all tests in `routes.rs` that call `app(registry)`** - -Every `app(registry)` in the test module must become `app(registry, std::path::PathBuf::from("/tmp/test-auth.sqlite"))`. - -Search for all occurrences: - -```bash -grep -n "app(registry" ~/personal/archivr/crates/archivr-server/src/routes.rs | head -20 -``` - -Update each one to `app(registry, tempfile::tempdir().unwrap().path().join("auth.sqlite"))`. Add `use tempfile;` if not present. (The `tempfile` crate is already a workspace dependency.) - -- [ ] **Step 4: Update `main.rs`** - -```rust -mod registry; -mod routes; - -use anyhow::{Context, Result}; -use std::{net::SocketAddr, path::PathBuf}; - -const DEFAULT_BIND: &str = "127.0.0.1:8080"; - -#[tokio::main] -async fn main() -> Result<()> { - let config_path = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("archivr-server.toml")); - - let registry = registry::load_registry(&config_path)?; - - // Auth DB lives next to the config file unless overridden. - let auth_db_path = registry.auth_db_path.clone().unwrap_or_else(|| { - config_path - .parent() - .unwrap_or_else(|| std::path::Path::new(".")) - .join("archivr-auth.sqlite") - }); - - let app = routes::app(registry.clone(), auth_db_path.clone()); - - let bind_str = std::env::var("ARCHIVR_BIND") - .ok() - .or_else(|| registry.bind.clone()) - .unwrap_or_else(|| DEFAULT_BIND.to_string()); - - let addr: SocketAddr = bind_str - .parse() - .with_context(|| format!("invalid bind address: {bind_str}"))?; - - let listener = tokio::net::TcpListener::bind(addr).await?; - println!("archivr-server listening on http://{addr}"); - axum::serve(listener, app).await?; - Ok(()) -} -``` - -- [ ] **Step 5: Run full test suite** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-server/src/routes.rs \ - crates/archivr-server/src/registry.rs \ - crates/archivr-server/src/main.rs -git commit -m "feat(auth): add auth_db_path to AppState, registry, and main.rs" -``` - ---- - -## Task 6: Create `auth.rs` — AuthUser extractor - -**Files:** -- Create: `crates/archivr-server/src/auth.rs` -- Modify: `crates/archivr-server/src/routes.rs` (add `mod auth; use auth::AuthUser;`) - -- [ ] **Step 1: Create `auth.rs`** - -Create `crates/archivr-server/src/auth.rs` with the following content: - -```rust -use anyhow::Result; -use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; -use argon2::password_hash::{SaltString, rand_core::OsRng}; -use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; -use axum_extra::extract::CookieJar; -use rand::RngCore; - -use crate::routes::{ApiError, AppState}; -use archivr_core::database; - -// ── Role bit constants ──────────────────────────────────────────────────────── -pub const ROLE_GUEST: u32 = 1; // bit 0 -pub const ROLE_USER: u32 = 2; // bit 1 -pub const ROLE_ADMIN: u32 = 4; // bit 2 -pub const ROLE_OWNER: u32 = 8; // bit 3 - -// ── AuthUser ───────────────────────────────────────────────────────────────── -#[derive(Clone, Debug)] -pub enum AuthUser { - Guest, - Authenticated { user_id: i64, role_bits: u32 }, -} - -impl AuthUser { - /// Returns (user_id, role_bits) or 401 if Guest. - pub fn require_auth(&self) -> Result<(i64, u32), ApiError> { - match self { - AuthUser::Authenticated { user_id, role_bits } => Ok((*user_id, *role_bits)), - AuthUser::Guest => Err(ApiError::unauthorized("login required")), - } - } - - /// Returns Ok(()) if the user has the given role bit set, else 401/403. - pub fn require_role(&self, bit: u32) -> Result<(), ApiError> { - match self { - AuthUser::Authenticated { role_bits, .. } if role_bits & bit != 0 => Ok(()), - AuthUser::Authenticated { .. } => Err(ApiError::forbidden("insufficient permissions")), - AuthUser::Guest => Err(ApiError::unauthorized("login required")), - } - } - - pub fn has_role(&self, bit: u32) -> bool { - matches!(self, AuthUser::Authenticated { role_bits, .. } if role_bits & bit != 0) - } -} - -#[async_trait] -impl FromRequestParts for AuthUser { - type Rejection = std::convert::Infallible; - - async fn from_request_parts( - parts: &mut Parts, - state: &AppState, - ) -> Result { - let auth_db_path = state.auth_db_path.as_ref(); - - // 1. Try session cookie - let jar = CookieJar::from_headers(&parts.headers); - if let Some(cookie) = jar.get("session") { - let session_uid = cookie.value().to_string(); - if let Ok(conn) = database::open_auth_db(auth_db_path) { - if let Ok(Some(session)) = database::get_session(&conn, &session_uid) { - // Conditional touch: only update last_seen_at if more than 60s have elapsed. - // The session row is already in memory, so no extra query needed. - let should_touch = chrono::NaiveDateTime::parse_from_str( - &session.last_seen_at, "%Y-%m-%dT%H-%M-%S%.3f", - ) - .map(|last| { - let last_utc = chrono::DateTime::::from_naive_utc_and_offset( - last, chrono::Utc, - ); - chrono::Utc::now() - last_utc > chrono::Duration::seconds(60) - }) - .unwrap_or(true); // if parse fails, touch anyway - if should_touch { - let _ = database::touch_session(&conn, &session_uid); - } - return Ok(AuthUser::Authenticated { - user_id: session.user_id, - role_bits: session.role_bits, - }); - } - } - } - - // 2. Try Bearer token - if let Some(auth_header) = parts.headers.get("Authorization") { - if let Ok(header_str) = auth_header.to_str() { - if let Some(raw_token) = header_str.strip_prefix("Bearer ") { - let token_hash = hash_token(raw_token); - if let Ok(conn) = database::open_auth_db(auth_db_path) { - if let Ok(Some(user_id)) = database::get_user_for_token(&conn, &token_hash) { - // Get token_uid for touch (find by hash) - // Compute role_bits live for tokens - if let Ok(role_bits) = database::compute_role_bits(&conn, user_id) { - return Ok(AuthUser::Authenticated { user_id, role_bits }); - } - } - } - } - } - } - - Ok(AuthUser::Guest) - } -} - -// ── Password helpers ────────────────────────────────────────────────────────── - -pub fn hash_password(password: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); - let hash = Argon2::default() - .hash_password(password.as_bytes(), &salt) - .map_err(|e| anyhow::anyhow!("password hashing failed: {e}"))? - .to_string(); - Ok(hash) -} - -pub fn verify_password(password: &str, hash: &str) -> Result { - let parsed = PasswordHash::new(hash) - .map_err(|e| anyhow::anyhow!("invalid password hash: {e}"))?; - Ok(Argon2::default() - .verify_password(password.as_bytes(), &parsed) - .is_ok()) -} - -// ── Token helpers ───────────────────────────────────────────────────────────── - -/// Generates a cryptographically random 32-byte token, base64url-encoded. -pub fn generate_token() -> String { - let mut bytes = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut bytes); - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bytes) -} - -/// SHA3-256 hex hash of a raw token string. Used for storage and lookup. -pub fn hash_token(raw: &str) -> String { - archivr_core::hash::hash_bytes(raw.as_bytes()) -} -``` - -- [ ] **Step 2: Wire into `routes.rs`** - -At the top of `routes.rs`, add after `mod` declarations: - -```rust -mod auth; -pub use auth::{AuthUser, ROLE_ADMIN, ROLE_OWNER, ROLE_USER}; -``` - -Also add `unauthorized` and `forbidden` constructors to `ApiError`, and update `IntoResponse` to return JSON: - -```rust -impl ApiError { - // ... existing constructors ... - - pub fn unauthorized(message: &str) -> Self { - Self { status: StatusCode::UNAUTHORIZED, message: message.to_string() } - } - - pub fn forbidden(message: &str) -> Self { - Self { status: StatusCode::FORBIDDEN, message: message.to_string() } - } -} - -impl IntoResponse for ApiError { - fn into_response(self) -> Response { - let body = serde_json::json!({ "error": self.message }); - (self.status, axum::Json(body)).into_response() - } -} -``` - -- [ ] **Step 3: Compile check** - -```bash -cd ~/personal/archivr && cargo check -p archivr-server 2>&1 | tail -10 -``` - -Fix any import errors. Common fix: add `use std::path::Path;` or check `axum_extra` cookie import. - -- [ ] **Step 4: Write extractor tests in `routes.rs`** - -In the `#[cfg(test)]` block in `routes.rs`, add helpers and tests: - -```rust -fn make_test_app() -> (Router, tempfile::TempDir) { - let dir = tempfile::tempdir().unwrap(); - let auth_db_path = dir.path().join("auth.sqlite"); - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - (app(registry, auth_db_path), dir) -} - -#[tokio::test] -async fn health_check_returns_ok() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); -} -``` - -Update the existing `archives_endpoint_lists_mounted_archives` test to use `make_test_app()`: - -```rust -#[tokio::test] -async fn archives_endpoint_lists_mounted_archives() { - let dir = tempfile::tempdir().unwrap(); - let auth_db_path = dir.path().join("auth.sqlite"); - let registry = ServerRegistry { - archives: vec![MountedArchive { - id: "personal".to_string(), - label: "Personal".to_string(), - archive_path: std::path::PathBuf::from("/tmp/personal/.archivr"), - }], - bind: None, - auth_db_path: None, - }; - let response = app(registry, auth_db_path) - .oneshot(Request::builder().uri("/api/archives").body(Body::empty()).unwrap()) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); -} -``` - -Apply the same pattern to **every other test** in the module that calls `app(registry)`. - -- [ ] **Step 5: Run all tests** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-server/src/auth.rs crates/archivr-server/src/routes.rs -git commit -m "feat(auth): AuthUser extractor, password helpers, token generation" -``` - ---- - -## Task 7: Auth endpoints — login, logout, /me, setup - -**Files:** -- Modify: `crates/archivr-server/src/routes.rs` - -Add the following to `routes.rs`. - -- [ ] **Step 1: Write failing tests for auth endpoints** - -```rust -#[tokio::test] -async fn setup_required_before_owner_created() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot( - Request::builder() - .uri("/api/auth/setup") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); - let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(json["setup_required"], true); -} - -#[tokio::test] -async fn login_with_wrong_password_returns_401() { - let dir = tempfile::tempdir().unwrap(); - let auth_db_path = dir.path().join("auth.sqlite"); - // Seed owner - { - let conn = archivr_core::database::open_auth_db(&auth_db_path).unwrap(); - let hash = crate::auth::hash_password("correct").unwrap(); - archivr_core::database::create_owner(&conn, "owner", &hash).unwrap(); - } - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - let response = app(registry, auth_db_path) - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/login") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner","password":"wrong"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); -} - -#[tokio::test] -async fn setup_post_creates_owner_and_returns_409_on_repeat() { - let (app_once, dir) = make_test_app(); - let auth_db_path = dir.path().join("auth.sqlite"); - let registry = ServerRegistry { archives: vec![], bind: None, auth_db_path: None }; - - // First POST creates the owner - let response = app_once - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/setup") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner","password":"hunter2"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::CREATED); - - // Second POST must return 409 - let app2 = app(registry, auth_db_path); - let response2 = app2 - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/setup") - .header("content-type", "application/json") - .body(Body::from(r#"{"username":"owner2","password":"hunter2"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response2.status(), StatusCode::CONFLICT); -} -``` - -- [ ] **Step 2: Run to confirm failure** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server setup_required login_with_wrong 2>&1 | tail -10 -``` - -Expected: FAILED. - -- [ ] **Step 3: Add auth routes to `app()` in `routes.rs`** - -In the `app()` function, add these routes before `.with_state(state)`: - -```rust -.route("/api/auth/setup", get(auth_setup_status).post(auth_setup)) -.route("/api/auth/login", post(auth_login)) -.route("/api/auth/logout", post(auth_logout)) -.route("/api/auth/me", get(auth_me)) -// Setup guard must come AFTER routes are defined and BEFORE .with_state -.layer(axum::middleware::from_fn_with_state(state.clone(), setup_guard)) -``` - -- [ ] **Step 3b: Implement `setup_guard` middleware** - -Add this function before the `app()` definition: - -```rust -/// Tower middleware: returns 503 on all non-exempt routes if setup hasn't been completed. -async fn setup_guard( - State(state): State, - req: axum::extract::Request, - next: axum::middleware::Next, -) -> Response { - let path = req.uri().path(); - let exempt = path == "/api/auth/setup" - || path == "/api/auth/login" - || path.starts_with("/assets") - || path == "/" - || path == "/health"; - if !exempt { - if let Ok(conn) = database::open_auth_db(&state.auth_db_path) { - if matches!(database::ensure_owner_exists(&conn), Ok(false)) { - return ( - StatusCode::SERVICE_UNAVAILABLE, - axum::Json(serde_json::json!({ "error": "setup_required" })), - ) - .into_response(); - } - } - } - next.run(req).await -} -``` - -- [ ] **Step 4: Add request/response types** - -After the existing `#[derive(serde::Deserialize)]` structs, add: - -```rust -#[derive(Debug, serde::Deserialize)] -struct LoginBody { - username: String, - password: String, -} - -#[derive(Debug, serde::Deserialize)] -struct SetupBody { - username: String, - password: String, -} -``` - -- [ ] **Step 5: Implement handler functions** - -Add after the existing handlers: - -```rust -async fn auth_setup_status( - State(state): State, -) -> Result, ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - let required = !database::ensure_owner_exists(&conn)?; - Ok(Json(serde_json::json!({ "setup_required": required }))) -} - -async fn auth_setup( - State(state): State, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - if database::ensure_owner_exists(&conn)? { - return Err(ApiError { - status: StatusCode::CONFLICT, - message: "already_configured".to_string(), - }); - } - if body.username.trim().is_empty() || body.password.len() < 8 { - return Err(ApiError::bad_request("username required and password must be at least 8 characters")); - } - let hash = auth::hash_password(&body.password).map_err(ApiError::from)?; - let user_id = database::create_owner(&conn, &body.username, &hash)?; - let user = database::get_user_by_username(&conn, &body.username)? - .ok_or_else(|| ApiError::internal("user not found after creation"))?; - Ok((StatusCode::CREATED, Json(serde_json::json!({ - "user_uid": user.user_uid, - "username": user.username, - })))) -} - -async fn auth_login( - State(state): State, - headers: axum::http::HeaderMap, - Json(body): Json, -) -> Result<(StatusCode, axum::http::HeaderMap, Json), ApiError> { - let conn = database::open_auth_db(&state.auth_db_path)?; - let user = database::get_user_by_username(&conn, &body.username)? - .filter(|u| u.status == "active") - .ok_or_else(|| ApiError::unauthorized("invalid_credentials"))?; - if !auth::verify_password(&body.password, &user.password_hash) - .map_err(ApiError::from)? - { - return Err(ApiError::unauthorized("invalid_credentials")); - } - let role_bits = database::compute_role_bits(&conn, user.id)?; - let user_agent = headers - .get("user-agent") - .and_then(|v| v.to_str().ok()); - let session_uid = database::create_session(&conn, user.id, role_bits, user_agent)?; - - // Build Set-Cookie header - let secure = headers - .get("x-forwarded-proto") - .and_then(|v| v.to_str().ok()) - .map(|v| v == "https") - .unwrap_or(false); - let cookie_value = format!( - "session={}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000{}", - session_uid, - if secure { "; Secure" } else { "" } - ); - let mut resp_headers = axum::http::HeaderMap::new(); - resp_headers.insert( - axum::http::header::SET_COOKIE, - cookie_value.parse().map_err(|_| ApiError::internal("cookie serialization failed"))?, - ); - - Ok((StatusCode::OK, resp_headers, Json(serde_json::json!({ - "user_uid": user.user_uid, - "username": user.username, - "role_bits": role_bits, - })))) -} - -async fn auth_logout( - State(state): State, - jar: CookieJar, -) -> Result<(StatusCode, axum::http::HeaderMap), ApiError> { - if let Some(cookie) = jar.get("session") { - let conn = database::open_auth_db(&state.auth_db_path)?; - database::delete_session(&conn, cookie.value())?; - } - let mut resp_headers = axum::http::HeaderMap::new(); - resp_headers.insert( - axum::http::header::SET_COOKIE, - "session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0" - .parse() - .unwrap(), - ); - Ok((StatusCode::NO_CONTENT, resp_headers)) -} - -async fn auth_me( - State(state): State, - auth_user: AuthUser, -) -> Result, ApiError> { - let (user_id, role_bits) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - // Look up username by user_id for response - let username: String = conn - .query_row("SELECT username FROM users WHERE id = ?1", [user_id], |r| r.get(0)) - .map_err(ApiError::from)?; - Ok(Json(serde_json::json!({ - "role_bits": role_bits, - "username": username, - }))) -} -``` - -Add `use axum_extra::extract::CookieJar;` at the top of `routes.rs` imports. - -- [ ] **Step 6: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server 2>&1 | tail -5 -``` - -Expected: all tests pass. - -- [ ] **Step 7: Commit** - -```bash -git add crates/archivr-server/src/routes.rs -git commit -m "feat(auth): login, logout, /me, setup endpoints" -``` - ---- - -## Task 8: API token endpoints - -**Files:** -- Modify: `crates/archivr-server/src/routes.rs` - -- [ ] **Step 1: Write failing test** - -```rust -#[tokio::test] -async fn create_token_requires_auth() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/api/auth/tokens") - .header("content-type", "application/json") - .body(Body::from(r#"{"name":"my token"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); -} -``` - -- [ ] **Step 2: Add token routes to `app()`** - -```rust -.route("/api/auth/tokens", get(list_tokens).post(create_token)) -.route("/api/auth/tokens/:token_uid", delete(delete_token)) -``` - -- [ ] **Step 3: Add request type** - -```rust -#[derive(Debug, serde::Deserialize)] -struct CreateTokenBody { - name: String, -} -``` - -- [ ] **Step 4: Implement token handlers** - -```rust -async fn create_token( - State(state): State, - auth_user: AuthUser, - Json(body): Json, -) -> Result<(StatusCode, Json), ApiError> { - let (user_id, _) = auth_user.require_auth()?; - if body.name.trim().is_empty() { - return Err(ApiError::bad_request("token name is required")); - } - let raw_token = auth::generate_token(); - let token_hash = auth::hash_token(&raw_token); - let conn = database::open_auth_db(&state.auth_db_path)?; - let token_uid = database::create_api_token(&conn, user_id, &token_hash, &body.name)?; - Ok((StatusCode::CREATED, Json(serde_json::json!({ - "token_uid": token_uid, - "raw_token": raw_token, - "name": body.name, - })))) -} - -async fn list_tokens( - State(state): State, - auth_user: AuthUser, -) -> Result>, ApiError> { - let (user_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - Ok(Json(database::list_user_tokens(&conn, user_id)?)) -} - -async fn delete_token( - State(state): State, - auth_user: AuthUser, - Path(token_uid): Path, -) -> Result { - let (user_id, _) = auth_user.require_auth()?; - let conn = database::open_auth_db(&state.auth_db_path)?; - if database::delete_api_token(&conn, &token_uid, user_id)? { - Ok(StatusCode::NO_CONTENT) - } else { - Err(ApiError::not_found("token not found")) - } -} -``` - -- [ ] **Step 5: Derive `serde::Serialize` on `ApiTokenRecord`** - -In `database.rs`, update the struct: - -```rust -#[derive(Debug, Clone, serde::Serialize)] -pub struct ApiTokenRecord { ... } -``` - -- [ ] **Step 6: Run tests** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server 2>&1 | tail -5 -``` - -Expected: all pass including `create_token_requires_auth`. - -- [ ] **Step 7: Commit** - -```bash -git add crates/archivr-server/src/routes.rs crates/archivr-core/src/database.rs -git commit -m "feat(auth): API token endpoints (create, list, delete)" -``` - ---- - -## Task 9: Route protection for existing routes - -**Files:** -- Modify: `crates/archivr-server/src/routes.rs` - -- [ ] **Step 1: Write failing tests** - -```rust -#[tokio::test] -async fn capture_returns_401_for_unauthenticated() { - let (app, _dir) = make_test_app(); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/api/archives/test/captures") - .header("content-type", "application/json") - .body(Body::from(r#"{"locator":"https://example.com"}"#)) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); -} -``` - -- [ ] **Step 2: Run to confirm failure** - -```bash -cd ~/personal/archivr && cargo test -p archivr-server capture_returns_401 2>&1 | tail -5 -``` - -Expected: FAILED (currently returns 200 or 404). - -- [ ] **Step 3: Add `AuthUser` to WRITE handlers** - -Update `capture_handler` to require auth: - -```rust -async fn capture_handler( - State(state): State, - auth_user: AuthUser, - Path(archive_id): Path, - Json(body): Json, -) -> Result, ApiError> { - auth_user.require_role(ROLE_USER)?; - // ... rest unchanged -} -``` - -Update `create_tag_handler`, `assign_entry_tag_handler`, `remove_entry_tag_handler` with the same `auth_user: AuthUser` parameter and `auth_user.require_role(ROLE_USER)?` guard. - -- [ ] **Step 4: Update security-boundary comment in `routes.rs`** - -Find the comment block at the top of `routes.rs` (lines 1–23). Replace the route classification list to reflect the new protection: - -```rust -// ── Security Boundary ────────────────────────────────────────────────────── -// -// Route protection tiers: -// STATIC — no auth: GET /, GET /assets/* -// PUBLIC_READ — no auth (visibility filtering deferred to Track 6): -// GET /api/archives, GET /api/archives/:id/entries, etc. -// AUTH — requires login (ROLE_USER bit): -// POST /api/archives/:id/captures -// POST/PUT/DELETE /api/archives/:id/tags -// POST/DELETE /api/archives/:id/entries/:uid/tags -// ADMIN — requires ROLE_ADMIN: -// GET /api/admin/archives (future) -// OWNER — requires ROLE_OWNER: -// instance settings (future) -// AUTH_SELF — no role guard (own resources): -// GET/POST/DELETE /api/auth/tokens -// POST /api/auth/logout -// GET /api/auth/me -// ──────────────────────────────────────────────────────────────────────────── -``` - -- [ ] **Step 5: Run all tests** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all pass. - -- [ ] **Step 6: Commit** - -```bash -git add crates/archivr-server/src/routes.rs -git commit -m "feat(auth): apply ROLE_USER guard to WRITE routes" -``` - ---- - -## Task 10: Session cleanup background task - -**Files:** -- Modify: `crates/archivr-server/src/main.rs` - -- [ ] **Step 1: Add cleanup task to `main.rs`** - -After the `app` binding and before `let listener`, add: - -```rust -// Spawn session cleanup background task: delete expired sessions at startup -// and every 24 hours. -let cleanup_auth_path = auth_db_path.clone(); -tokio::spawn(async move { - loop { - if let Ok(conn) = archivr_core::database::open_auth_db(&cleanup_auth_path) { - match archivr_core::database::delete_expired_sessions(&conn) { - Ok(n) if n > 0 => eprintln!("info: cleaned up {n} expired sessions"), - Err(e) => eprintln!("warn: session cleanup failed: {e:#}"), - _ => {} - } - } - tokio::time::sleep(tokio::time::Duration::from_secs(24 * 60 * 60)).await; - } -}); -``` - -Add `use archivr_core;` at the top if not already present (it's brought in transitively via routes). - -- [ ] **Step 2: Verify compilation** - -```bash -cd ~/personal/archivr && cargo check -p archivr-server 2>&1 | tail -5 -``` - -Expected: compiles cleanly. - -- [ ] **Step 3: Commit** - -```bash -git add crates/archivr-server/src/main.rs -git commit -m "feat(auth): session cleanup background task (24h interval)" -``` - ---- - -## Task 11: Frontend — auth state, api.js, LoginPage, SetupPage - -**Files:** -- Modify: `frontend/src/App.jsx` -- Modify: `frontend/src/api.js` -- Create: `frontend/src/components/LoginPage.jsx` -- Create: `frontend/src/components/SetupPage.jsx` - -- [ ] **Step 1: Update `api.js`** - -Open `frontend/src/api.js`. Add auth helper functions and the 401 interceptor at the bottom of the file (replace or append to the existing `export` structure): - -```js -// ── Auth helpers ───────────────────────────────────────────────────────────── - -export async function checkSetup() { - const r = await fetch('/api/auth/setup'); - const data = await r.json(); - return data.setup_required === true; -} - -export async function doSetup(username, password) { - const r = await fetch('/api/auth/setup', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - if (!r.ok) throw new Error((await r.json()).error || 'Setup failed'); - return r.json(); -} - -export async function login(username, password) { - const r = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - if (!r.ok) throw new Error((await r.json()).error || 'Login failed'); - return r.json(); // { user_uid, username, role_bits } -} - -export async function logout() { - await fetch('/api/auth/logout', { method: 'POST' }); -} - -export async function fetchMe() { - const r = await fetch('/api/auth/me'); - if (r.status === 401) return null; - return r.json(); -} - -// ── 401 interceptor ─────────────────────────────────────────────────────────── -// Wrap fetch so any 401 dispatches a custom event for App.jsx to handle. -const _origFetch = window.fetch; -window.fetch = async (...args) => { - const r = await _origFetch(...args); - if (r.status === 401) { - const url = typeof args[0] === 'string' ? args[0] : args[0]?.url ?? ''; - // Don't intercept auth endpoints themselves - if (!url.includes('/api/auth/')) { - window.dispatchEvent(new CustomEvent('auth:expired')); - } - } - return r; -}; -``` - -- [ ] **Step 2: Create `LoginPage.jsx`** - -```jsx -import { useState } from 'react'; -import { login } from '../api.js'; - -export default function LoginPage({ onLogin }) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function handleSubmit(e) { - e.preventDefault(); - setError(null); - setLoading(true); - try { - const user = await login(username, password); - onLogin(user); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - } - - return ( -
-

Archivr

-
- - - {error &&

{error}

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

Welcome to Archivr

-

Create your owner account to get started.

-
- - - - {error &&

{error}

} - -
-
- ); -} -``` - -- [ ] **Step 4: Update `App.jsx`** - -Open `frontend/src/App.jsx`. Add the following at the top (after existing imports): - -```jsx -import { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { checkSetup, fetchMe, logout as apiLogout } from './api.js'; -import LoginPage from './components/LoginPage.jsx'; -import SetupPage from './components/SetupPage.jsx'; - -export const AuthContext = createContext(null); -``` - -Inside the `App` function, add state and effects at the top: - -```jsx -const [authState, setAuthState] = useState('loading'); // 'loading' | 'setup' | 'login' | 'authenticated' -const [currentUser, setCurrentUser] = useState(null); - -useEffect(() => { - (async () => { - const needsSetup = await checkSetup(); - if (needsSetup) { setAuthState('setup'); return; } - const user = await fetchMe(); - if (!user) { setAuthState('login'); return; } - setCurrentUser(user); - setAuthState('authenticated'); - })(); -}, []); - -// Listen for 401s from the fetch interceptor in api.js -useEffect(() => { - const handler = () => { setCurrentUser(null); setAuthState('login'); }; - window.addEventListener('auth:expired', handler); - return () => window.removeEventListener('auth:expired', handler); -}, []); -``` - -In the `App` return, add guards at the very top of the JSX: - -```jsx -if (authState === 'loading') return
Loading…
; -if (authState === 'setup') return setAuthState('login')} />; -if (authState === 'login') return { setCurrentUser(user); setAuthState('authenticated'); }} />; -``` - -Wrap the existing return content in: - -```jsx -return ( - - {/* existing app JSX */} - -); -``` - -- [ ] **Step 5: Build to check for errors** - -```bash -cd ~/personal/archivr/frontend && bun run build 2>&1 | tail -10 -``` - -Expected: builds successfully, no type errors. - -- [ ] **Step 6: Commit** - -```bash -cd ~/personal/archivr -git add frontend/src/App.jsx frontend/src/api.js \ - frontend/src/components/LoginPage.jsx \ - frontend/src/components/SetupPage.jsx -git commit -m "feat(auth): frontend login/setup pages + auth state in App.jsx" -``` - ---- - -## Task 12: Topbar — user menu and logout button - -**Files:** -- Modify: `frontend/src/components/Topbar.jsx` - -- [ ] **Step 1: Update `Topbar.jsx`** - -Open `frontend/src/components/Topbar.jsx`. Import `AuthContext` and `logout`: - -```jsx -import { useContext, useState } from 'react'; -import { AuthContext } from '../App.jsx'; -import { logout as apiLogout } from '../api.js'; -``` - -Inside the component, add: - -```jsx -const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}; -const [loggingOut, setLoggingOut] = useState(false); - -async function handleLogout() { - setLoggingOut(true); - await apiLogout(); - setCurrentUser(null); - window.location.reload(); // simplest way to reset all state -} -``` - -In the JSX, add a user menu at the end of the topbar: - -```jsx -{currentUser && ( -
- {currentUser.username} - -
-)} -``` - -- [ ] **Step 2: Build to check for errors** - -```bash -cd ~/personal/archivr/frontend && bun run build 2>&1 | tail -5 -``` - -Expected: clean build. - -- [ ] **Step 3: Commit** - -```bash -cd ~/personal/archivr -git add frontend/src/components/Topbar.jsx -git commit -m "feat(auth): user menu and logout button in Topbar" -``` - ---- - -## Task 13: Update NEXT.md - -**Files:** -- Modify: `NEXT.md` - -- [ ] **Step 1: Add Track 4 as done, renumber remaining tracks** - -Open `NEXT.md`. Add a new Track 4 section (done) after Track 3, following the same ~~strikethrough~~ ✅ pattern as Tracks 1 and 2. Rename the old Track 4 (Cloud backup) to Track 9 and Track 5 (Cloud storage) to Track 10. Add stub sections for new Tracks 5, 6, 7, 8 with brief descriptions and "pending" status. - -Use this numbering: - -| # | Track | Status | -|---|---|---| -| 1 | Generic URL capture | Done | -| 2 | Web page archiving | Done | -| 3 | Async capture jobs | Pending | -| **4** | **Auth foundation** | **Done (this track)** | -| 5 | User management | Pending | -| 6 | Permissions & visibility | Pending | -| 7 | Settings | Pending | -| 8 | Collections UI | Pending | -| 9 | Cloud backup | Pending (was 4) | -| 10 | Cloud storage | Pending (was 5) | - -- [ ] **Step 2: Commit** - -```bash -git add NEXT.md -git commit -m "docs: mark Track 4 auth foundation done, renumber roadmap tracks" -``` - ---- - -## Final verification - -- [ ] **Run the full test suite** - -```bash -cd ~/personal/archivr && cargo test 2>&1 | tail -5 -``` - -Expected: all tests pass (count will be higher than the pre-implementation baseline due to new tests). - -- [ ] **Build the frontend** - -```bash -cd ~/personal/archivr/frontend && bun run build 2>&1 | tail -5 -``` - -Expected: builds cleanly. diff --git a/docs/superpowers/specs/2026-06-25-auth-foundation-design.md b/docs/superpowers/specs/2026-06-25-auth-foundation-design.md deleted file mode 100644 index 23e74c8..0000000 --- a/docs/superpowers/specs/2026-06-25-auth-foundation-design.md +++ /dev/null @@ -1,403 +0,0 @@ -# Auth Foundation Design - -**Track:** 4 of the roadmap (inserted after Track 3: Async capture jobs) -**Date:** 2026-06-25 -**Status:** Approved for implementation - ---- - -## Context & Roadmap Position - -Archivr is evolving from a local-only tool (single hard-coded user, 127.0.0.1 binding) into a -self-hosted multi-user platform — think ArchiveBox but with real accounts, roles, and -public/private visibility. This track lays the foundation. All subsequent tracks depend on it. - -**Full decomposition:** - -| Track | Scope | Depends on | -|---|---|---| -| 4 (this) | Auth foundation | — | -| 5 | User management — registration, custom roles, admin panel | Track 4 | -| 6 | Permissions & visibility — collection model, per-membership visibility | Track 5 | -| 7 | Settings — account profile, instance-wide toggles | Track 5 | -| 8 | Collections UI | Tracks 5–6 | - ---- - -## Goals - -- Password-protected login with cookie sessions and API tokens -- Role table with bitmask-based visibility (extensible to custom roles in Track 5) -- Auth middleware that protects write/admin routes -- First-run owner setup wizard -- Frontend login page and session-aware API calls - -## Non-Goals (explicitly deferred) - -- Custom role creation UI → Track 5 -- User registration flow → Track 5 -- Visibility enforcement on queries → Track 6 -- Collection model (replacing `archived_entries.visibility`) → Track 6 -- Account settings page → Track 7 -- API token management UI → Track 7 - ---- - -## Schema - -### New table: `roles` - -```sql -CREATE TABLE IF NOT EXISTS roles ( - id INTEGER PRIMARY KEY, - role_uid TEXT NOT NULL UNIQUE, - slug TEXT NOT NULL UNIQUE, -- 'guest', 'user', 'admin', 'owner', or custom - name TEXT NOT NULL, - level INTEGER NOT NULL, -- ordering: guest=0, user=1, admin=3, owner=4 - bit_position INTEGER NOT NULL UNIQUE, -- position in visibility bitmask - is_builtin INTEGER NOT NULL DEFAULT 0 CHECK (is_builtin IN (0, 1)) -); -``` - -**Built-in rows seeded at schema init:** - -| slug | level | bit_position | bit value | is_builtin | -|---|---|---|---|---| -| guest | 0 | 0 | 1 | 1 | -| user | 1 | 1 | 2 | 1 | -| admin | 3 | 2 | 4 | 1 | -| owner | 4 | 3 | 8 | 1 | - -Bit position 2 (value 4) is reserved for `admin`. Bit positions 4+ (values 16, 32, …) are assigned -to custom roles in Track 5. Level 2 is reserved for custom roles sitting between `user` and `admin`. - -**role_bits computation — implicit guest floor:** - -`role_bits` for any **authenticated** user is computed as: -``` -role_bits = ROLE_GUEST | (OR of bit values for all rows in user_roles) -``` -The `ROLE_GUEST` bit (1) is always included for authenticated users so they can access -public (guest-visible) content. Example: an owner assigned only the `owner` role gets -`role_bits = 1 | 8 = 9`, which passes `role_bits & ROLE_USER (2) = 0` — still broken. - -**Therefore, role assignment is cumulative by level.** When a role is assigned, all -built-in roles at lower levels are also assigned: -- Assigning `owner` (level 4) → also assign `admin`, `user` in `user_roles` -- Assigning `admin` (level 3) → also assign `user` in `user_roles` -- Assigning `user` (level 1) → no additional rows -- `guest` is never assigned; it is the implicit unauthenticated floor - -Setup creates owner with three `user_roles` rows: `user`, `admin`, `owner`. -Resulting `role_bits = ROLE_GUEST | ROLE_USER | ROLE_ADMIN | ROLE_OWNER = 1|2|4|8 = 15`. - -**Visibility check:** `viewer.role_bits & content.visibility != 0` passes if the viewer -has any bit the content requires. Owner (15) can see everything. User (1|2=3) can see -guest-visible (1) and user-visible (2) content but not admin-only (4). ✓ - -`is_builtin = 1` rows cannot be deleted. - -### New table: `sessions` - -```sql -CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY, - session_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - role_bits INTEGER NOT NULL, -- snapshot of bitmask at login time; role changes take effect on next login - created_at TEXT NOT NULL, - last_seen_at TEXT NOT NULL, - expires_at TEXT NOT NULL, -- 30 days from last_seen_at - user_agent TEXT -); -CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); -``` - -### New table: `api_tokens` - -```sql -CREATE TABLE IF NOT EXISTS api_tokens ( - id INTEGER PRIMARY KEY, - token_uid TEXT NOT NULL UNIQUE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - token_hash TEXT NOT NULL UNIQUE, -- SHA-256 of raw token; raw token never stored - name TEXT NOT NULL, - created_at TEXT NOT NULL, - last_used_at TEXT, - expires_at TEXT -- NULL = never expires -); -CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id ON api_tokens(user_id); -``` - -### `users` table — existing, minimally changed - -The existing `role TEXT NOT NULL CHECK (role IN ('admin','user'))` column is **kept but inert** — -auth middleware reads from `user_roles`, not this column. It will be removed in Track 5 cleanup. -`ensure_owner_exists` must supply a value for this column; use `'admin'` as the placeholder. - -`ensure_default_user` is replaced by `ensure_owner_exists` which returns `false` if no owner -row exists in `user_roles` (triggers setup mode). The old local-admin stub is never created on -fresh instances. Session lookup JOINs `users` and checks `users.status = 'active'`; a session -belonging to a disabled user resolves to `AuthUser::Guest`. - -### `instance_settings` — one new column - -The column is added **inside** the existing `CREATE TABLE IF NOT EXISTS instance_settings` DDL, -not via `ALTER TABLE` (which is not idempotent in `initialize_schema`): - -```sql -CREATE TABLE IF NOT EXISTS instance_settings ( - id INTEGER PRIMARY KEY CHECK (id = 1), - public_index_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_index_enabled IN (0, 1)), - public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)), - public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)), - default_entry_visibility INTEGER NOT NULL DEFAULT 2 -- 2 = user-visible by default -); -``` - -The existing `INSERT OR IGNORE INTO instance_settings … VALUES (1, 0, 0, 0)` seed row must be -updated to include the new column: `VALUES (1, 0, 0, 0, 2)`. - -### `archived_entries.visibility` — deprecated, not removed - -Flagged with a `-- DEPRECATED: replaced by collection_entries.visibility in Track 6` comment in -`initialize_schema`. No data migration needed yet; Track 6 handles it. - ---- - -## Auth Flow - -### Login - -``` -POST /api/auth/login -Body: { username: string, password: string } -``` - -1. Look up user by username. -2. Verify password with Argon2id (`argon2` crate). -3. Compute `role_bits = ROLE_GUEST | (OR of bit values for all user_roles rows)` (cumulative; see Schema § role_bits computation). -4. Insert `sessions` row (`session_uid` = UUID, `expires_at` = now + 30 days). -5. Set-Cookie: `session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000`. - Add `Secure` flag when the request arrived over HTTPS (detected via `X-Forwarded-Proto: https` - header or TLS connection info). Omit `Secure` for plain HTTP to support local dev without TLS. -6. Return `200 { user_uid, username, role_bits }`. - -On failure: `401 { error: "invalid_credentials" }` — same message for unknown user and wrong -password (no user enumeration). - -### Logout - -``` -POST /api/auth/logout -``` - -Deletes the `sessions` row for the current session cookie. Responds with -`Set-Cookie: session=; Max-Age=0` to clear the browser cookie. Returns `204`. - -### Current user - -``` -GET /api/auth/me -``` - -Returns `200 { user_uid, username, role_bits }` for an authenticated request, or `401` for a -guest. The frontend calls this once on mount to restore session state. - -### First-run setup - -``` -GET /api/auth/setup → 200 { setup_required: bool } -POST /api/auth/setup → 201 { user_uid, username } -Body: { username: string, password: string } -``` - -`setup_required` is `true` when no user has the `owner` role in `user_roles`. On `POST`: -- If setup is **already complete** (an owner exists): return `409 { error: "already_configured" }`. -- Otherwise: create the user (with `users.role = 'admin'` as placeholder), assign `user_roles` - rows for `user`, `admin`, `owner` (cumulative), seed `instance_settings` row if absent. - Return `201 { user_uid, username }`. Normal login flow applies immediately after. - -All non-setup API routes return `503 { error: "setup_required" }` until setup is complete. -The following routes are **exempt** from the 503 check: `GET /api/auth/setup`, -`POST /api/auth/setup`, `GET /` (static), `GET /assets/*` (static). - -### API tokens - -``` -POST /api/auth/tokens → 201 { token_uid, raw_token, name, created_at } -GET /api/auth/tokens → 200 [{ token_uid, name, created_at, last_used_at }] -DELETE /api/auth/tokens/:token_uid → 204 -``` - -`raw_token` is a cryptographically random 32-byte value, base64url-encoded, returned once. The -server stores only its SHA-256 hash. The management UI for these endpoints is in Track 7; the -endpoints are implemented here. - -### Password hashing - -Argon2id with default parameters from the `argon2` crate (memory=19 MiB, iterations=2, -parallelism=1). The current `"disabled-local-password"` sentinel in `ensure_default_user` becomes -irrelevant once setup is required on fresh instances. - -### Session expiry & cleanup - -`last_seen_at` is updated on every authenticated request using a **conditional update**: the -session row is already read during extraction; if `now() - last_seen_at > 60s`, issue an UPDATE. -This adds no extra query — only an extra UPDATE when the threshold is crossed. -`expires_at` = `last_seen_at + 30 days`, recalculated on each UPDATE. A background task in -`archivr-server/src/main.rs` runs `DELETE FROM sessions WHERE expires_at < now()` at startup -and every 24 hours via `tokio::time::interval`. - ---- - -## Auth Extractor - -New file: `crates/archivr-server/src/auth.rs` - -```rust -pub enum AuthUser { - Guest, - Authenticated { user_id: i64, role_bits: u32 }, -} - -impl AuthUser { - pub fn require_auth(&self) -> Result<(i64, u32), ApiError> // 401 if Guest - pub fn require_role(&self, bit: u32) -> Result<(), ApiError> // 403 if bit not set - pub fn has_role(&self, bit: u32) -> bool -} - -// Role bit constants -pub const ROLE_GUEST: u32 = 1; -pub const ROLE_USER: u32 = 2; -pub const ROLE_ADMIN: u32 = 4; -pub const ROLE_OWNER: u32 = 8; -``` - -Implemented as an Axum `FromRequestParts` extractor. Tries `session` cookie first, then -`Authorization: Bearer` header. -- **Cookie path**: look up `sessions` row JOIN `users` WHERE `session_uid = ?` - AND `users.status = 'active'` AND `expires_at > now()`. Use cached `role_bits` from the - session row. -- **Bearer path**: SHA-256 the token, look up `api_tokens` row JOIN `users` WHERE - `token_hash = ?` AND `users.status = 'active'` AND (`expires_at IS NULL OR expires_at > now()`). - Compute `role_bits` live: `ROLE_GUEST | (OR of user_roles bit values for that user)`. - Update `api_tokens.last_used_at`. -- Missing or invalid credential → `AuthUser::Guest` (never a hard error at extraction time). - ---- - -## Route Protection Tiers - -The existing security-boundary comment block in `routes.rs` is updated: - -| Tier | Requirement | Examples | -|---|---|---| -| `STATIC` | none | `GET /`, `GET /assets/*` | -| `PUBLIC_READ` | none (visibility filtering deferred to Track 6) | `GET /api/archives/:id/entries` | -| `AUTH_READ` | `ROLE_USER` bit | authenticated entry access | -| `WRITE` | `ROLE_USER` bit | `POST /api/archives/:id/captures`, tag mutations | -| `ADMIN` | `ROLE_ADMIN` bit | `GET /api/admin/archives`, user management | -| `OWNER` | `ROLE_OWNER` bit | instance settings, ownership transfer | - -**Error responses:** -- No/invalid session → `401` (frontend redirects to login) -- Valid session, insufficient role → `403` -- Private resource accessed without sufficient role → `404` (do not reveal existence) - -Track 4 applies `ROLE_USER` enforcement to all existing `WRITE` routes and `ROLE_ADMIN` to -`/api/admin/*`. `PUBLIC_READ` routes return all data for now; Track 6 adds visibility filters. - ---- - -## Frontend Changes - -### New components - -| Component | Purpose | -|---|---| -| `SetupPage.jsx` | First-run owner account creation wizard | -| `LoginPage.jsx` | Username/password login form | - -### App.jsx changes - -- On mount: call `GET /api/auth/setup`; if `setup_required`, render `` and nothing else. -- Otherwise: call `GET /api/auth/me`; store result as `currentUser` state (null = guest). -- Pass `currentUser` down via React context (`AuthContext`). -- Any `401` response from any API call sets `currentUser` to null → triggers ``. - -### api.js changes - -- Thin response interceptor: if status is `401`, dispatch a global `auth:expired` event that - `App.jsx` listens to and handles by clearing `currentUser`. -- No token storage in JS — cookies are handled entirely by the browser. - -### Topbar.jsx changes - -- When `currentUser` is set: show `username` and a **Log out** button. -- Log out calls `POST /api/auth/logout`, then clears `currentUser`. - -### What is NOT in Track 4 frontend - -- Settings page (Track 7) -- User management UI (Track 5) -- Role or visibility controls (Track 6) -- API token management UI (Track 7) - ---- - -## New Dependencies - -| Crate | Purpose | -|---|---| -| `argon2` | Password hashing (Argon2id) | -| `rand` | Cryptographically random token generation | -| `tower-cookies` | Cookie extraction in Axum (or use `axum-extra`) | - -Add to `archivr-server/Cargo.toml` and workspace `Cargo.toml` as needed. - ---- - -## Files Changed - -| File | Change | -|---|---| -| `crates/archivr-core/src/database.rs` | Add `roles`, `user_roles`, `sessions`, `api_tokens` tables; seed built-in roles; add `instance_settings.default_entry_visibility`; replace `ensure_default_user` with `ensure_owner_exists`; add session/token CRUD helpers | -| `crates/archivr-server/src/auth.rs` | New: `AuthUser` extractor, role bit constants, session/token lookup | -| `crates/archivr-server/src/routes.rs` | Add auth endpoints (`/api/auth/*`); apply `AuthUser` extractor to WRITE/ADMIN routes; update security-boundary comment | -| `crates/archivr-server/src/main.rs` | Session cleanup background task | -| `frontend/src/App.jsx` | Setup check, auth state, `AuthContext` | -| `frontend/src/api.js` | 401 interceptor | -| `frontend/src/components/LoginPage.jsx` | New | -| `frontend/src/components/SetupPage.jsx` | New | -| `frontend/src/components/Topbar.jsx` | User menu + logout | -| `Cargo.toml` | Add `argon2`, `rand`, `tower-cookies` (or `axum-extra`) | - ---- - -## Test Coverage - -- `database.rs`: role seeding, `ensure_owner_exists`, session CRUD, token hash round-trip -- `auth.rs`: extractor resolves cookie → session → user; extractor resolves Bearer → token → user; - missing credential → Guest; expired session → Guest -- `routes.rs`: login happy path; login wrong password returns 401; logout clears session; - setup endpoint returns 503 after setup complete; WRITE route returns 401 for Guest; - WRITE route returns 403 for insufficient role; setup flow end-to-end - ---- - -## Track Numbering Update for NEXT.md - -Original tracks 4 and 5 shift to 8 and 9. Collections is a named future track (no number until -scoped): - -| # | Track | -|---|---| -| 3 | Async capture jobs | -| **4** | **Auth foundation (this spec)** | -| **5** | **User management** | -| **6** | **Permissions & visibility (collection model)** | -| **7** | **Settings** | -| **8** | **Collections UI** | -| 9 | Cloud backup (was 4) | -| 10 | Cloud storage (was 5) | diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3f4c258..9ef3122 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -86,6 +86,7 @@ export default function App() { const [archives, setArchives] = useState([]) const [archiveId, setArchiveId] = useState(null) const [entries, setEntries] = useState([]) + const [deletedUids, setDeletedUids] = useState(() => new Set()) const [selectedEntryUid, setSelectedEntryUid] = useState(() => parseLocation().entry) const [selectedEntry, setSelectedEntry] = useState(null) const [selectedUids, setSelectedUids] = useState(() => { @@ -131,6 +132,11 @@ export default function App() { const pendingSearchFocus = useRef(false) const firstArchiveLoad = useRef(true) const lastAnchorIndexRef = useRef(null) + // Monotonic token for j/k keyboard navigation; cancels stale fetchEntryDetail calls. + const jkSeqRef = useRef(0) + // uid → entry object cache; populated on every row click so ctrl/shift + // selections can resolve child entries that aren't in the root entries array. + const entryCacheRef = useRef(new Map()) const humanizeTags = currentUser?.humanize_slugs ?? false; @@ -260,12 +266,20 @@ export default function App() { }, []) const handleRowClick = useCallback((entry, e) => { + // Cache every clicked entry so shift/ctrl can resolve child entries + // that are not present in the root `entries` array. + entryCacheRef.current.set(entry.entry_uid, entry) + // Invalidate any in-flight j/k uncached-child fetch. + ++jkSeqRef.current + if (e.shiftKey && lastAnchorIndexRef.current !== null) { e.preventDefault() - const anchorIdx = entries.findIndex(x => x.entry_uid === lastAnchorIndexRef.current) - const clickIdx = entries.findIndex(x => x.entry_uid === entry.entry_uid) + // Use DOM order so child rows (not in the `entries` array) participate + // in range selection. Every rendered row has data-entry-uid. + const allNodes = [...document.querySelectorAll('#entries-body [data-entry-uid]')] + const anchorIdx = allNodes.findIndex(n => n.dataset.entryUid === lastAnchorIndexRef.current) + const clickIdx = allNodes.findIndex(n => n.dataset.entryUid === entry.entry_uid) if (anchorIdx === -1 || clickIdx === -1) { - // anchor evicted by search/filter/delete — fall back to single select lastAnchorIndexRef.current = entry.entry_uid setSelectedUids(new Set([entry.entry_uid])) selectEntry(entry) @@ -273,24 +287,41 @@ export default function App() { } const lo = Math.min(anchorIdx, clickIdx) const hi = Math.max(anchorIdx, clickIdx) - const range = entries.slice(lo, hi + 1) - const uids = new Set(range.map(x => x.entry_uid)) + const uids = new Set(allNodes.slice(lo, hi + 1).map(n => n.dataset.entryUid)) setSelectedUids(uids) - if (uids.size === 1) selectEntry(range[0]) + if (uids.size === 1) selectEntry(entry) } else if (e.ctrlKey || e.metaKey) { lastAnchorIndexRef.current = entry.entry_uid - setSelectedUids(prev => { - const next = new Set(prev) - if (next.has(entry.entry_uid)) next.delete(entry.entry_uid) - else next.add(entry.entry_uid) - return next - }) + const next = new Set(selectedUids) + if (next.has(entry.entry_uid)) next.delete(entry.entry_uid) + else next.add(entry.entry_uid) + setSelectedUids(next) + if (next.size === 0) { + selectEntry(null) + } else if (next.size === 1) { + // Resolve the remaining UID — may be a child not in the root entries array + // and not yet cached (e.g. picked up via shift-range without a direct click). + const [remainingUid] = next + const cached = entryCacheRef.current.get(remainingUid) + ?? entries.find(x => x.entry_uid === remainingUid) + ?? null + if (cached) { + selectEntry(cached) + } else { + const tok = ++jkSeqRef.current + fetchEntryDetail(archiveId, remainingUid) + .then(det => { if (tok === jkSeqRef.current && det?.summary) selectEntry(det.summary) }) + .catch(() => {}) + } + } else { + selectEntry(null) + } } else { lastAnchorIndexRef.current = entry.entry_uid setSelectedUids(new Set([entry.entry_uid])) selectEntry(entry) } - }, [entries, selectEntry]) + }, [entries, selectedUids, selectEntry, archiveId]) const handleTagFilterSet = useCallback((fullPath) => { setTagFilter(fullPath) @@ -342,18 +373,26 @@ export default function App() { }, [archiveId, selectedEntry]) const handleEntryDeleted = useCallback((entryUid) => { + const isRoot = entries.some(e => e.entry_uid === entryUid) + setDeletedUids(prev => { const n = new Set(prev); n.add(entryUid); return n }) setEntries(prev => prev.filter(e => e.entry_uid !== entryUid)) setSelectedEntry(prev => prev?.entry_uid === entryUid ? null : prev) setSelectedEntryUid(prev => prev === entryUid ? null : prev) setSelectedUids(prev => { const n = new Set(prev); n.delete(entryUid); return n }) - }, []) + // Child delete: parent row's child_count/size are stale — reload after state updates. + if (!isRoot) loadEntries(archiveId, searchQuery, tagFilter) + }, [entries, archiveId, searchQuery, tagFilter, loadEntries]) const handleBulkDeleted = useCallback((uids) => { + const rootUids = new Set(entries.map(e => e.entry_uid)) + const hasChildDelete = [...uids].some(u => !rootUids.has(u)) + setDeletedUids(prev => { const n = new Set(prev); uids.forEach(u => n.add(u)); return n }) setEntries(prev => prev.filter(e => !uids.has(e.entry_uid))) setSelectedUids(new Set()) setSelectedEntry(null) setSelectedEntryUid(null) - }, []) + if (hasChildDelete) loadEntries(archiveId, searchQuery, tagFilter) + }, [entries, archiveId, searchQuery, tagFilter, loadEntries]) // Auto-snap: drive selectedEntryUid from selectedUids so URL sync and detail // panel stay correct. size >= 2 clears single-entry state (bulk panel takes over). @@ -397,24 +436,81 @@ export default function App() { if (current !== url) history.replaceState(null, '', url) }, [searchQuery, tagFilter, selectedEntryUid, view]) - // ⌘K / Ctrl+K: focus the search input, switching to archive view first if needed. + // ⌘K / Ctrl+K / /: focus the search input, switching to archive view first if needed. useEffect(() => { const handler = (e) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault() - if (view === 'archive') { - searchInputRef.current?.focus() - searchInputRef.current?.select() - } else { - pendingSearchFocus.current = true - setView('archive') - } + const isSlash = e.key === '/' && !e.metaKey && !e.ctrlKey && !e.altKey + const isCmdK = (e.metaKey || e.ctrlKey) && e.key === 'k' + if (!isSlash && !isCmdK) return + // Don't intercept / when already typing in an input + const tag = document.activeElement?.tagName + if (isSlash && (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')) return + if (isSlash && document.activeElement?.isContentEditable) return + e.preventDefault() + if (view === 'archive') { + searchInputRef.current?.focus() + searchInputRef.current?.select() + } else { + pendingSearchFocus.current = true + setView('archive') } } document.addEventListener('keydown', handler) return () => document.removeEventListener('keydown', handler) }, [view]) + // j/k: navigate entries down/up when not focused on an input element. + useEffect(() => { + const handler = (e) => { + // Ignore when a modifier is held (don't steal browser/app shortcuts) + if (e.metaKey || e.ctrlKey || e.altKey) return + if (e.key !== 'j' && e.key !== 'k') return + // Ignore when focus is on any editable target + const tag = document.activeElement?.tagName + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return + if (document.activeElement?.isContentEditable) return + + const allNodes = [...document.querySelectorAll('#entries-body [data-entry-uid]')] + if (allNodes.length === 0) return + + const [currentUid] = selectedUids.size === 1 ? selectedUids : [null] + const currentIdx = currentUid + ? allNodes.findIndex(n => n.dataset.entryUid === currentUid) + : -1 + + const nextIdx = e.key === 'j' + ? Math.min(currentIdx + 1, allNodes.length - 1) + : Math.max(currentIdx - 1, 0) + + if (nextIdx === currentIdx && currentIdx !== -1) return + + const nextNode = allNodes[nextIdx < 0 ? 0 : nextIdx] + const nextUid = nextNode.dataset.entryUid + + lastAnchorIndexRef.current = nextUid + const tok = ++jkSeqRef.current + setSelectedUids(new Set([nextUid])) + + // Resolve entry object: cache → root entries array → server fetch. + // Token guards against a slow fetch settling after the user has moved on. + const cached = entryCacheRef.current.get(nextUid) + ?? entries.find(x => x.entry_uid === nextUid) + ?? null + if (cached) { + selectEntry(cached) + } else { + fetchEntryDetail(archiveId, nextUid) + .then(det => { if (tok === jkSeqRef.current && det?.summary) selectEntry(det.summary) }) + .catch(() => {}) + } + + nextNode.scrollIntoView({ block: 'nearest' }) + e.preventDefault() + } + document.addEventListener('keydown', handler) + return () => document.removeEventListener('keydown', handler) + }, [selectedUids, entries, selectEntry, archiveId]) + // After switching to archive view via ⌘K, focus the search input once rendered. useEffect(() => { if (view === 'archive' && pendingSearchFocus.current) { @@ -563,6 +659,7 @@ export default function App() { onRowClick={handleRowClick} archiveId={archiveId} pendingCaptures={pendingCaptures} + deletedUids={deletedUids} /> )} {view === 'runs' && } diff --git a/frontend/src/api.js b/frontend/src/api.js index d9fd243..43c08d4 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -25,6 +25,10 @@ export async function fetchEntryDetail(archiveId, entryUid) { return getJson(`/api/archives/${archiveId}/entries/${entryUid}`); } +export async function fetchEntryChildren(archiveId, entryUid) { + return getJson(`/api/archives/${archiveId}/entries/${entryUid}/children`); +} + // Fetch multiple artifact JSON payloads for an entry in parallel. // Returns a Promise preserving index order. export function fetchEntryArtifacts(archiveId, entryUid, indices) { @@ -149,6 +153,8 @@ export async function submitCapture(archiveId, locator, quality = null, extensio if (typeof extensions.cookie_ext_enabled === 'boolean') payload.cookie_ext_enabled = extensions.cookie_ext_enabled if (typeof extensions.modal_closer_enabled === 'boolean') payload.modal_closer_enabled = extensions.modal_closer_enabled if (typeof extensions.via_freedium === 'boolean') payload.via_freedium = extensions.via_freedium + if (extensions.per_item_quality && typeof extensions.per_item_quality === 'object' && Object.keys(extensions.per_item_quality).length > 0) payload.per_item_quality = extensions.per_item_quality + if (extensions.sync === true) payload.sync = true } const res = await fetch(`/api/archives/${archiveId}/captures`, { method: "POST", @@ -168,6 +174,19 @@ export async function probeCapture(archiveId, locator) { return getJson(`/api/archives/${archiveId}/captures/probe?locator=${encodeURIComponent(locator)}`); } +export async function probePlaylist(archiveId, locator) { + const res = await fetch(`/api/archives/${archiveId}/captures/probe-playlist`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ locator }), + }) + if (!res.ok) { + const body = await res.json().catch(() => ({})) + throw new Error(body.error || `HTTP ${res.status}`) + } + return res.json() +} + export async function pollCaptureJob(archiveId, jobUid) { return getJson(`/api/archives/${archiveId}/capture_jobs/${jobUid}`); } diff --git a/frontend/src/components/CaptureDialog.jsx b/frontend/src/components/CaptureDialog.jsx index 3878303..d3e57b8 100644 --- a/frontend/src/components/CaptureDialog.jsx +++ b/frontend/src/components/CaptureDialog.jsx @@ -1,5 +1,5 @@ import { useRef, useEffect, useState, useCallback } from 'react' -import { submitCapture, pollCaptureJob, probeCapture, getInstanceSettings } from '../api' +import { submitCapture, pollCaptureJob, probeCapture, probePlaylist, getInstanceSettings } from '../api' let nextItemId = 1 @@ -14,7 +14,11 @@ function isVideoSource(locator) { for (const scheme of ['yt:', 'youtube:']) { if (ll.startsWith(scheme)) { const after = ll.slice(scheme.length) - return after.startsWith('video/') || after.startsWith('short/') || after.startsWith('shorts/') + if (after.startsWith('video/') || after.startsWith('short/') || after.startsWith('shorts/')) + return true + // bare yt:ID — exactly 11 chars [A-Za-z0-9_-], same predicate as is_youtube_video_id in core + if (/^[a-z0-9_-]{11}$/i.test(after)) return true + return false } } @@ -63,6 +67,52 @@ function isVideoSource(locator) { return false } +function isPlaylistSource(locator) { + const l = locator.trim() + const ll = l.toLowerCase() + + // yt: / youtube: shorthands — playlist, channel, @ handles + for (const scheme of ['yt:', 'youtube:']) { + if (ll.startsWith(scheme)) { + const after = ll.slice(scheme.length) + return after.startsWith('playlist/') || after.startsWith('@') || + after.startsWith('channel/') || after.startsWith('c/') || after.startsWith('user/') + } + } + + // ytm: shorthand — playlist + if (ll.startsWith('ytm:')) { + return ll.slice(4).startsWith('playlist/') + } + + // spotify: shorthands — album and playlist (not track) + if (ll.startsWith('spotify:')) { + const after = ll.slice(8) + return after.startsWith('album:') || after.startsWith('playlist:') + } + + // HTTP/HTTPS URLs + if (ll.startsWith('http://') || ll.startsWith('https://')) { + try { + const url = new URL(l) + const host = url.hostname + if (host === 'youtube.com' || host === 'www.youtube.com') { + if (url.pathname === '/playlist' && url.searchParams.has('list')) return true + if (url.pathname.startsWith('/@') || url.pathname.startsWith('/channel/') || + url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) return true + } + if (host === 'music.youtube.com') { + if (url.pathname === '/playlist' && url.searchParams.has('list')) return true + } + if (host === 'open.spotify.com') { + if (url.pathname.startsWith('/album/') || url.pathname.startsWith('/playlist/')) return true + } + } catch {} + } + + return false +} + function makeItem(locator = '') { return { id: nextItemId++, locator, quality: 'best', @@ -71,9 +121,47 @@ function makeItem(locator = '') { probeQualities: null, // null | string[] when done, e.g. ["1080p","720p","480p"] probeHasAudio: false, // true when probe confirms at least one audio track status: 'idle', error: null, jobUid: null, archiveId: null, + // playlist probe state + playlistProbeState: 'idle', // 'idle' | 'probing' | 'done' | 'error' + playlistInfo: null, // raw API response or null + playlistItems: null, // [{id, url, title, qualities, has_audio, quality}] or null + playlistQuality: null, // selected playlist-level quality string or null + playlistExpanded: false, // whether per-video list is expanded + syncEnabled: false, // sync mode toggle } } +function applyPlaylistQuality(newQ, currentItems) { + if (newQ === 'best') { + return currentItems.map(item => ({ ...item, quality: 'best' })) + } + if (newQ === 'audio') { + return currentItems.map(item => { + if (item.has_audio) return { ...item, quality: 'audio' } + // No audio track — same conflict rule as unsupported height: + // keep a prior selection if one exists, otherwise leave null (blocks archive). + if (item.quality !== null) return item + return { ...item, quality: null } + }) + } + return currentItems.map(item => { + // Exact match only: the video must list this quality in its available formats. + // maxHeight >= newHeight would be a cap (yt-dlp silent fallback), not what + // the user selected; a video with [2160p, 1080p] does NOT support 1440p. + if (item.qualities.includes(newQ)) { + return { ...item, quality: newQ } + } + // Quality not available for this item — keep prior selection if one exists, + // otherwise null (conflict, blocks archive until user picks manually). + if (item.quality !== null) return item + return { ...item, quality: null } + }) +} + +function hasConflict(item) { + return Array.isArray(item.playlistItems) && item.playlistItems.some(pi => pi.quality === null) +} + export default function CaptureDialog({ open, archiveId, onClose, onCaptured, onToast, onJobStarted, onJobSettled, activeJobs = [] }) { const dialogRef = useRef(null) const isFirstRenderRef = useRef(true) @@ -105,7 +193,9 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on const idle = saved.filter(it => !it.status || it.status === 'idle') if (idle.length > 0) { idle.forEach(it => { if (it.id >= nextItemId) nextItemId = it.id + 1 }) - return idle + // Merge with makeItem() defaults so items saved before the playlist + // fields were added don't have undefined where null/false is expected. + return idle.map(it => ({ ...makeItem(it.locator), ...it })) } } } catch {} @@ -283,7 +373,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on onToastRef.current(text, null, type, headline) } - async function submitBgJob(locator, quality, batchId) { + async function submitBgJob(locator, quality, batchId, extraExtensions = {}) { const aid = archiveIdRef.current const id = crypto.randomUUID?.() ?? `job-${Date.now()}-${Math.random()}` // Capture session options at call time (synchronous — before first await) @@ -293,6 +383,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on cookie_ext_enabled: cookieExtEnabled, modal_closer_enabled: modalCloserEnabled, via_freedium: freediumEnabled, + ...extraExtensions, } try { const job = await submitCapture(aid, locator, quality, extensions) @@ -309,19 +400,31 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on function handleArchive() { const toSubmit = items.filter(it => it.locator.trim()) if (toSubmit.length === 0) return + if (toSubmit.some(it => hasConflict(it))) return + if (toSubmit.some(it => it.probeState === 'probing' || + (isPlaylistSource(it.locator) && it.playlistProbeState !== 'done'))) return + if (toSubmit.some(it => Array.isArray(it.playlistItems) && it.playlistItems.length === 0)) return const batchId = toSubmit.length > 1 ? (crypto.randomUUID?.() ?? `batch-${Date.now()}`) : null if (batchId) { batchRef.current.set(batchId, { total: toSubmit.length, archived: 0, warnings: 0, failed: 0, failedLocators: [], warningLocators: [] }) } - // Capture options at call time before any state changes - const capturedQuality = toSubmit.map(it => it.quality || 'best') + // Capture all submission data before any state changes + const submissions = toSubmit.map(it => ({ + locator: it.locator.trim(), + quality: it.playlistItems !== null ? null : (it.quality || 'best'), + extraExtensions: it.playlistItems !== null + ? { per_item_quality: Object.fromEntries(it.playlistItems.map(pi => [pi.id, pi.quality])), sync: it.syncEnabled } + : {}, + })) // Reset form and close dialog immediately setItems([makeItem()]) dialogRef.current?.close() // Submit each in background - toSubmit.forEach((it, i) => submitBgJob(it.locator.trim(), capturedQuality[i], batchId)) + submissions.forEach(({ locator, quality, extraExtensions }) => + submitBgJob(locator, quality, batchId, extraExtensions) + ) } function addRow() { @@ -340,45 +443,108 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on function updateLocator(id, val) { // Cancel any in-flight debounce and immediately clear stale probe results. // This prevents old qualities from being visible (and submittable) while - // the 600ms debounce is pending for the new URL. + // the debounce is pending for the new URL. clearTimeout(probeTimers.current.get(id)) setItems(prev => prev.map(it => it.id === id - ? { ...it, locator: val, probeState: 'idle', probeQualities: null, probeHasAudio: false, quality: 'best' } + ? { ...it, locator: val, probeState: 'idle', probeQualities: null, probeHasAudio: false, quality: 'best', + playlistProbeState: 'idle', playlistInfo: null, playlistItems: null, playlistQuality: null, playlistExpanded: false } : it )) - if (!isVideoSource(val)) return - - // Schedule a fresh probe after the user stops typing - const timer = setTimeout(async () => { - probeTimers.current.delete(id) - setItems(prev => prev.map(it => it.id === id ? { ...it, probeState: 'probing' } : it)) - try { - const result = await probeCapture(archiveIdRef.current, val.trim()) - setItems(prev => prev.map(it => { - if (it.id !== id || it.locator !== val) return it // stale — locator changed again - const qualities = result.qualities ?? [] - const hasAudio = result.has_audio ?? false - // Audio-only source: no video heights but audio confirmed — force audio mode - const quality = (qualities.length === 0 && hasAudio) ? 'audio' : 'best' - return { ...it, probeState: 'done', probeQualities: qualities, probeHasAudio: hasAudio, quality } - })) - } catch { - // Probe failed (network error, etc.) — clear silently; user can still submit - setItems(prev => prev.map(it => - it.id === id ? { ...it, probeState: 'idle', probeQualities: null } : it - )) - } - }, 600) - probeTimers.current.set(id, timer) + if (isVideoSource(val)) { + // Schedule a fresh probe after the user stops typing + const timer = setTimeout(async () => { + probeTimers.current.delete(id) + setItems(prev => prev.map(it => it.id === id ? { ...it, probeState: 'probing' } : it)) + try { + const result = await probeCapture(archiveIdRef.current, val.trim()) + setItems(prev => prev.map(it => { + if (it.id !== id || it.locator !== val) return it // stale — locator changed again + const qualities = result.qualities ?? [] + const hasAudio = result.has_audio ?? false + // Audio-only source: no video heights but audio confirmed — force audio mode + const quality = (qualities.length === 0 && hasAudio) ? 'audio' : 'best' + return { ...it, probeState: 'done', probeQualities: qualities, probeHasAudio: hasAudio, quality } + })) + } catch { + // Probe failed (network error, etc.) — clear silently; user can still submit + setItems(prev => prev.map(it => + it.id === id ? { ...it, probeState: 'idle', probeQualities: null } : it + )) + } + }, 600) + probeTimers.current.set(id, timer) + } else if (isPlaylistSource(val)) { + // Schedule a playlist probe (playlists are slower — 800ms debounce) + const timer = setTimeout(async () => { + probeTimers.current.delete(id) + setItems(prev => prev.map(it => it.id === id ? { ...it, playlistProbeState: 'probing' } : it)) + try { + const result = await probePlaylist(archiveIdRef.current, val.trim()) + setItems(prev => prev.map(it => { + if (it.id !== id || it.locator !== val) return it // stale — locator changed again + return { + ...it, + playlistProbeState: 'done', + playlistInfo: result, + playlistItems: result.items.map(pi => ({ ...pi, quality: null })), + playlistQuality: null, + } + })) + } catch { + setItems(prev => prev.map(it => + it.id === id ? { ...it, playlistProbeState: 'error' } : it + )) + } + }, 800) + probeTimers.current.set(id, timer) + } } function updateQuality(id, val) { setItems(prev => prev.map(it => it.id === id ? { ...it, quality: val } : it)) } + function updatePlaylistQuality(id, q) { + setItems(prev => prev.map(it => { + if (it.id !== id) return it + const newItems = applyPlaylistQuality(q, it.playlistItems) + return { ...it, playlistQuality: q, playlistItems: newItems } + })) + } + function updatePlaylistItemQuality(id, videoId, q) { + setItems(prev => prev.map(it => { + if (it.id !== id) return it + return { ...it, playlistItems: it.playlistItems.map(pi => pi.id === videoId ? { ...pi, quality: q } : pi) } + })) + } + function togglePlaylistExpanded(id) { + setItems(prev => prev.map(it => it.id === id ? { ...it, playlistExpanded: !it.playlistExpanded } : it)) + } + function updateSync(id, val) { + setItems(prev => prev.map(it => it.id === id ? { ...it, syncEnabled: val } : it)) + } + function deletePlaylistItem(itemId, videoId) { + setItems(prev => prev.map(it => + it.id !== itemId ? it : + { ...it, playlistItems: it.playlistItems.filter(pi => pi.id !== videoId) } + )) + } + + const pendingCount = items.filter(it => it.locator.trim()).length + const anyConflict = items.some(it => hasConflict(it)) + // True if any playlist row has had all its videos deleted — archive would be a no-op. + const anyEmptyPlaylist = items.some(it => + Array.isArray(it.playlistItems) && it.playlistItems.length === 0 + ) + const anyProbing = items.some(it => + it.probeState === 'probing' || + // For playlist sources block unless probe completed successfully: + // idle = debounce not yet fired; probing = in flight; error = no quality data. + (isPlaylistSource(it.locator) && it.playlistProbeState !== 'done') + ) return ( @@ -407,6 +573,11 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on onQualityChange={val => updateQuality(item.id, val)} onRemove={() => removeRow(item.id)} onSubmit={handleArchive} + onPlaylistQualityChange={q => updatePlaylistQuality(item.id, q)} + onPlaylistItemQualityChange={(vid, q) => updatePlaylistItemQuality(item.id, vid, q)} + onPlaylistToggle={() => togglePlaylistExpanded(item.id)} + onSyncChange={val => updateSync(item.id, val)} + onPlaylistItemDelete={(vid) => deletePlaylistItem(item.id, vid)} /> ))} @@ -530,7 +701,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on type="button" className="capture-submit" onClick={handleArchive} - disabled={pendingCount === 0} + disabled={pendingCount === 0 || anyConflict || anyProbing || anyEmptyPlaylist} > {pendingCount > 1 ? `Archive ${pendingCount}` : 'Archive'} @@ -543,7 +714,8 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on ) } -function CaptureRow({ item, autoFocus, onLocatorChange, onQualityChange, onRemove, onSubmit }) { +function CaptureRow({ item, autoFocus, onLocatorChange, onQualityChange, onRemove, onSubmit, + onPlaylistQualityChange, onPlaylistItemQualityChange, onPlaylistToggle, onSyncChange, onPlaylistItemDelete }) { const inputRef = useRef(null) useEffect(() => { @@ -554,6 +726,47 @@ function CaptureRow({ item, autoFocus, onLocatorChange, onQualityChange, onRemov // Quality control shown right of the input const qualityEl = (() => { + // Playlist source handling + if (isPlaylistSource(item.locator)) { + if (item.playlistProbeState === 'probing') { + return + } + if (item.playlistProbeState === 'done') { + const allHeights = [...new Set( + item.playlistItems.flatMap(pi => pi.qualities.map(q => parseInt(q))) + )].sort((a, b) => b - a) + const allHaveAudio = item.playlistItems.every(pi => pi.has_audio) + const conflictCount = item.playlistItems.filter(pi => pi.quality === null).length + return ( + <> + + {conflictCount > 0 && ( + {conflictCount} need selection + )} + + ) + } + if (item.playlistProbeState === 'error') { + return ( + + Probe failed — edit URL to retry + + ) + } + return null + } + + // Video source handling (unchanged) if (!isVideoSource(item.locator)) return null if (item.probeState === 'probing') { return @@ -565,8 +778,6 @@ function CaptureRow({ item, autoFocus, onLocatorChange, onQualityChange, onRemov return No media detected } if (qualities.length === 0 && hasAudio) { - // Audio-only source: no video tracks, only audio available. - // Don't offer "Best quality" — it would request a video format and fail. return ( onSyncChange(e.target.checked)} /> + Sync — skip already-archived videos + + ) : null + return (
+ {isPlaylistSource(item.locator) ? ( + item.playlistProbeState === 'done' ? ( + + ) : null + ) : null} {item.error}

)} + {isPlaylistSource(item.locator) && item.playlistProbeState === 'done' && item.playlistExpanded ? ( +
+ {item.playlistItems.map(pi => ( +
+ {pi.title || pi.url} + + {pi.quality === null && ( + Choose quality + )} + +
+ ))} + {syncToggle} +
+ ) : syncToggle}
) } diff --git a/frontend/src/components/EntriesView.jsx b/frontend/src/components/EntriesView.jsx index 71a1c0c..af42f32 100644 --- a/frontend/src/components/EntriesView.jsx +++ b/frontend/src/components/EntriesView.jsx @@ -2,7 +2,7 @@ import SkeletonEntryRow from './SkeletonEntryRow'; import EntryRow from './EntryRow'; -export default function EntriesView({ entries, selectedUids, onRowClick, archiveId, pendingCaptures = [] }) { +export default function EntriesView({ entries, selectedUids, onRowClick, archiveId, pendingCaptures = [], deletedUids }) { return (
@@ -18,14 +18,17 @@ export default function EntriesView({ entries, selectedUids, onRowClick, archive {pendingCaptures.filter(c => c.archiveId === archiveId).reverse().map(cap => ( ))} - {entries.map(entry => ( + {entries.map((entry, idx) => ( = 2 && selectedUids.has(entry.entry_uid)} onRowClick={onRowClick} + selectedUids={selectedUids} + deletedUids={deletedUids} /> ))}
diff --git a/frontend/src/components/EntryRow.jsx b/frontend/src/components/EntryRow.jsx index 57b6abf..2aef582 100644 --- a/frontend/src/components/EntryRow.jsx +++ b/frontend/src/components/EntryRow.jsx @@ -1,8 +1,51 @@ import { useState } from 'react'; import { formatTimestamp, formatBytes, valueText, sourceIconSvg } from '../utils'; +import { fetchEntryChildren } from '../api'; -export default function EntryRow({ entry, archiveId, isSelected, isMultiSelected, onRowClick }) { +function ChildRow({ entry, index, onRowClick, selectedUids }) { + const isSelected = (selectedUids?.size === 1) && selectedUids.has(entry.entry_uid); + const isMultiSelected = (selectedUids?.size >= 2) && selectedUids.has(entry.entry_uid); + + const cls = ['child-entry-row', + index % 2 === 0 ? 'child-entry-row--light' : 'child-entry-row--dark', + isSelected && 'is-selected', + isMultiSelected && 'is-multi-selected', + ].filter(Boolean).join(' '); + + return ( +
{ if (e.shiftKey) e.preventDefault(); }} + onClick={e => onRowClick(entry, e)} + onKeyDown={e => { if (e.key === 'Enter') onRowClick(entry, e); }} + > + + ); +} + +export default function EntryRow({ entry, archiveId, rowIndex, isSelected, isMultiSelected, onRowClick, selectedUids, deletedUids }) { const [favFailed, setFavFailed] = useState(false); + const [expanded, setExpanded] = useState(false); + const [children, setChildren] = useState(null); + const [childrenLoading, setChildrenLoading] = useState(false); + const showFavicon = entry.source_kind === 'web' && entry.entity_kind === 'page' && @@ -24,49 +67,108 @@ export default function EntryRow({ entry, archiveId, isSelected, isMultiSelected ); const checked = isSelected || isMultiSelected; + const hasChildren = entry.child_count > 0; function handleCheckboxClick(e) { e.stopPropagation(); - // treat checkbox tap as ctrl+click: toggle this entry without clearing others onRowClick(entry, { ctrlKey: true, metaKey: false, shiftKey: false, preventDefault() {} }); } + async function handleExpandClick(e) { + e.stopPropagation(); + if (expanded) { + setExpanded(false); + return; + } + setExpanded(true); + if (children === null && !childrenLoading) { + setChildrenLoading(true); + try { + const result = await fetchEntryChildren(archiveId, entry.entry_uid); + setChildren(result); + } catch (_) { + setChildren([]); + } finally { + setChildrenLoading(false); + } + } + } + + const outerClass = [ + 'entry-row-outer', + rowIndex % 2 === 0 ? 'entry-row-outer--light' : 'entry-row-outer--dark', + isSelected && 'is-selected', + isMultiSelected && 'is-multi-selected', + ].filter(Boolean).join(' '); + return ( -
{ if (e.shiftKey) e.preventDefault() }} - onClick={e => onRowClick(entry, e)} - onKeyDown={e => { if (e.key === 'Enter') onRowClick(entry, e) }} - > -
-
+
{formatTimestamp(entry.archived_at)}
+
+ {hasChildren && ( +
+
+ {valueText(entry.entity_kind)} +
+
+ {formatBytes(entry.total_artifact_bytes)} + {entry.cached_bytes > 0 && entry.cacheable_bytes > 0 && ( + + {Math.round(entry.cached_bytes / entry.cacheable_bytes * 100)}% cached + + )} +
+
{valueText(entry.original_url)}
-
{formatTimestamp(entry.archived_at)}
-
- {icon} - {valueText(entry.title) || valueText(entry.entry_uid)} -
-
- {valueText(entry.entity_kind)} -
-
- {formatBytes(entry.total_artifact_bytes)} - {entry.cached_bytes > 0 && entry.total_artifact_bytes > 0 && ( - - {Math.round(entry.cached_bytes / entry.total_artifact_bytes * 100)}% cached - - )} -
-
{valueText(entry.original_url)}
+ {expanded && ( + <> + {childrenLoading &&
Loading…
} +
+ {children && children + .filter(c => !deletedUids?.has(c.entry_uid)) + .map((child, idx) => ( + + ))} +
+ + )}
); } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 9b8fe4f..4c0208d 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -299,17 +299,19 @@ select { border-bottom: 1px solid var(--line-soft); } #entries-body > div > div { padding: 7px 10px; flex-shrink: 0; overflow: hidden; } -#entries-body > div:nth-child(even) { background: #f2ede5; } -#entries-body > div:nth-child(odd) { background: var(--paper-3); } +/* Skeleton rows (no index class) fall back to nth-child; real rows use explicit classes. */ +#entries-body > div:not(.entry-row-outer):nth-child(even) { background: #f2ede5; } +#entries-body > div:not(.entry-row-outer):nth-child(odd) { background: var(--paper-3); } +/* Index-based stripes for real entry rows — immune to skeleton sibling count. */ +#entries-body > .entry-row-outer--light { background: var(--paper-3); } +#entries-body > .entry-row-outer--dark { background: #f2ede5; } #entries-body > div.is-selected { background: #eee2d2; - outline: 2px solid var(--accent); - outline-offset: -2px; + box-shadow: inset 0 0 0 2px var(--accent); } #entries-body > div.is-multi-selected { background: #eee2d2; - outline: 2px solid var(--accent); - outline-offset: -2px; + box-shadow: inset 0 0 0 2px var(--accent); } .col-added { width: 162px; color: var(--muted); } @@ -339,8 +341,7 @@ select { .source-icon > * { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } .source-icon svg { width: 100%; height: 100%; } .url-cell { color: #555b55; white-space: nowrap; text-overflow: ellipsis; word-break: break-all; } -#entries-body .url-cell:hover, -#entries-body .is-selected .url-cell { overflow: visible; white-space: normal; } +#entries-body .url-cell:hover { overflow: visible; white-space: normal; } .type-pill { display: inline-block; padding: 2px 6px; background: #d8e3df; color: #275a5f; border: 1px solid #bfd0ca; border-radius: var(--r); } /* ── Multi-select checkbox column ───────────────────────────────────────── */ @@ -890,6 +891,7 @@ select { color: var(--muted); font-style: italic; } +.capture-quality-hint--error { color: #c05000; font-style: normal; } /* Status dot */ .cap-dot { @@ -2726,3 +2728,254 @@ body.has-audio-bar { padding-bottom: 56px; } @media (pointer: coarse) { .skeleton-row .col-added { padding-left: 10px; } } + +/* ── Child entry expansion ───────────────────────────────────────────────── */ + +/* Outer wrapper: one block per entry-group so nth-child stays correct. + #entries-body > .entry-row-outer beats #entries-body > div on specificity + (id + class > id + element) so display:flex is safely overridden. */ +#entries-body > .entry-row-outer { + display: block; + border-bottom: none; +} + +/* Inner flex row: replicates the #entries-body > div row behaviour. */ +#entries-body > .entry-row-outer > .entry-row-main { + display: flex; + align-items: center; + cursor: default; + border-bottom: 1px solid var(--line-soft); + /* Reset padding that #entries-body > div > div would otherwise apply. */ + padding: 0; + flex-shrink: unset; + overflow: visible; +} + +/* Column cells inside the inner row. */ +#entries-body > .entry-row-outer > .entry-row-main > div { + padding: 7px 10px; + flex-shrink: 0; + overflow: hidden; +} +#entries-body > .entry-row-outer > .entry-row-main > div:last-child { padding-right: 22px; } +#entries-body > .entry-row-outer > .entry-row-main .col-added { padding-left: 22px; } + + +/* Selection: class lives on outer wrapper; background + stroke scoped to inner row + so expanded child entries don't inherit the selection highlight. */ +#entries-body > .entry-row-outer.is-selected { background: unset; outline: none; box-shadow: none; } +#entries-body > .entry-row-outer.is-multi-selected { background: unset; outline: none; box-shadow: none; } +#entries-body > .entry-row-outer.is-selected > .entry-row-main { + background: #eee2d2; + box-shadow: inset 0 0 0 2px var(--accent); +} +#entries-body > .entry-row-outer.is-multi-selected > .entry-row-main { + background: #eee2d2; + box-shadow: inset 0 0 0 2px var(--accent); +} + +/* URL overflow on hover only — explicit pointer, not selection. */ +#entries-body > .entry-row-outer .url-cell:hover { overflow: visible; white-space: normal; } + +/* child-entries container: reset the padding that #entries-body > div > div applies. */ +#entries-body > .entry-row-outer > .child-entries { + display: block; + padding: 0; + flex-shrink: unset; + overflow: visible; + border-left: 2px solid var(--line-soft); + margin-left: 32px; +} + +/* Each child row reuses the same .col-* flex widths as normal rows. */ +.child-entry-row { + display: flex; + align-items: center; + cursor: default; + border-bottom: 1px solid var(--line-soft); + opacity: 0.88; +} +.child-entry-row:hover { opacity: 1; } +.child-entry-row:last-child { border-bottom: none; } +.child-entry-row--light { background: #fafaf8; } +.child-entry-row--dark { background: #f2f0ec; } +.child-entry-row.is-selected { + background: #eee2d2; + box-shadow: inset 0 0 0 2px var(--accent); + opacity: 1; +} +.child-entry-row.is-multi-selected { + background: #eee2d2; + box-shadow: inset 0 0 0 2px var(--accent); + opacity: 1; +} +.child-entry-row > div { + padding: 6px 10px; + flex-shrink: 0; + overflow: hidden; +} +.child-entry-row .col-added { padding-left: 22px; } +.child-entry-row > div:last-child { padding-right: 22px; } + +/* Expand chevron button */ +.entry-expand-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + padding: 0; + margin-right: 2px; + background: none; + border: none; + cursor: pointer; + opacity: 0.45; + color: inherit; + transition: opacity 0.15s; +} +.entry-expand-btn:focus { outline: none; } +.entry-expand-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; } +.entry-expand-btn::before { + content: ''; + display: block; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 6px solid currentColor; + transition: transform 0.15s; +} +.entry-expand-btn:hover { opacity: 1; } +.entry-expand-btn.is-expanded::before { transform: rotate(90deg); } + +/* Child count badge next to title */ +.child-count-badge { + display: inline-block; + margin-left: 5px; + padding: 0 5px; + font-size: 0.72em; + font-weight: 600; + background: color-mix(in srgb, var(--line-soft) 60%, transparent); + border-radius: 10px; + opacity: 0.75; + vertical-align: middle; + line-height: 1.6; +} + +/* Loading placeholder inside child-entries */ +.child-entries-loading { + padding: 8px 12px; + font-size: 0.85em; + opacity: 0.55; +} + +/* ── Playlist quality expansion ──────────────────────────── */ +.capture-playlist-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 0.75rem; + color: var(--muted); + background: none; + border: none; + cursor: pointer; + padding: 2px 6px; +} +.capture-playlist-toggle:hover { color: var(--text); } +.capture-playlist-toggle--left { + flex-shrink: 0; + padding: 2px 4px; + font-size: 0.7rem; +} + +.capture-conflict-badge { + font-size: 0.7rem; + color: #c07000; + background: #fff3cd; + border: 1px solid #e0a000; + border-radius: 4px; + padding: 1px 6px; + margin-left: 6px; +} + +.capture-playlist-items { + border-top: 1px solid var(--line-soft); + max-height: 320px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--line) transparent; +} + +.capture-playlist-item { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 10px 5px 14px; + font-size: 0.8rem; + border-bottom: 1px solid var(--line-soft); + border-left: 3px solid transparent; +} +.capture-playlist-item:last-of-type { border-bottom: none; } +.capture-playlist-item--conflict { + border-left-color: #c07000; + background: color-mix(in srgb, #c07000 6%, transparent); +} +.capture-playlist-item-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text); + opacity: 0.9; +} +.capture-item-quality { + font-size: 0.73rem; + padding: 2px 4px; + border: 1px solid var(--line); + border-radius: 3px; + background: var(--paper-3); + color: var(--text); + flex-shrink: 0; +} +.capture-playlist-conflict-badge { + font-size: 0.68rem; + font-weight: 600; + color: #c07000; + white-space: nowrap; + flex-shrink: 0; +} +.capture-playlist-item-remove { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + border: none; + background: none; + color: var(--muted); + cursor: pointer; + border-radius: 3px; + opacity: 0.3; + transition: opacity 0.1s, color 0.1s; +} +.capture-playlist-item:hover .capture-playlist-item-remove, +.capture-playlist-item-remove:focus-visible { opacity: 1; } +.capture-playlist-item-remove:hover { color: #c04000; opacity: 1; } +@media (pointer: coarse) { .capture-playlist-item-remove { opacity: 1; } } +.capture-playlist-item-remove { font-size: 0.75rem; line-height: 1; } + +.capture-sync-row { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-size: 0.78rem; + color: var(--muted); + border-top: 1px solid var(--line-soft); + cursor: pointer; +} +.capture-sync-row input[type=checkbox] { cursor: pointer; }