diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 473fe3a..0000000 --- a/.editorconfig +++ /dev/null @@ -1,32 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.rs] -indent_style = space -indent_size = 2 - -[justfile] -indent_style = space -indent_size = 2 - -[*.toml] -indent_style = space -indent_size = 2 - -[*.md] -indent_style = space -indent_size = 2 -trim_trailing_whitespace = false - -[*.nix] -indent_style = space -indent_size = 2 - -[*.scss] -indent_style = space -indent_size = 2 diff --git a/Cargo.lock b/Cargo.lock index 41cca31..622df0c 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index 395f440..8d9ea1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*", "packages/*", "xtask"] +members = ["crates/*", "xtask"] exclude = ["crates/pinakes-core/tests/fixtures/test-plugin"] resolver = "3" @@ -11,44 +11,35 @@ readme = true rust-version = "1.95.0" # follows nightly Rust [workspace.dependencies] -# Crate components for Pinakes. Those are the internal dependencies that are built -# while building any package. +# Crate components for Pinakes. pinakes-core = { path = "./crates/pinakes-core" } +pinakes-server = { path = "./crates/pinakes-server" } pinakes-plugin-api = { path = "./crates/pinakes-plugin-api" } +pinakes-ui = { path = "./crates/pinakes-ui" } +pinakes-tui = { path = "./crates/pinakes-tui" } -# Pinakes itself is a REST API server. UI and TUI are official visual components -# that connect to the server. Using the API documentation, the user can write -# their own clients, but we separate "crates" and "packages" to establish the -# distinction properly. -pinakes-server = { path = "./packages/pinakes-server" } -pinakes-ui = { path = "./packages/pinakes-ui" } -pinakes-tui = { path = "./packages/pinakes-tui" } - -# Other dependencies. Declaring them in the virtual manifests lets use reuse the crates -# without having to track individual crate version across different types of crates. This -# also includes *dev* dependencies. -tokio = { version = "1.50.0", features = ["full"] } +tokio = { version = "1.49.0", features = ["full"] } tokio-util = { version = "0.7.18", features = ["rt"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -toml = "1.0.7" -clap = { version = "4.6.0", features = ["derive", "env"] } +toml = "1.0.3" +clap = { version = "4.5.60", features = ["derive", "env"] } chrono = { version = "0.4.44", features = ["serde"] } -uuid = { version = "1.22.0", features = ["v7", "serde"] } +uuid = { version = "1.21.0", features = ["v7", "serde"] } thiserror = "2.0.18" anyhow = "1.0.102" tracing = "0.1.44" -tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] } +tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } blake3 = "1.8.3" rustc-hash = "2.1.1" -ed25519-dalek = { version = "2.2.0", features = ["std"] } -lofty = "0.23.3" -lopdf = "0.40.0" +ed25519-dalek = { version = "2.1.1", features = ["std"] } +lofty = "0.23.2" +lopdf = "0.39.0" epub = "2.1.5" matroska = "0.30.0" gray_matter = "0.3.2" kamadak-exif = "0.6.1" -rusqlite = { version = "0.37.0", features = ["bundled", "column_decltype"] } +rusqlite = { version = "=0.37.0", features = ["bundled", "column_decltype"] } tokio-postgres = { version = "0.7.16", features = [ "with-uuid-1", "with-chrono-0_4", @@ -61,7 +52,7 @@ native-tls = "0.2.18" refinery = { version = "0.9.0", features = ["rusqlite", "tokio-postgres"] } walkdir = "2.5.0" notify = { version = "8.2.0", features = ["macos_fsevent"] } -winnow = "1.0.0" +winnow = "0.7.14" axum = { version = "0.8.8", features = ["macros", "multipart"] } axum-server = { version = "0.8.0" } tower = "0.5.3" @@ -76,7 +67,7 @@ dioxus = { version = "0.7.3", features = ["desktop", "router"] } dioxus-core = { version = "0.7.3" } async-trait = "0.1.89" futures = "0.3.32" -image = { version = "0.25.10", default-features = false, features = [ +image = { version = "0.25.9", default-features = false, features = [ "jpeg", "png", "webp", @@ -84,7 +75,7 @@ image = { version = "0.25.10", default-features = false, features = [ "tiff", "bmp", ] } -pulldown-cmark = "0.13.3" +pulldown-cmark = "0.13.1" ammonia = "4.1.2" argon2 = { version = "0.5.3", features = ["std"] } mime_guess = "2.0.5" @@ -93,18 +84,17 @@ dioxus-free-icons = { version = "0.10.0", features = ["font-awesome-solid"] } rfd = "0.17.2" gloo-timers = { version = "0.3.0", features = ["futures"] } rand = "0.10.0" -moka = { version = "0.12.15", features = ["future"] } +moka = { version = "0.12.14", features = ["future"] } urlencoding = "2.1.3" image_hasher = "3.1.1" percent-encoding = "2.3.2" http = "1.4.0" -wasmtime = { version = "43.0.0", features = ["component-model"] } -wit-bindgen = "0.54.0" -tempfile = "3.27.0" +wasmtime = { version = "42.0.1", features = ["component-model"] } +wit-bindgen = "0.53.1" +tempfile = "3.26.0" utoipa = { version = "5.4.0", features = ["axum_extras", "uuid", "chrono"] } utoipa-axum = { version = "0.2.0" } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } -http-body-util = "0.1.3" # See: # diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..5f5bafb --- /dev/null +++ b/HACKING.md @@ -0,0 +1,32 @@ +# Hacking Pinakes + +Pinakes is a lot of things. One of the things it aims to be is _complete_. To be +complete in features, to be complete in documentation and to be complete in +hackability. This document covers, very comprehensively, how you may: + +- Build Pinakes +- Develop Pinakes +- Contribute to Pinakes + +for developers as well as: + +- Distribute Pinakes + +for Pinakes maintainers and packagers. + +## Building Pinakes + +Pinakes is built with Rust (nightly edition) and various crates. The most +_notable_ crate among those is Dioxus, which provides its own toolkit. The UI +for Pinakes is usually _not_ built with the Dioxus CLI but instead with +`cargo build`. This also applies to distributable build results. + +[Direnv]: https://direnv.net + +To build Pinakes, simply pick the components you want and build them with +`cargo build --release --package `. A Nix shell is provided for +reproducible developer environments and you may obtain all build dependencies by +simply running `nix develop` or `direnv allow` if you use [Direnv]. Nix is a +cross-platform build tool and works on most Linux distributions as well as +Darwin. While distro-specific package managers _might_ work, Nix is the only +supported one. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 9ba5d42..0000000 Binary files a/LICENSE and /dev/null differ diff --git a/crates/pinakes-core/Cargo.toml b/crates/pinakes-core/Cargo.toml index 51b1dab..c4ba043 100644 --- a/crates/pinakes-core/Cargo.toml +++ b/crates/pinakes-core/Cargo.toml @@ -4,9 +4,6 @@ edition.workspace = true version.workspace = true license.workspace = true -[features] -ffmpeg-tests = [] - [dependencies] tokio = { workspace = true } serde = { workspace = true } @@ -46,13 +43,18 @@ moka = { workspace = true } urlencoding = { workspace = true } image_hasher = { workspace = true } rustc-hash = { workspace = true } -pinakes-plugin-api = { workspace = true } -wasmtime = { workspace = true } -ed25519-dalek = { workspace = true } + +# Plugin system +pinakes-plugin-api.workspace = true +wasmtime.workspace = true +ed25519-dalek.workspace = true + +[features] +ffmpeg-tests = [] + +[lints] +workspace = true [dev-dependencies] tempfile = { workspace = true } rand = { workspace = true } - -[lints] -workspace = true diff --git a/crates/pinakes-plugin-api/Cargo.toml b/crates/pinakes-plugin-api/Cargo.toml index b8e8f49..51a6686 100644 --- a/crates/pinakes-plugin-api/Cargo.toml +++ b/crates/pinakes-plugin-api/Cargo.toml @@ -4,25 +4,32 @@ version.workspace = true edition.workspace = true license.workspace = true -[features] -default = [] -wasm = ["wit-bindgen"] - [dependencies] +# Core dependencies serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } async-trait = { workspace = true } tracing = { workspace = true } + +# For plugin manifest parsing toml = { workspace = true } + +# For media types and identifiers uuid = { workspace = true } chrono = { workspace = true } mime_guess = { workspace = true } rustc-hash = { workspace = true } -wit-bindgen = { workspace = true, optional = true } -[dev-dependencies] -tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +# WASM bridge types +wit-bindgen = { workspace = true, optional = true } [lints] workspace = true + +[features] +default = [] +wasm = ["wit-bindgen"] + +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } diff --git a/packages/pinakes-server/Cargo.toml b/crates/pinakes-server/Cargo.toml similarity index 96% rename from packages/pinakes-server/Cargo.toml rename to crates/pinakes-server/Cargo.toml index aacccdf..14e329e 100644 --- a/packages/pinakes-server/Cargo.toml +++ b/crates/pinakes-server/Cargo.toml @@ -36,10 +36,10 @@ utoipa = { workspace = true } utoipa-axum = { workspace = true } utoipa-swagger-ui = { workspace = true } -[dev-dependencies] -http-body-util = { workspace = true } -reqwest = { workspace = true } -tempfile = { workspace = true } - [lints] workspace = true + +[dev-dependencies] +http-body-util = "0.1.3" +reqwest = { workspace = true } +tempfile = { workspace = true } diff --git a/packages/pinakes-server/src/api_doc.rs b/crates/pinakes-server/src/api_doc.rs similarity index 100% rename from packages/pinakes-server/src/api_doc.rs rename to crates/pinakes-server/src/api_doc.rs diff --git a/packages/pinakes-server/src/app.rs b/crates/pinakes-server/src/app.rs similarity index 100% rename from packages/pinakes-server/src/app.rs rename to crates/pinakes-server/src/app.rs diff --git a/packages/pinakes-server/src/auth.rs b/crates/pinakes-server/src/auth.rs similarity index 100% rename from packages/pinakes-server/src/auth.rs rename to crates/pinakes-server/src/auth.rs diff --git a/packages/pinakes-server/src/dto/analytics.rs b/crates/pinakes-server/src/dto/analytics.rs similarity index 100% rename from packages/pinakes-server/src/dto/analytics.rs rename to crates/pinakes-server/src/dto/analytics.rs diff --git a/packages/pinakes-server/src/dto/audit.rs b/crates/pinakes-server/src/dto/audit.rs similarity index 100% rename from packages/pinakes-server/src/dto/audit.rs rename to crates/pinakes-server/src/dto/audit.rs diff --git a/packages/pinakes-server/src/dto/batch.rs b/crates/pinakes-server/src/dto/batch.rs similarity index 100% rename from packages/pinakes-server/src/dto/batch.rs rename to crates/pinakes-server/src/dto/batch.rs diff --git a/packages/pinakes-server/src/dto/collections.rs b/crates/pinakes-server/src/dto/collections.rs similarity index 100% rename from packages/pinakes-server/src/dto/collections.rs rename to crates/pinakes-server/src/dto/collections.rs diff --git a/packages/pinakes-server/src/dto/config.rs b/crates/pinakes-server/src/dto/config.rs similarity index 100% rename from packages/pinakes-server/src/dto/config.rs rename to crates/pinakes-server/src/dto/config.rs diff --git a/packages/pinakes-server/src/dto/enrichment.rs b/crates/pinakes-server/src/dto/enrichment.rs similarity index 100% rename from packages/pinakes-server/src/dto/enrichment.rs rename to crates/pinakes-server/src/dto/enrichment.rs diff --git a/packages/pinakes-server/src/dto/media.rs b/crates/pinakes-server/src/dto/media.rs similarity index 100% rename from packages/pinakes-server/src/dto/media.rs rename to crates/pinakes-server/src/dto/media.rs diff --git a/packages/pinakes-server/src/dto/mod.rs b/crates/pinakes-server/src/dto/mod.rs similarity index 100% rename from packages/pinakes-server/src/dto/mod.rs rename to crates/pinakes-server/src/dto/mod.rs diff --git a/packages/pinakes-server/src/dto/playlists.rs b/crates/pinakes-server/src/dto/playlists.rs similarity index 100% rename from packages/pinakes-server/src/dto/playlists.rs rename to crates/pinakes-server/src/dto/playlists.rs diff --git a/packages/pinakes-server/src/dto/plugins.rs b/crates/pinakes-server/src/dto/plugins.rs similarity index 100% rename from packages/pinakes-server/src/dto/plugins.rs rename to crates/pinakes-server/src/dto/plugins.rs diff --git a/packages/pinakes-server/src/dto/scan.rs b/crates/pinakes-server/src/dto/scan.rs similarity index 100% rename from packages/pinakes-server/src/dto/scan.rs rename to crates/pinakes-server/src/dto/scan.rs diff --git a/packages/pinakes-server/src/dto/search.rs b/crates/pinakes-server/src/dto/search.rs similarity index 100% rename from packages/pinakes-server/src/dto/search.rs rename to crates/pinakes-server/src/dto/search.rs diff --git a/packages/pinakes-server/src/dto/sharing.rs b/crates/pinakes-server/src/dto/sharing.rs similarity index 100% rename from packages/pinakes-server/src/dto/sharing.rs rename to crates/pinakes-server/src/dto/sharing.rs diff --git a/packages/pinakes-server/src/dto/social.rs b/crates/pinakes-server/src/dto/social.rs similarity index 100% rename from packages/pinakes-server/src/dto/social.rs rename to crates/pinakes-server/src/dto/social.rs diff --git a/packages/pinakes-server/src/dto/statistics.rs b/crates/pinakes-server/src/dto/statistics.rs similarity index 100% rename from packages/pinakes-server/src/dto/statistics.rs rename to crates/pinakes-server/src/dto/statistics.rs diff --git a/packages/pinakes-server/src/dto/subtitles.rs b/crates/pinakes-server/src/dto/subtitles.rs similarity index 100% rename from packages/pinakes-server/src/dto/subtitles.rs rename to crates/pinakes-server/src/dto/subtitles.rs diff --git a/packages/pinakes-server/src/dto/sync.rs b/crates/pinakes-server/src/dto/sync.rs similarity index 100% rename from packages/pinakes-server/src/dto/sync.rs rename to crates/pinakes-server/src/dto/sync.rs diff --git a/packages/pinakes-server/src/dto/tags.rs b/crates/pinakes-server/src/dto/tags.rs similarity index 100% rename from packages/pinakes-server/src/dto/tags.rs rename to crates/pinakes-server/src/dto/tags.rs diff --git a/packages/pinakes-server/src/dto/transcode.rs b/crates/pinakes-server/src/dto/transcode.rs similarity index 100% rename from packages/pinakes-server/src/dto/transcode.rs rename to crates/pinakes-server/src/dto/transcode.rs diff --git a/packages/pinakes-server/src/dto/users.rs b/crates/pinakes-server/src/dto/users.rs similarity index 100% rename from packages/pinakes-server/src/dto/users.rs rename to crates/pinakes-server/src/dto/users.rs diff --git a/packages/pinakes-server/src/error.rs b/crates/pinakes-server/src/error.rs similarity index 100% rename from packages/pinakes-server/src/error.rs rename to crates/pinakes-server/src/error.rs diff --git a/packages/pinakes-server/src/lib.rs b/crates/pinakes-server/src/lib.rs similarity index 100% rename from packages/pinakes-server/src/lib.rs rename to crates/pinakes-server/src/lib.rs diff --git a/packages/pinakes-server/src/main.rs b/crates/pinakes-server/src/main.rs similarity index 100% rename from packages/pinakes-server/src/main.rs rename to crates/pinakes-server/src/main.rs diff --git a/packages/pinakes-server/src/routes/analytics.rs b/crates/pinakes-server/src/routes/analytics.rs similarity index 100% rename from packages/pinakes-server/src/routes/analytics.rs rename to crates/pinakes-server/src/routes/analytics.rs diff --git a/packages/pinakes-server/src/routes/audit.rs b/crates/pinakes-server/src/routes/audit.rs similarity index 100% rename from packages/pinakes-server/src/routes/audit.rs rename to crates/pinakes-server/src/routes/audit.rs diff --git a/packages/pinakes-server/src/routes/auth.rs b/crates/pinakes-server/src/routes/auth.rs similarity index 100% rename from packages/pinakes-server/src/routes/auth.rs rename to crates/pinakes-server/src/routes/auth.rs diff --git a/packages/pinakes-server/src/routes/backup.rs b/crates/pinakes-server/src/routes/backup.rs similarity index 100% rename from packages/pinakes-server/src/routes/backup.rs rename to crates/pinakes-server/src/routes/backup.rs diff --git a/packages/pinakes-server/src/routes/books.rs b/crates/pinakes-server/src/routes/books.rs similarity index 100% rename from packages/pinakes-server/src/routes/books.rs rename to crates/pinakes-server/src/routes/books.rs diff --git a/packages/pinakes-server/src/routes/collections.rs b/crates/pinakes-server/src/routes/collections.rs similarity index 100% rename from packages/pinakes-server/src/routes/collections.rs rename to crates/pinakes-server/src/routes/collections.rs diff --git a/packages/pinakes-server/src/routes/config.rs b/crates/pinakes-server/src/routes/config.rs similarity index 100% rename from packages/pinakes-server/src/routes/config.rs rename to crates/pinakes-server/src/routes/config.rs diff --git a/packages/pinakes-server/src/routes/database.rs b/crates/pinakes-server/src/routes/database.rs similarity index 100% rename from packages/pinakes-server/src/routes/database.rs rename to crates/pinakes-server/src/routes/database.rs diff --git a/packages/pinakes-server/src/routes/duplicates.rs b/crates/pinakes-server/src/routes/duplicates.rs similarity index 100% rename from packages/pinakes-server/src/routes/duplicates.rs rename to crates/pinakes-server/src/routes/duplicates.rs diff --git a/packages/pinakes-server/src/routes/enrichment.rs b/crates/pinakes-server/src/routes/enrichment.rs similarity index 100% rename from packages/pinakes-server/src/routes/enrichment.rs rename to crates/pinakes-server/src/routes/enrichment.rs diff --git a/packages/pinakes-server/src/routes/export.rs b/crates/pinakes-server/src/routes/export.rs similarity index 100% rename from packages/pinakes-server/src/routes/export.rs rename to crates/pinakes-server/src/routes/export.rs diff --git a/packages/pinakes-server/src/routes/health.rs b/crates/pinakes-server/src/routes/health.rs similarity index 100% rename from packages/pinakes-server/src/routes/health.rs rename to crates/pinakes-server/src/routes/health.rs diff --git a/packages/pinakes-server/src/routes/integrity.rs b/crates/pinakes-server/src/routes/integrity.rs similarity index 100% rename from packages/pinakes-server/src/routes/integrity.rs rename to crates/pinakes-server/src/routes/integrity.rs diff --git a/packages/pinakes-server/src/routes/jobs.rs b/crates/pinakes-server/src/routes/jobs.rs similarity index 100% rename from packages/pinakes-server/src/routes/jobs.rs rename to crates/pinakes-server/src/routes/jobs.rs diff --git a/packages/pinakes-server/src/routes/media.rs b/crates/pinakes-server/src/routes/media.rs similarity index 100% rename from packages/pinakes-server/src/routes/media.rs rename to crates/pinakes-server/src/routes/media.rs diff --git a/packages/pinakes-server/src/routes/mod.rs b/crates/pinakes-server/src/routes/mod.rs similarity index 100% rename from packages/pinakes-server/src/routes/mod.rs rename to crates/pinakes-server/src/routes/mod.rs diff --git a/packages/pinakes-server/src/routes/notes.rs b/crates/pinakes-server/src/routes/notes.rs similarity index 100% rename from packages/pinakes-server/src/routes/notes.rs rename to crates/pinakes-server/src/routes/notes.rs diff --git a/packages/pinakes-server/src/routes/photos.rs b/crates/pinakes-server/src/routes/photos.rs similarity index 100% rename from packages/pinakes-server/src/routes/photos.rs rename to crates/pinakes-server/src/routes/photos.rs diff --git a/packages/pinakes-server/src/routes/playlists.rs b/crates/pinakes-server/src/routes/playlists.rs similarity index 100% rename from packages/pinakes-server/src/routes/playlists.rs rename to crates/pinakes-server/src/routes/playlists.rs diff --git a/packages/pinakes-server/src/routes/plugins.rs b/crates/pinakes-server/src/routes/plugins.rs similarity index 100% rename from packages/pinakes-server/src/routes/plugins.rs rename to crates/pinakes-server/src/routes/plugins.rs diff --git a/packages/pinakes-server/src/routes/saved_searches.rs b/crates/pinakes-server/src/routes/saved_searches.rs similarity index 100% rename from packages/pinakes-server/src/routes/saved_searches.rs rename to crates/pinakes-server/src/routes/saved_searches.rs diff --git a/packages/pinakes-server/src/routes/scan.rs b/crates/pinakes-server/src/routes/scan.rs similarity index 100% rename from packages/pinakes-server/src/routes/scan.rs rename to crates/pinakes-server/src/routes/scan.rs diff --git a/packages/pinakes-server/src/routes/scheduled_tasks.rs b/crates/pinakes-server/src/routes/scheduled_tasks.rs similarity index 100% rename from packages/pinakes-server/src/routes/scheduled_tasks.rs rename to crates/pinakes-server/src/routes/scheduled_tasks.rs diff --git a/packages/pinakes-server/src/routes/search.rs b/crates/pinakes-server/src/routes/search.rs similarity index 100% rename from packages/pinakes-server/src/routes/search.rs rename to crates/pinakes-server/src/routes/search.rs diff --git a/packages/pinakes-server/src/routes/shares.rs b/crates/pinakes-server/src/routes/shares.rs similarity index 100% rename from packages/pinakes-server/src/routes/shares.rs rename to crates/pinakes-server/src/routes/shares.rs diff --git a/packages/pinakes-server/src/routes/social.rs b/crates/pinakes-server/src/routes/social.rs similarity index 100% rename from packages/pinakes-server/src/routes/social.rs rename to crates/pinakes-server/src/routes/social.rs diff --git a/packages/pinakes-server/src/routes/statistics.rs b/crates/pinakes-server/src/routes/statistics.rs similarity index 100% rename from packages/pinakes-server/src/routes/statistics.rs rename to crates/pinakes-server/src/routes/statistics.rs diff --git a/packages/pinakes-server/src/routes/streaming.rs b/crates/pinakes-server/src/routes/streaming.rs similarity index 100% rename from packages/pinakes-server/src/routes/streaming.rs rename to crates/pinakes-server/src/routes/streaming.rs diff --git a/packages/pinakes-server/src/routes/subtitles.rs b/crates/pinakes-server/src/routes/subtitles.rs similarity index 100% rename from packages/pinakes-server/src/routes/subtitles.rs rename to crates/pinakes-server/src/routes/subtitles.rs diff --git a/packages/pinakes-server/src/routes/sync.rs b/crates/pinakes-server/src/routes/sync.rs similarity index 100% rename from packages/pinakes-server/src/routes/sync.rs rename to crates/pinakes-server/src/routes/sync.rs diff --git a/packages/pinakes-server/src/routes/tags.rs b/crates/pinakes-server/src/routes/tags.rs similarity index 100% rename from packages/pinakes-server/src/routes/tags.rs rename to crates/pinakes-server/src/routes/tags.rs diff --git a/packages/pinakes-server/src/routes/transcode.rs b/crates/pinakes-server/src/routes/transcode.rs similarity index 100% rename from packages/pinakes-server/src/routes/transcode.rs rename to crates/pinakes-server/src/routes/transcode.rs diff --git a/packages/pinakes-server/src/routes/upload.rs b/crates/pinakes-server/src/routes/upload.rs similarity index 100% rename from packages/pinakes-server/src/routes/upload.rs rename to crates/pinakes-server/src/routes/upload.rs diff --git a/packages/pinakes-server/src/routes/users.rs b/crates/pinakes-server/src/routes/users.rs similarity index 100% rename from packages/pinakes-server/src/routes/users.rs rename to crates/pinakes-server/src/routes/users.rs diff --git a/packages/pinakes-server/src/routes/webhooks.rs b/crates/pinakes-server/src/routes/webhooks.rs similarity index 100% rename from packages/pinakes-server/src/routes/webhooks.rs rename to crates/pinakes-server/src/routes/webhooks.rs diff --git a/packages/pinakes-server/src/state.rs b/crates/pinakes-server/src/state.rs similarity index 100% rename from packages/pinakes-server/src/state.rs rename to crates/pinakes-server/src/state.rs diff --git a/packages/pinakes-server/tests/api.rs b/crates/pinakes-server/tests/api.rs similarity index 100% rename from packages/pinakes-server/tests/api.rs rename to crates/pinakes-server/tests/api.rs diff --git a/packages/pinakes-server/tests/books.rs b/crates/pinakes-server/tests/books.rs similarity index 100% rename from packages/pinakes-server/tests/books.rs rename to crates/pinakes-server/tests/books.rs diff --git a/packages/pinakes-server/tests/common/mod.rs b/crates/pinakes-server/tests/common/mod.rs similarity index 100% rename from packages/pinakes-server/tests/common/mod.rs rename to crates/pinakes-server/tests/common/mod.rs diff --git a/packages/pinakes-server/tests/e2e.rs b/crates/pinakes-server/tests/e2e.rs similarity index 100% rename from packages/pinakes-server/tests/e2e.rs rename to crates/pinakes-server/tests/e2e.rs diff --git a/packages/pinakes-server/tests/enrichment.rs b/crates/pinakes-server/tests/enrichment.rs similarity index 100% rename from packages/pinakes-server/tests/enrichment.rs rename to crates/pinakes-server/tests/enrichment.rs diff --git a/packages/pinakes-server/tests/media_ops.rs b/crates/pinakes-server/tests/media_ops.rs similarity index 100% rename from packages/pinakes-server/tests/media_ops.rs rename to crates/pinakes-server/tests/media_ops.rs diff --git a/packages/pinakes-server/tests/notes.rs b/crates/pinakes-server/tests/notes.rs similarity index 100% rename from packages/pinakes-server/tests/notes.rs rename to crates/pinakes-server/tests/notes.rs diff --git a/packages/pinakes-server/tests/plugin.rs b/crates/pinakes-server/tests/plugin.rs similarity index 100% rename from packages/pinakes-server/tests/plugin.rs rename to crates/pinakes-server/tests/plugin.rs diff --git a/packages/pinakes-server/tests/shares.rs b/crates/pinakes-server/tests/shares.rs similarity index 100% rename from packages/pinakes-server/tests/shares.rs rename to crates/pinakes-server/tests/shares.rs diff --git a/packages/pinakes-server/tests/sync.rs b/crates/pinakes-server/tests/sync.rs similarity index 100% rename from packages/pinakes-server/tests/sync.rs rename to crates/pinakes-server/tests/sync.rs diff --git a/packages/pinakes-server/tests/users.rs b/crates/pinakes-server/tests/users.rs similarity index 100% rename from packages/pinakes-server/tests/users.rs rename to crates/pinakes-server/tests/users.rs diff --git a/packages/pinakes-server/tests/webhooks.rs b/crates/pinakes-server/tests/webhooks.rs similarity index 100% rename from packages/pinakes-server/tests/webhooks.rs rename to crates/pinakes-server/tests/webhooks.rs diff --git a/packages/pinakes-tui/Cargo.toml b/crates/pinakes-tui/Cargo.toml similarity index 100% rename from packages/pinakes-tui/Cargo.toml rename to crates/pinakes-tui/Cargo.toml diff --git a/packages/pinakes-tui/src/app.rs b/crates/pinakes-tui/src/app.rs similarity index 100% rename from packages/pinakes-tui/src/app.rs rename to crates/pinakes-tui/src/app.rs diff --git a/packages/pinakes-tui/src/client.rs b/crates/pinakes-tui/src/client.rs similarity index 100% rename from packages/pinakes-tui/src/client.rs rename to crates/pinakes-tui/src/client.rs diff --git a/packages/pinakes-tui/src/event.rs b/crates/pinakes-tui/src/event.rs similarity index 100% rename from packages/pinakes-tui/src/event.rs rename to crates/pinakes-tui/src/event.rs diff --git a/packages/pinakes-tui/src/input.rs b/crates/pinakes-tui/src/input.rs similarity index 100% rename from packages/pinakes-tui/src/input.rs rename to crates/pinakes-tui/src/input.rs diff --git a/packages/pinakes-tui/src/main.rs b/crates/pinakes-tui/src/main.rs similarity index 100% rename from packages/pinakes-tui/src/main.rs rename to crates/pinakes-tui/src/main.rs diff --git a/packages/pinakes-tui/src/ui/admin.rs b/crates/pinakes-tui/src/ui/admin.rs similarity index 100% rename from packages/pinakes-tui/src/ui/admin.rs rename to crates/pinakes-tui/src/ui/admin.rs diff --git a/packages/pinakes-tui/src/ui/audit.rs b/crates/pinakes-tui/src/ui/audit.rs similarity index 100% rename from packages/pinakes-tui/src/ui/audit.rs rename to crates/pinakes-tui/src/ui/audit.rs diff --git a/packages/pinakes-tui/src/ui/books.rs b/crates/pinakes-tui/src/ui/books.rs similarity index 100% rename from packages/pinakes-tui/src/ui/books.rs rename to crates/pinakes-tui/src/ui/books.rs diff --git a/packages/pinakes-tui/src/ui/collections.rs b/crates/pinakes-tui/src/ui/collections.rs similarity index 100% rename from packages/pinakes-tui/src/ui/collections.rs rename to crates/pinakes-tui/src/ui/collections.rs diff --git a/packages/pinakes-tui/src/ui/database.rs b/crates/pinakes-tui/src/ui/database.rs similarity index 100% rename from packages/pinakes-tui/src/ui/database.rs rename to crates/pinakes-tui/src/ui/database.rs diff --git a/packages/pinakes-tui/src/ui/detail.rs b/crates/pinakes-tui/src/ui/detail.rs similarity index 100% rename from packages/pinakes-tui/src/ui/detail.rs rename to crates/pinakes-tui/src/ui/detail.rs diff --git a/packages/pinakes-tui/src/ui/duplicates.rs b/crates/pinakes-tui/src/ui/duplicates.rs similarity index 100% rename from packages/pinakes-tui/src/ui/duplicates.rs rename to crates/pinakes-tui/src/ui/duplicates.rs diff --git a/packages/pinakes-tui/src/ui/import.rs b/crates/pinakes-tui/src/ui/import.rs similarity index 100% rename from packages/pinakes-tui/src/ui/import.rs rename to crates/pinakes-tui/src/ui/import.rs diff --git a/packages/pinakes-tui/src/ui/library.rs b/crates/pinakes-tui/src/ui/library.rs similarity index 100% rename from packages/pinakes-tui/src/ui/library.rs rename to crates/pinakes-tui/src/ui/library.rs diff --git a/packages/pinakes-tui/src/ui/metadata_edit.rs b/crates/pinakes-tui/src/ui/metadata_edit.rs similarity index 100% rename from packages/pinakes-tui/src/ui/metadata_edit.rs rename to crates/pinakes-tui/src/ui/metadata_edit.rs diff --git a/packages/pinakes-tui/src/ui/mod.rs b/crates/pinakes-tui/src/ui/mod.rs similarity index 100% rename from packages/pinakes-tui/src/ui/mod.rs rename to crates/pinakes-tui/src/ui/mod.rs diff --git a/packages/pinakes-tui/src/ui/playlists.rs b/crates/pinakes-tui/src/ui/playlists.rs similarity index 100% rename from packages/pinakes-tui/src/ui/playlists.rs rename to crates/pinakes-tui/src/ui/playlists.rs diff --git a/packages/pinakes-tui/src/ui/queue.rs b/crates/pinakes-tui/src/ui/queue.rs similarity index 100% rename from packages/pinakes-tui/src/ui/queue.rs rename to crates/pinakes-tui/src/ui/queue.rs diff --git a/packages/pinakes-tui/src/ui/search.rs b/crates/pinakes-tui/src/ui/search.rs similarity index 100% rename from packages/pinakes-tui/src/ui/search.rs rename to crates/pinakes-tui/src/ui/search.rs diff --git a/packages/pinakes-tui/src/ui/settings.rs b/crates/pinakes-tui/src/ui/settings.rs similarity index 100% rename from packages/pinakes-tui/src/ui/settings.rs rename to crates/pinakes-tui/src/ui/settings.rs diff --git a/packages/pinakes-tui/src/ui/statistics.rs b/crates/pinakes-tui/src/ui/statistics.rs similarity index 100% rename from packages/pinakes-tui/src/ui/statistics.rs rename to crates/pinakes-tui/src/ui/statistics.rs diff --git a/packages/pinakes-tui/src/ui/tags.rs b/crates/pinakes-tui/src/ui/tags.rs similarity index 100% rename from packages/pinakes-tui/src/ui/tags.rs rename to crates/pinakes-tui/src/ui/tags.rs diff --git a/packages/pinakes-tui/src/ui/tasks.rs b/crates/pinakes-tui/src/ui/tasks.rs similarity index 100% rename from packages/pinakes-tui/src/ui/tasks.rs rename to crates/pinakes-tui/src/ui/tasks.rs diff --git a/packages/pinakes-ui/Cargo.toml b/crates/pinakes-ui/Cargo.toml similarity index 100% rename from packages/pinakes-ui/Cargo.toml rename to crates/pinakes-ui/Cargo.toml index 1142850..6c52e77 100644 --- a/packages/pinakes-ui/Cargo.toml +++ b/crates/pinakes-ui/Cargo.toml @@ -4,12 +4,6 @@ edition.workspace = true version.workspace = true license.workspace = true -[features] -default = ["web"] -web = ["dioxus/web"] -desktop = ["dioxus/desktop"] -mobile = ["dioxus/mobile"] - [dependencies] serde = { workspace = true } serde_json = { workspace = true } @@ -38,3 +32,9 @@ rustc-hash = { workspace = true } [lints] workspace = true + +[features] +default = ["web"] +web = ["dioxus/web"] +desktop = ["dioxus/desktop"] +mobile = ["dioxus/mobile"] diff --git a/packages/pinakes-ui/Dioxus.toml b/crates/pinakes-ui/Dioxus.toml similarity index 100% rename from packages/pinakes-ui/Dioxus.toml rename to crates/pinakes-ui/Dioxus.toml diff --git a/crates/pinakes-ui/assets/css/main.css b/crates/pinakes-ui/assets/css/main.css new file mode 100644 index 0000000..d25d383 --- /dev/null +++ b/crates/pinakes-ui/assets/css/main.css @@ -0,0 +1 @@ +@media (prefers-reduced-motion: reduce){*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important}}*{margin:0;padding:0;box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.06) rgba(0,0,0,0)}*::-webkit-scrollbar{width:5px;height:5px}*::-webkit-scrollbar-track{background:rgba(0,0,0,0)}*::-webkit-scrollbar-thumb{background:rgba(255,255,255,.06);border-radius:3px}*::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.14)}:root{--bg-0: #111118;--bg-1: #18181f;--bg-2: #1f1f28;--bg-3: #26263a;--border-subtle: rgba(255,255,255,.06);--border: rgba(255,255,255,.09);--border-strong: rgba(255,255,255,.14);--text-0: #dcdce4;--text-1: #a0a0b8;--text-2: #6c6c84;--accent: #7c7ef5;--accent-dim: rgba(124,126,245,.15);--accent-text: #9698f7;--success: #3ec97a;--error: #e45858;--warning: #d4a037;--radius-sm: 3px;--radius: 5px;--radius-md: 7px;--shadow-sm: 0 1px 3px rgba(0,0,0,.3);--shadow: 0 2px 8px rgba(0,0,0,.35);--shadow-lg: 0 4px 20px rgba(0,0,0,.45)}body{font-family:"Inter",-apple-system,"Segoe UI",system-ui,sans-serif;background:var(--bg-0);color:var(--text-0);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden}:focus-visible:focus-visible{outline:2px solid #7c7ef5;outline-offset:2px}::selection{background:rgba(124,126,245,.15);color:#9698f7}a{color:#9698f7;text-decoration:none}a:hover{text-decoration:underline}code{padding:1px 5px;border-radius:3px;background:#111118;color:#9698f7;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:11px}ul{list-style:none;padding:0}ul li{padding:3px 0;font-size:12px;color:#a0a0b8}.text-muted{color:#a0a0b8}.text-sm{font-size:11px}.mono{font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px}.flex-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px}.flex-between{display:flex;justify-content:space-between;align-items:center}.mb-16{margin-bottom:16px}.mb-8{margin-bottom:12px}.mt-16{margin-top:16px}.mt-8{margin-top:12px}@keyframes fade-in{from{opacity:0}to{opacity:1}}@keyframes slide-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes pulse{0%, 100%{opacity:1}50%{opacity:.3}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes skeleton-pulse{0%{opacity:.6}50%{opacity:.3}100%{opacity:.6}}@keyframes indeterminate{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}}.app{display:flex;flex-direction:row;justify-content:flex-start;align-items:stretch;height:100vh;overflow:hidden}.sidebar{width:220px;min-width:220px;max-width:220px;background:#18181f;border-right:1px solid rgba(255,255,255,.09);display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;flex-shrink:0;user-select:none;overflow-y:auto;overflow-x:hidden;z-index:10;transition:width .15s,min-width .15s,max-width .15s}.sidebar.collapsed{width:48px;min-width:48px;max-width:48px}.sidebar.collapsed .nav-label,.sidebar.collapsed .sidebar-header .logo,.sidebar.collapsed .sidebar-header .version,.sidebar.collapsed .nav-badge,.sidebar.collapsed .nav-item-text,.sidebar.collapsed .sidebar-footer .status-text,.sidebar.collapsed .user-name,.sidebar.collapsed .role-badge,.sidebar.collapsed .user-info .btn,.sidebar.collapsed .sidebar-import-header span,.sidebar.collapsed .sidebar-import-file{display:none}.sidebar.collapsed .nav-item{justify-content:center;padding:8px;border-left:none;border-radius:3px}.sidebar.collapsed .nav-item.active{border-left:none}.sidebar.collapsed .nav-icon{width:auto;margin:0}.sidebar.collapsed .sidebar-header{padding:12px 8px;justify-content:center}.sidebar.collapsed .nav-section{padding:0 4px}.sidebar.collapsed .sidebar-footer{padding:8px}.sidebar.collapsed .sidebar-footer .user-info{justify-content:center;padding:4px}.sidebar.collapsed .sidebar-import-progress{padding:6px}.sidebar-header{padding:16px 16px 20px;display:flex;flex-direction:row;justify-content:flex-start;align-items:baseline;gap:8px}.sidebar-header .logo{font-size:15px;font-weight:700;letter-spacing:-.4px;color:#dcdce4}.sidebar-header .version{font-size:10px;color:#6c6c84}.sidebar-toggle{background:rgba(0,0,0,0);border:none;color:#6c6c84;padding:8px;font-size:18px;width:100%;text-align:center}.sidebar-toggle:hover{color:#dcdce4}.sidebar-spacer{flex:1}.sidebar-footer{padding:12px;border-top:1px solid rgba(255,255,255,.06);overflow:visible;min-width:0}.nav-section{padding:0 8px;margin-bottom:2px}.nav-label{padding:8px 8px 4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#6c6c84}.nav-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:6px 8px;border-radius:3px;cursor:pointer;color:#a0a0b8;font-size:13px;font-weight:450;transition:color .1s,background .1s;border:none;background:none;width:100%;text-align:left;border-left:2px solid rgba(0,0,0,0);margin-left:0}.nav-item:hover{color:#dcdce4;background:rgba(255,255,255,.03)}.nav-item.active{color:#9698f7;border-left-color:#7c7ef5;background:rgba(124,126,245,.15)}.nav-item-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.sidebar:not(.collapsed) .nav-item-text{overflow:visible}.nav-icon{width:18px;text-align:center;font-size:14px;opacity:.7}.nav-badge{margin-left:auto;font-size:10px;font-weight:600;color:#6c6c84;background:#26263a;padding:1px 6px;border-radius:12px;min-width:20px;text-align:center;font-variant-numeric:tabular-nums}.status-indicator{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:6px;font-size:11px;font-weight:500;min-width:0;overflow:visible}.sidebar:not(.collapsed) .status-indicator{justify-content:flex-start}.status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.status-dot.connected{background:#3ec97a}.status-dot.disconnected{background:#e45858}.status-dot.checking{background:#d4a037;animation:pulse 1.5s infinite}.status-text{color:#6c6c84;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.sidebar:not(.collapsed) .status-text{overflow:visible}.main{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;overflow:hidden;min-width:0}.header{height:48px;min-height:48px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;padding:0 20px;background:#18181f}.page-title{font-size:14px;font-weight:600;color:#dcdce4}.header-spacer{flex:1}.content{flex:1;overflow-y:auto;padding:20px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.06) rgba(0,0,0,0)}.sidebar-import-progress{padding:10px 12px;background:#1f1f28;border-top:1px solid rgba(255,255,255,.06);font-size:11px}.sidebar-import-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;margin-bottom:4px;color:#a0a0b8}.sidebar-import-file{color:#6c6c84;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}.sidebar-import-progress .progress-bar{height:3px}.user-info{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;font-size:12px;overflow:hidden;min-width:0}.user-name{font-weight:500;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:90px;flex-shrink:1}.role-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase}.role-badge.role-admin{background:rgba(139,92,246,.1);color:#9d8be0}.role-badge.role-editor{background:rgba(34,160,80,.1);color:#5cb97a}.role-badge.role-viewer{background:rgba(59,120,200,.1);color:#6ca0d4}.btn{padding:5px 12px;border-radius:3px;border:none;cursor:pointer;font-size:12px;font-weight:500;transition:all .1s;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;line-height:1.5}.btn-primary{background:#7c7ef5;color:#fff}.btn-primary:hover{background:#8b8df7}.btn-secondary{background:#26263a;color:#dcdce4;border:1px solid rgba(255,255,255,.09)}.btn-secondary:hover{border-color:rgba(255,255,255,.14);background:rgba(255,255,255,.06)}.btn-danger{background:rgba(0,0,0,0);color:#e45858;border:1px solid rgba(228,88,88,.25)}.btn-danger:hover{background:rgba(228,88,88,.08)}.btn-ghost{background:rgba(0,0,0,0);border:none;color:#a0a0b8;padding:5px 8px}.btn-ghost:hover{color:#dcdce4;background:rgba(255,255,255,.04)}.btn-sm{padding:3px 8px;font-size:11px}.btn-icon{padding:4px;border-radius:3px;background:rgba(0,0,0,0);border:none;color:#6c6c84;cursor:pointer;transition:color .1s;font-size:13px}.btn-icon:hover{color:#dcdce4}.btn:disabled,.btn[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}.btn.btn-disabled-hint:disabled{opacity:.6;border-style:dashed;pointer-events:auto;cursor:help}.card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px}.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.card-title{font-size:14px;font-weight:600}.card-body{padding-top:8px}.data-table{width:100%;border-collapse:collapse;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;overflow:hidden}.data-table thead th{padding:8px 14px;text-align:left;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#6c6c84;border-bottom:1px solid rgba(255,255,255,.09);background:#26263a}.data-table tbody td{padding:8px 14px;font-size:13px;border-bottom:1px solid rgba(255,255,255,.06);max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.data-table tbody tr{cursor:pointer;transition:background .08s}.data-table tbody tr:hover{background:rgba(255,255,255,.02)}.data-table tbody tr.row-selected{background:rgba(99,102,241,.12)}.data-table tbody tr:last-child td{border-bottom:none}.sortable-header{cursor:pointer;user-select:none;transition:color .1s}.sortable-header:hover{color:#9698f7}input[type=text],textarea,select{padding:6px 10px;border-radius:3px;border:1px solid rgba(255,255,255,.09);background:#111118;color:#dcdce4;font-size:13px;outline:none;transition:border-color .15s;font-family:inherit}input[type=text]::placeholder,textarea::placeholder,select::placeholder{color:#6c6c84}input[type=text]:focus,textarea:focus,select:focus{border-color:#7c7ef5}input[type=text][type=number],textarea[type=number],select[type=number]{width:80px;padding:6px 8px;-moz-appearance:textfield}input[type=text][type=number]::-webkit-outer-spin-button,input[type=text][type=number]::-webkit-inner-spin-button,textarea[type=number]::-webkit-outer-spin-button,textarea[type=number]::-webkit-inner-spin-button,select[type=number]::-webkit-outer-spin-button,select[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}textarea{min-height:64px;resize:vertical}select{appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%236c6c84' d='M5 7L1 3h8z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:26px;min-width:100px}.form-group{margin-bottom:12px}.form-label{display:block;font-size:11px;font-weight:600;color:#a0a0b8;margin-bottom:4px;text-transform:uppercase;letter-spacing:.03em}.form-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-end;gap:8px}.form-row input[type=text]{flex:1}.form-label-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:2px;margin-bottom:4px}.form-label-row .form-label{margin-bottom:0}.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:50%;font-size:9px;font-weight:600;letter-spacing:.5px;text-transform:uppercase}.badge-neutral{background:rgba(255,255,255,.04);color:#a0a0b8}.badge-success{background:rgba(62,201,122,.08);color:#3ec97a}.badge-warning{background:rgba(212,160,55,.06);color:#d4a037}.badge-danger{background:rgba(228,88,88,.06);color:#d47070}.tab-bar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px;border-bottom:1px solid rgba(255,255,255,.09);margin-bottom:12px}.tab-btn{background:rgba(0,0,0,0);border:none;padding:6px 8px;font-size:13px;color:#a0a0b8;border-bottom:2px solid rgba(0,0,0,0);margin-bottom:-1px;cursor:pointer;transition:color .1s,border-color .1s}.tab-btn:hover{color:#dcdce4}.tab-btn.active{color:#9698f7;border-bottom-color:#7c7ef5}.input-sm{padding:6px 10px;border-radius:3px;border:1px solid rgba(255,255,255,.09);background:#111118;color:#dcdce4;font-size:13px;outline:none;transition:border-color .15s;font-family:inherit;padding:3px 8px;font-size:12px}.input-sm::placeholder{color:#6c6c84}.input-sm:focus{border-color:#7c7ef5}.input-suffix{display:flex;flex-direction:row;justify-content:flex-start;align-items:stretch}.input-suffix input{border-radius:3px 0 0 3px;border-right:none;flex:1}.input-suffix .btn{border-radius:0 3px 3px 0}.field-error{color:#d47070;font-size:11px;margin-top:2px}.comments-list{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px}.comment-item{padding:8px;background:#18181f;border-radius:5px;border:1px solid rgba(255,255,255,.06)}.comment-text{font-size:13px;color:#dcdce4;line-height:1.5;margin-top:4px}input[type=checkbox]{appearance:none;-webkit-appearance:none;width:16px;height:16px;border:1px solid rgba(255,255,255,.14);border-radius:3px;background:#1f1f28;cursor:pointer;position:relative;flex-shrink:0;transition:all .15s ease}input[type=checkbox]:hover{border-color:#7c7ef5;background:#26263a}input[type=checkbox]:checked{background:#7c7ef5;border-color:#7c7ef5}input[type=checkbox]:checked::after{content:"";position:absolute;left:5px;top:2px;width:4px;height:8px;border:solid #111118;border-width:0 2px 2px 0;transform:rotate(45deg)}input[type=checkbox]:focus-visible:focus-visible{outline:2px solid #7c7ef5;outline-offset:2px}.checkbox-label{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#a0a0b8;user-select:none}.checkbox-label:hover{color:#dcdce4}.checkbox-label input[type=checkbox]{margin:0}.toggle{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#dcdce4}.toggle.disabled{opacity:.4;cursor:not-allowed}.toggle-track{width:32px;height:18px;border-radius:9px;background:#26263a;border:1px solid rgba(255,255,255,.09);position:relative;transition:background .15s;flex-shrink:0}.toggle-track.active{background:#7c7ef5;border-color:#7c7ef5}.toggle-track.active .toggle-thumb{transform:translateX(14px)}.toggle-thumb{width:14px;height:14px;border-radius:50%;background:#dcdce4;position:absolute;top:1px;left:1px;transition:transform .15s}.filter-bar{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:12px;padding:12px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:5px;margin-bottom:12px}.filter-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.filter-label{font-size:11px;font-weight:500;color:#6c6c84;text-transform:uppercase;letter-spacing:.5px;margin-right:4px}.filter-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:14px;cursor:pointer;font-size:11px;color:#a0a0b8;transition:all .15s ease;user-select:none}.filter-chip:hover{background:#26263a;border-color:rgba(255,255,255,.14);color:#dcdce4}.filter-chip.active{background:rgba(124,126,245,.15);border-color:#7c7ef5;color:#9698f7}.filter-chip input[type=checkbox]{width:12px;height:12px;margin:0}.filter-chip input[type=checkbox]:checked::after{left:3px;top:1px;width:3px;height:6px}.filter-group{display:flex;align-items:center;gap:6px}.filter-group label{display:flex;align-items:center;gap:3px;cursor:pointer;color:#a0a0b8;font-size:11px;white-space:nowrap}.filter-group label:hover{color:#dcdce4}.filter-separator{width:1px;height:20px;background:rgba(255,255,255,.09);flex-shrink:0}.view-toggle{display:flex;border:1px solid rgba(255,255,255,.09);border-radius:3px;overflow:hidden}.view-btn{padding:4px 10px;background:#1f1f28;border:none;color:#6c6c84;cursor:pointer;font-size:18px;line-height:1;transition:background .1s,color .1s}.view-btn:first-child{border-right:1px solid rgba(255,255,255,.09)}.view-btn:hover{color:#dcdce4;background:#26263a}.view-btn.active{background:rgba(124,126,245,.15);color:#9698f7}.breadcrumb{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px;padding:10px 16px;font-size:.85rem;color:#6c6c84}.breadcrumb-sep{color:#6c6c84;opacity:.5}.breadcrumb-link{color:#9698f7;text-decoration:none;cursor:pointer}.breadcrumb-link:hover{text-decoration:underline}.breadcrumb-current{color:#dcdce4;font-weight:500}.progress-bar{width:100%;height:8px;background:#26263a;border-radius:4px;overflow:hidden;margin-bottom:6px}.progress-fill{height:100%;background:#7c7ef5;border-radius:4px;transition:width .3s ease}.progress-fill.indeterminate{width:30%;animation:indeterminate 1.5s ease-in-out infinite}.loading-overlay{display:flex;align-items:center;justify-content:center;padding:48px 16px;color:#6c6c84;font-size:13px;gap:10px}.spinner{width:18px;height:18px;border:2px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .7s linear infinite}.spinner-small{width:14px;height:14px;border:2px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .7s linear infinite}.spinner-tiny{width:10px;height:10px;border:1.5px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .7s linear infinite}.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:100;animation:fade-in .1s ease-out}.modal{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:20px;min-width:360px;max-width:480px;box-shadow:0 4px 20px rgba(0,0,0,.45)}.modal.wide{max-width:600px;max-height:70vh;overflow-y:auto}.modal-title{font-size:15px;font-weight:600;margin-bottom:6px}.modal-body{font-size:12px;color:#a0a0b8;margin-bottom:16px;line-height:1.5}.modal-actions{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;gap:6px}.tooltip-trigger{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;background:#26263a;color:#6c6c84;font-size:9px;font-weight:700;cursor:help;position:relative;flex-shrink:0;margin-left:4px}.tooltip-trigger:hover{background:rgba(124,126,245,.15);color:#9698f7}.tooltip-trigger:hover .tooltip-text{display:block}.tooltip-text{display:none;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);padding:6px 10px;background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#dcdce4;font-size:11px;font-weight:400;line-height:1.4;white-space:normal;width:220px;text-transform:none;letter-spacing:normal;box-shadow:0 2px 8px rgba(0,0,0,.35);z-index:100;pointer-events:none}.media-player{position:relative;background:#111118;border-radius:5px;overflow:hidden}.media-player:focus{outline:none}.media-player-audio .player-artwork{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:8px;padding:24px 16px 8px}.player-native-video{width:100%;display:block}.player-native-audio{display:none}.player-artwork img{max-width:200px;max-height:200px;border-radius:5px;object-fit:cover}.player-artwork-placeholder{width:120px;height:120px;display:flex;align-items:center;justify-content:center;background:#1f1f28;border-radius:5px;font-size:48px;opacity:.3}.player-title{font-size:13px;font-weight:500;color:#dcdce4;text-align:center}.player-controls{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:10px 14px;background:#1f1f28}.media-player-video .player-controls{position:absolute;bottom:0;left:0;right:0;background:rgba(0,0,0,.7);opacity:0;transition:opacity .2s}.media-player-video:hover .player-controls{opacity:1}.play-btn,.mute-btn,.fullscreen-btn{background:none;border:none;color:#dcdce4;cursor:pointer;font-size:18px;padding:4px;line-height:1;transition:color .1s}.play-btn:hover,.mute-btn:hover,.fullscreen-btn:hover{color:#9698f7}.player-time{font-size:11px;color:#6c6c84;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;min-width:36px;text-align:center;user-select:none}.seek-bar{flex:1;-webkit-appearance:none;appearance:none;height:4px;border-radius:4px;background:#26263a;outline:none;cursor:pointer}.seek-bar::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;border-radius:50%;background:#7c7ef5;cursor:pointer;border:none}.seek-bar::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#7c7ef5;cursor:pointer;border:none}.volume-slider{width:70px;-webkit-appearance:none;appearance:none;height:4px;border-radius:4px;background:#26263a;outline:none;cursor:pointer}.volume-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:10px;height:10px;border-radius:50%;background:#a0a0b8;cursor:pointer;border:none}.volume-slider::-moz-range-thumb{width:10px;height:10px;border-radius:50%;background:#a0a0b8;cursor:pointer;border:none}.image-viewer-overlay{position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:150;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;animation:fade-in .15s ease-out}.image-viewer-overlay:focus{outline:none}.image-viewer-toolbar{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;background:rgba(0,0,0,.5);border-bottom:1px solid rgba(255,255,255,.08);z-index:2;user-select:none}.image-viewer-toolbar-left,.image-viewer-toolbar-center,.image-viewer-toolbar-right{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px}.iv-btn{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#dcdce4;border-radius:3px;padding:4px 10px;font-size:12px;cursor:pointer;transition:background .1s}.iv-btn:hover{background:rgba(255,255,255,.12)}.iv-btn.iv-close{color:#e45858;font-weight:600}.iv-zoom-label{font-size:11px;color:#a0a0b8;min-width:40px;text-align:center;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace}.image-viewer-canvas{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}.image-viewer-canvas img{max-width:100%;max-height:100%;object-fit:contain;user-select:none;-webkit-user-drag:none}.pdf-viewer{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;height:100%;min-height:500px;background:#111118;border-radius:5px;overflow:hidden}.pdf-toolbar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;padding:10px 12px;background:#18181f;border-bottom:1px solid rgba(255,255,255,.09)}.pdf-toolbar-group{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px}.pdf-toolbar-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#a0a0b8;font-size:14px;cursor:pointer;transition:all .15s}.pdf-toolbar-btn:hover:not(:disabled){background:#26263a;color:#dcdce4}.pdf-toolbar-btn:disabled{opacity:.4;cursor:not-allowed}.pdf-zoom-label{min-width:45px;text-align:center;font-size:12px;color:#a0a0b8}.pdf-container{flex:1;position:relative;overflow:hidden;background:#1f1f28}.pdf-object{width:100%;height:100%;border:none}.pdf-loading,.pdf-error{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:12px;background:#18181f;color:#a0a0b8}.pdf-error{padding:12px;text-align:center}.pdf-fallback{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:16px;padding:48px 12px;text-align:center;color:#6c6c84}.markdown-viewer{padding:16px;text-align:left;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:12px}.markdown-toolbar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:8px;background:#1f1f28;border-radius:5px;border:1px solid rgba(255,255,255,.09)}.toolbar-btn{padding:6px 12px;border:1px solid rgba(255,255,255,.09);border-radius:3px;background:#18181f;color:#a0a0b8;font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}.toolbar-btn:hover{background:#26263a;border-color:rgba(255,255,255,.14)}.toolbar-btn.active{background:#7c7ef5;color:#fff;border-color:#7c7ef5}.markdown-source{max-width:100%;background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px;overflow-x:auto;font-family:"Menlo","Monaco","Courier New",monospace;font-size:13px;line-height:1.7;color:#dcdce4;white-space:pre-wrap;word-wrap:break-word}.markdown-source code{font-family:inherit;background:none;padding:0;border:none}.markdown-content{max-width:800px;color:#dcdce4;line-height:1.7;font-size:14px;text-align:left}.markdown-content h1{font-size:1.8em;font-weight:700;margin:1em 0 .5em;border-bottom:1px solid rgba(255,255,255,.06);padding-bottom:.3em}.markdown-content h2{font-size:1.5em;font-weight:600;margin:.8em 0 .4em;border-bottom:1px solid rgba(255,255,255,.06);padding-bottom:.2em}.markdown-content h3{font-size:1.25em;font-weight:600;margin:.6em 0 .3em}.markdown-content h4{font-size:1.1em;font-weight:600;margin:.5em 0 .25em}.markdown-content h5,.markdown-content h6{font-size:1em;font-weight:600;margin:.4em 0 .2em;color:#a0a0b8}.markdown-content p{margin:0 0 1em}.markdown-content a{color:#7c7ef5;text-decoration:none}.markdown-content a:hover{text-decoration:underline}.markdown-content pre{background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:3px;padding:12px 16px;overflow-x:auto;margin:0 0 1em;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px;line-height:1.5}.markdown-content code{background:#26263a;padding:1px 5px;border-radius:3px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:.9em}.markdown-content pre code{background:none;padding:0}.markdown-content blockquote{border-left:3px solid #7c7ef5;padding:4px 16px;margin:0 0 1em;color:#a0a0b8;background:rgba(124,126,245,.04)}.markdown-content table{width:100%;border-collapse:collapse;margin:0 0 1em}.markdown-content th,.markdown-content td{padding:6px 12px;border:1px solid rgba(255,255,255,.09);font-size:13px}.markdown-content th{background:#26263a;font-weight:600;text-align:left}.markdown-content tr:nth-child(even){background:#1f1f28}.markdown-content ul,.markdown-content ol{margin:0 0 1em;padding-left:16px}.markdown-content ul{list-style:disc}.markdown-content ol{list-style:decimal}.markdown-content li{padding:2px 0;font-size:14px;color:#dcdce4}.markdown-content hr{border:none;border-top:1px solid rgba(255,255,255,.09);margin:1.5em 0}.markdown-content img{max-width:100%;border-radius:5px}.markdown-content .footnote-definition{font-size:.85em;color:#a0a0b8;margin-top:.5em;padding-left:1.5em}.markdown-content .footnote-definition sup{color:#7c7ef5;margin-right:4px}.markdown-content sup a{color:#7c7ef5;text-decoration:none;font-size:.8em}.wikilink{color:#9698f7;text-decoration:none;border-bottom:1px dashed #7c7ef5;cursor:pointer;transition:border-color .1s,color .1s}.wikilink:hover{color:#7c7ef5;border-bottom-style:solid}.wikilink-embed{display:inline-block;padding:2px 8px;background:rgba(139,92,246,.08);border:1px dashed rgba(139,92,246,.3);border-radius:3px;color:#9d8be0;font-size:12px;cursor:default}.media-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:12px}.media-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;overflow:hidden;cursor:pointer;transition:border-color .12s,box-shadow .12s;position:relative}.media-card:hover{border-color:rgba(255,255,255,.14);box-shadow:0 1px 3px rgba(0,0,0,.3)}.media-card.selected{border-color:#7c7ef5;box-shadow:0 0 0 1px #7c7ef5}.card-checkbox{position:absolute;top:6px;left:6px;z-index:2;opacity:0;transition:opacity .1s}.card-checkbox input[type=checkbox]{width:16px;height:16px;cursor:pointer;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}.media-card:hover .card-checkbox,.media-card.selected .card-checkbox{opacity:1}.card-thumbnail{width:100%;aspect-ratio:1;background:#111118;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}.card-thumbnail img,.card-thumbnail .card-thumb-img{width:100%;height:100%;object-fit:cover;position:absolute;top:0;left:0;z-index:1}.card-type-icon{font-size:32px;opacity:.4;display:flex;align-items:center;justify-content:center;width:100%;height:100%;position:absolute;top:0;left:0;z-index:0}.card-info{padding:8px 10px}.card-name{font-size:12px;font-weight:500;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}.card-title,.card-artist{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3}.card-meta{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;font-size:10px}.card-size{color:#6c6c84;font-size:10px}.table-thumb-cell{width:36px;padding:4px 6px !important;position:relative}.table-thumb{width:28px;height:28px;object-fit:cover;border-radius:3px;display:block}.table-thumb-overlay{position:absolute;top:4px;left:6px;z-index:1}.table-type-icon{display:flex;align-items:center;justify-content:center;width:28px;height:28px;font-size:14px;opacity:.5;border-radius:3px;background:#111118;z-index:0}.type-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.type-badge.type-audio{background:rgba(139,92,246,.1);color:#9d8be0}.type-badge.type-video{background:rgba(200,72,130,.1);color:#d07eaa}.type-badge.type-image{background:rgba(34,160,80,.1);color:#5cb97a}.type-badge.type-document{background:rgba(59,120,200,.1);color:#6ca0d4}.type-badge.type-text{background:rgba(200,160,36,.1);color:#c4a840}.type-badge.type-other{background:rgba(128,128,160,.08);color:#6c6c84}.tag-list{display:flex;flex-wrap:wrap;gap:4px}.tag-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;background:rgba(124,126,245,.15);color:#9698f7;border-radius:12px;font-size:11px;font-weight:500}.tag-badge.selected{background:#7c7ef5;color:#fff;cursor:pointer}.tag-badge:not(.selected){cursor:pointer}.tag-badge .tag-remove{cursor:pointer;opacity:.4;font-size:13px;line-height:1;transition:opacity .1s}.tag-badge .tag-remove:hover{opacity:1}.tag-group{margin-bottom:6px}.tag-children{margin-left:16px;margin-top:4px;display:flex;flex-wrap:wrap;gap:4px}.tag-confirm-delete{display:inline-flex;align-items:center;gap:4px;font-size:10px;color:#a0a0b8}.tag-confirm-yes{cursor:pointer;color:#e45858;font-weight:600}.tag-confirm-yes:hover{text-decoration:underline}.tag-confirm-no{cursor:pointer;color:#6c6c84;font-weight:500}.tag-confirm-no:hover{text-decoration:underline}.detail-actions{display:flex;gap:6px;margin-bottom:16px}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.detail-field{padding:10px 12px;background:#111118;border-radius:3px;border:1px solid rgba(255,255,255,.06)}.detail-field.full-width{grid-column:1/-1}.detail-field input[type=text],.detail-field textarea,.detail-field select{width:100%;margin-top:4px}.detail-field textarea{min-height:64px;resize:vertical}.detail-label{font-size:10px;font-weight:600;color:#6c6c84;text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px}.detail-value{font-size:13px;color:#dcdce4;word-break:break-all}.detail-value.mono{font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:11px;color:#a0a0b8}.detail-preview{margin-bottom:16px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:5px;overflow:hidden;text-align:center}.detail-preview:has(.markdown-viewer){max-height:none;overflow-y:auto;text-align:left}.detail-preview:not(:has(.markdown-viewer)){max-height:450px}.detail-preview img{max-width:100%;max-height:400px;object-fit:contain;display:block;margin:0 auto}.detail-preview audio{width:100%;padding:16px}.detail-preview video{max-width:100%;max-height:400px;display:block;margin:0 auto}.detail-no-preview{padding:16px 16px;text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px}.frontmatter-card{max-width:800px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:12px 16px;margin-bottom:16px}.frontmatter-fields{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;margin:0}.frontmatter-fields dt{font-weight:600;font-size:12px;color:#a0a0b8;text-transform:capitalize}.frontmatter-fields dd{font-size:13px;color:#dcdce4;margin:0}.empty-state{text-align:center;padding:48px 12px;color:#6c6c84}.empty-state .empty-icon{font-size:32px;margin-bottom:12px;opacity:.3}.empty-title{font-size:15px;font-weight:600;color:#a0a0b8;margin-bottom:4px}.empty-subtitle{font-size:12px;max-width:320px;margin:0 auto;line-height:1.5}.toast-container{position:fixed;bottom:16px;right:16px;z-index:300;display:flex;flex-direction:column-reverse;gap:6px;align-items:flex-end}.toast-container .toast{position:static;transform:none}.toast{position:fixed;bottom:16px;right:16px;padding:10px 16px;border-radius:5px;background:#26263a;border:1px solid rgba(255,255,255,.09);color:#dcdce4;font-size:12px;box-shadow:0 2px 8px rgba(0,0,0,.35);z-index:300;animation:slide-up .15s ease-out;max-width:420px}.toast.success{border-left:3px solid #3ec97a}.toast.error{border-left:3px solid #e45858}.offline-banner,.error-banner{background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);border-radius:3px;padding:10px 12px;margin-bottom:12px;font-size:12px;color:#d47070;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px}.offline-banner .offline-icon,.offline-banner .error-icon,.error-banner .offline-icon,.error-banner .error-icon{font-size:14px;flex-shrink:0}.error-banner{padding:10px 14px}.readonly-banner{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:10px 12px;background:rgba(212,160,55,.06);border:1px solid rgba(212,160,55,.15);border-radius:3px;margin-bottom:16px;font-size:12px;color:#d4a037}.batch-actions{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:8px 10px;background:rgba(124,126,245,.15);border:1px solid rgba(124,126,245,.2);border-radius:3px;margin-bottom:12px;font-size:12px;font-weight:500;color:#9698f7}.select-all-banner{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:8px;padding:10px 16px;background:rgba(99,102,241,.08);border-radius:6px;margin-bottom:8px;font-size:.85rem;color:#a0a0b8}.select-all-banner button{background:none;border:none;color:#7c7ef5;cursor:pointer;font-weight:600;text-decoration:underline;font-size:.85rem;padding:0}.select-all-banner button:hover{color:#dcdce4}.import-status-panel{background:#1f1f28;border:1px solid #7c7ef5;border-radius:5px;padding:12px 16px;margin-bottom:16px}.import-status-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;margin-bottom:8px;font-size:13px;color:#dcdce4}.import-current-file{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:2px;margin-bottom:6px;font-size:12px;overflow:hidden}.import-file-label{color:#6c6c84;flex-shrink:0}.import-file-name{color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:monospace;font-size:11px}.import-queue-indicator{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:2px;margin-bottom:8px;font-size:11px}.import-queue-badge{display:flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 6px;background:rgba(124,126,245,.15);color:#9698f7;border-radius:9px;font-weight:600;font-size:10px}.import-queue-text{color:#6c6c84}.import-tabs{display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid rgba(255,255,255,.09)}.import-tab{padding:10px 16px;background:none;border:none;border-bottom:2px solid rgba(0,0,0,0);color:#6c6c84;font-size:12px;font-weight:500;cursor:pointer;transition:color .1s,border-color .1s}.import-tab:hover{color:#dcdce4}.import-tab.active{color:#9698f7;border-bottom-color:#7c7ef5}.queue-panel{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;border-left:1px solid rgba(255,255,255,.09);background:#18181f;min-width:280px;max-width:320px}.queue-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06)}.queue-header h3{margin:0;font-size:.9rem;color:#dcdce4}.queue-controls{display:flex;gap:2px}.queue-list{overflow-y:auto;flex:1}.queue-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;padding:8px 16px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.06);transition:background .15s}.queue-item:hover{background:#1f1f28}.queue-item:hover .queue-item-remove{opacity:1}.queue-item-active{background:rgba(124,126,245,.15);border-left:3px solid #7c7ef5}.queue-item-info{flex:1;min-width:0}.queue-item-title{display:block;font-size:.85rem;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.queue-item-artist{display:block;font-size:.75rem;color:#6c6c84}.queue-item-remove{opacity:0;transition:opacity .15s}.queue-empty{padding:16px 16px;text-align:center;color:#6c6c84;font-size:.85rem}.statistics-page{padding:20px}.stats-overview,.stats-grid{display:grid;grid-template-columns:repeat(3, 1fr);gap:16px;margin-bottom:24px}@media (max-width: 768px){.stats-overview,.stats-grid{grid-template-columns:1fr}}.stat-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:20px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:16px}.stat-card.stat-primary{border-left:3px solid #7c7ef5}.stat-card.stat-success{border-left:3px solid #3ec97a}.stat-card.stat-info{border-left:3px solid #6ca0d4}.stat-card.stat-warning{border-left:3px solid #d4a037}.stat-card.stat-purple{border-left:3px solid #9d8be0}.stat-card.stat-danger{border-left:3px solid #e45858}.stat-icon{flex-shrink:0;color:#6c6c84}.stat-content{flex:1}.stat-value{font-size:28px;font-weight:700;color:#dcdce4;line-height:1.2;font-variant-numeric:tabular-nums}.stat-label{font-size:12px;color:#6c6c84;margin-top:4px;font-weight:500}.stats-section{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px;margin-bottom:20px}.section-title{font-size:16px;font-weight:600;color:#dcdce4;margin-bottom:20px}.section-title.small{font-size:14px;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid rgba(255,255,255,.06)}.chart-bars{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:16px}.bar-item{display:grid;grid-template-columns:120px 1fr 80px;align-items:center;gap:16px}.bar-label{font-size:13px;font-weight:500;color:#a0a0b8;text-align:right}.bar-track{height:28px;background:#26263a;border-radius:3px;overflow:hidden;position:relative}.bar-fill{height:100%;transition:width .6s cubic-bezier(.4, 0, .2, 1);border-radius:3px}.bar-fill.bar-primary{background:linear-gradient(90deg, #7c7ef5 0%, #7c7ef3 100%)}.bar-fill.bar-success{background:linear-gradient(90deg, #3ec97a 0%, #66bb6a 100%)}.bar-value{font-size:13px;font-weight:600;color:#a0a0b8;text-align:right;font-variant-numeric:tabular-nums}.settings-section{margin-bottom:16px}.settings-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:20px;margin-bottom:16px}.settings-card.danger-card{border:1px solid rgba(228,88,88,.25)}.settings-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid rgba(255,255,255,.06)}.settings-card-title{font-size:14px;font-weight:600}.settings-card-body{padding-top:2px}.settings-field{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.06)}.settings-field:last-child{border-bottom:none}.settings-field select{min-width:120px}.config-path{font-size:11px;color:#6c6c84;margin-bottom:12px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;padding:6px 10px;background:#111118;border-radius:3px;border:1px solid rgba(255,255,255,.06)}.config-status{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600}.config-status.writable{background:rgba(62,201,122,.1);color:#3ec97a}.config-status.readonly{background:rgba(228,88,88,.1);color:#e45858}.root-list{list-style:none}.root-item{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:3px;margin-bottom:4px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px;color:#a0a0b8}.info-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:13px}.info-row:last-child{border-bottom:none}.info-label{color:#a0a0b8;font-weight:500}.info-value{color:#dcdce4}.tasks-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(400px, 1fr));gap:16px;padding:12px}.task-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;overflow:hidden;transition:all .2s}.task-card:hover{border-color:rgba(255,255,255,.14);box-shadow:0 4px 12px rgba(0,0,0,.08);transform:translateY(-2px)}.task-card-enabled{border-left:3px solid #3ec97a}.task-card-disabled{border-left:3px solid #4a4a5e;opacity:.7}.task-card-header{display:flex;justify-content:space-between;align-items:center;align-items:flex-start;padding:16px;border-bottom:1px solid rgba(255,255,255,.06)}.task-header-left{flex:1;min-width:0}.task-name{font-size:16px;font-weight:600;color:#dcdce4;margin-bottom:2px}.task-schedule{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;font-size:12px;color:#6c6c84;font-family:"Menlo","Monaco","Courier New",monospace}.schedule-icon{font-size:14px}.task-status-badge{flex-shrink:0}.status-badge{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;padding:2px 10px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}.status-badge.status-enabled{background:rgba(76,175,80,.12);color:#3ec97a}.status-badge.status-enabled .status-dot{animation:pulse 1.5s infinite}.status-badge.status-disabled{background:#26263a;color:#6c6c84}.status-badge .status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:currentColor}.task-info-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(120px, 1fr));gap:12px;padding:16px}.task-info-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;gap:10px}.task-info-icon{font-size:18px;color:#6c6c84;flex-shrink:0}.task-info-content{flex:1;min-width:0}.task-info-label{font-size:10px;color:#6c6c84;font-weight:600;text-transform:uppercase;letter-spacing:.03em;margin-bottom:2px}.task-info-value{font-size:12px;color:#a0a0b8;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.task-card-actions{display:flex;gap:8px;padding:10px 16px;background:#18181f;border-top:1px solid rgba(255,255,255,.06)}.task-card-actions button{flex:1}.db-actions{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:16px;padding:10px}.db-action-row{display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:16px;padding:10px;border-radius:6px;background:rgba(0,0,0,.06)}.db-action-info{flex:1}.db-action-info h4{font-size:.95rem;font-weight:600;color:#dcdce4;margin-bottom:2px}.db-action-confirm{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;flex-shrink:0}.library-toolbar{display:flex;justify-content:space-between;align-items:center;padding:8px 0;margin-bottom:12px;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:10px}.toolbar-right{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:10px}.sort-control select,.page-size-control select{padding:4px 24px 4px 8px;font-size:11px;background:#1f1f28}.page-size-control{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px}.library-stats{display:flex;justify-content:space-between;align-items:center;padding:2px 0 6px 0;font-size:11px}.type-filter-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;padding:4px 0;margin-bottom:6px;flex-wrap:wrap}.pagination{display:flex;align-items:center;justify-content:center;gap:4px;margin-top:16px;padding:8px 0}.page-btn{min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.page-ellipsis{color:#6c6c84;padding:0 4px;font-size:12px;user-select:none}.audit-controls{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;margin-bottom:12px}.filter-select{padding:4px 24px 4px 8px;font-size:11px;background:#1f1f28}.action-danger{background:rgba(228,88,88,.1);color:#d47070}.action-updated{background:rgba(59,120,200,.1);color:#6ca0d4}.action-collection{background:rgba(34,160,80,.1);color:#5cb97a}.action-collection-remove{background:rgba(212,160,55,.1);color:#c4a840}.action-opened{background:rgba(139,92,246,.1);color:#9d8be0}.action-scanned{background:rgba(128,128,160,.08);color:#6c6c84}.clickable{cursor:pointer;color:#9698f7}.clickable:hover{text-decoration:underline}.clickable-row{cursor:pointer}.clickable-row:hover{background:rgba(255,255,255,.03)}.duplicates-view{padding:0}.duplicates-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.duplicates-header h3{margin:0}.duplicates-summary{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px}.duplicate-group{border:1px solid rgba(255,255,255,.09);border-radius:5px;margin-bottom:8px;overflow:hidden}.duplicate-group-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;width:100%;padding:10px 14px;background:#1f1f28;border:none;cursor:pointer;text-align:left;color:#dcdce4;font-size:13px}.duplicate-group-header:hover{background:#26263a}.expand-icon{font-size:10px;width:14px;flex-shrink:0}.group-name{font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.group-badge{background:#7c7ef5;color:#fff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;flex-shrink:0}.group-size{flex-shrink:0;font-size:12px}.group-hash{font-size:11px;flex-shrink:0}.duplicate-items{border-top:1px solid rgba(255,255,255,.09)}.duplicate-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.06)}.duplicate-item:last-child{border-bottom:none}.duplicate-item-keep{background:rgba(76,175,80,.06)}.dup-thumb{width:48px;height:48px;flex-shrink:0;border-radius:3px;overflow:hidden}.dup-thumb-img{width:100%;height:100%;object-fit:cover}.dup-thumb-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#26263a;font-size:20px;color:#6c6c84}.dup-info{flex:1;min-width:0}.dup-filename{font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dup-path{font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dup-meta{font-size:12px;margin-top:2px}.dup-actions{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;flex-shrink:0}.keep-badge{background:rgba(76,175,80,.12);color:#4caf50;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600}.saved-searches-list{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:4px;max-height:300px;overflow-y:auto}.saved-search-item{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#18181f;border-radius:3px;cursor:pointer;transition:background .15s ease}.saved-search-item:hover{background:#1f1f28}.saved-search-info{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:2px;flex:1;min-width:0}.saved-search-name{font-weight:500;color:#dcdce4}.saved-search-query{font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.backlinks-panel,.outgoing-links-panel{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;margin-top:16px;overflow:hidden}.backlinks-header,.outgoing-links-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:10px 14px;background:#26263a;cursor:pointer;user-select:none;transition:background .1s}.backlinks-header:hover,.outgoing-links-header:hover{background:rgba(255,255,255,.04)}.backlinks-toggle,.outgoing-links-toggle{font-size:10px;color:#6c6c84;width:12px;text-align:center}.backlinks-title,.outgoing-links-title{font-size:12px;font-weight:600;color:#dcdce4;flex:1}.backlinks-count,.outgoing-links-count{font-size:11px;color:#6c6c84}.backlinks-reindex-btn{display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;margin-left:auto;background:rgba(0,0,0,0);border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#6c6c84;font-size:12px;cursor:pointer;transition:background .1s,color .1s,border-color .1s}.backlinks-reindex-btn:hover:not(:disabled){background:#1f1f28;color:#dcdce4;border-color:rgba(255,255,255,.14)}.backlinks-reindex-btn:disabled{opacity:.5;cursor:not-allowed}.backlinks-content,.outgoing-links-content{padding:12px;border-top:1px solid rgba(255,255,255,.06)}.backlinks-loading,.outgoing-links-loading{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:12px;color:#6c6c84;font-size:12px}.backlinks-error,.outgoing-links-error{padding:8px 12px;background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);border-radius:3px;font-size:12px;color:#e45858}.backlinks-empty,.outgoing-links-empty{padding:16px;text-align:center;color:#6c6c84;font-size:12px;font-style:italic}.backlinks-list,.outgoing-links-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:6px}.backlink-item,.outgoing-link-item{padding:10px 12px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:3px;cursor:pointer;transition:background .1s,border-color .1s}.backlink-item:hover,.outgoing-link-item:hover{background:#18181f;border-color:rgba(255,255,255,.09)}.backlink-item.unresolved,.outgoing-link-item.unresolved{opacity:.7;border-style:dashed}.backlink-source,.outgoing-link-target{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;margin-bottom:2px}.backlink-title,.outgoing-link-text{font-size:13px;font-weight:500;color:#dcdce4;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.backlink-type-badge,.outgoing-link-type-badge{display:inline-block;padding:1px 6px;border-radius:12px;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}.backlink-type-badge.backlink-type-wikilink,.backlink-type-badge.link-type-wikilink,.outgoing-link-type-badge.backlink-type-wikilink,.outgoing-link-type-badge.link-type-wikilink{background:rgba(124,126,245,.15);color:#9698f7}.backlink-type-badge.backlink-type-embed,.backlink-type-badge.link-type-embed,.outgoing-link-type-badge.backlink-type-embed,.outgoing-link-type-badge.link-type-embed{background:rgba(139,92,246,.1);color:#9d8be0}.backlink-type-badge.backlink-type-markdown_link,.backlink-type-badge.link-type-markdown_link,.outgoing-link-type-badge.backlink-type-markdown_link,.outgoing-link-type-badge.link-type-markdown_link{background:rgba(59,120,200,.1);color:#6ca0d4}.backlink-context{font-size:11px;color:#6c6c84;line-height:1.4;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.backlink-line{color:#a0a0b8;font-weight:500}.unresolved-badge{padding:1px 6px;border-radius:12px;font-size:9px;font-weight:600;background:rgba(212,160,55,.1);color:#d4a037}.outgoing-links-unresolved-badge{margin-left:8px;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:500;background:rgba(212,160,55,.12);color:#d4a037}.outgoing-links-global-unresolved{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;margin-top:12px;padding:10px 12px;background:rgba(212,160,55,.06);border:1px solid rgba(212,160,55,.15);border-radius:3px;font-size:11px;color:#6c6c84}.outgoing-links-global-unresolved .unresolved-icon{color:#d4a037}.backlinks-message{padding:8px 10px;margin-bottom:10px;border-radius:3px;font-size:11px}.backlinks-message.success{background:rgba(62,201,122,.08);border:1px solid rgba(62,201,122,.2);color:#3ec97a}.backlinks-message.error{background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);color:#e45858}.graph-view{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;height:100%;background:#18181f;border-radius:5px;overflow:hidden}.graph-toolbar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:16px;padding:12px 16px;background:#1f1f28;border-bottom:1px solid rgba(255,255,255,.09)}.graph-title{font-size:14px;font-weight:600;color:#dcdce4}.graph-controls{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;font-size:12px;color:#a0a0b8}.graph-controls select{padding:4px 20px 4px 8px;font-size:11px;background:#26263a}.graph-stats{margin-left:auto;font-size:11px;color:#6c6c84}.graph-container{flex:1;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#111118}.graph-loading,.graph-error,.graph-empty{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;padding:48px;color:#6c6c84;font-size:13px;text-align:center}.graph-svg{max-width:100%;max-height:100%;cursor:grab}.graph-svg-container{position:relative;width:100%;height:100%}.graph-zoom-controls{position:absolute;top:16px;left:16px;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px;z-index:5}.zoom-btn{width:36px;height:36px;border-radius:6px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);color:#dcdce4;font-size:18px;font-weight:bold;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s;box-shadow:0 1px 3px rgba(0,0,0,.3)}.zoom-btn:hover{background:#26263a;border-color:rgba(255,255,255,.14);transform:scale(1.05)}.zoom-btn:active{transform:scale(.95)}.graph-edges line{stroke:rgba(255,255,255,.14);stroke-width:1;opacity:.6}.graph-edges line.edge-type-wikilink{stroke:#7c7ef5}.graph-edges line.edge-type-embed{stroke:#9d8be0;stroke-dasharray:4 2}.graph-nodes .graph-node{cursor:pointer}.graph-nodes .graph-node circle{fill:#4caf50;stroke:#388e3c;stroke-width:2;transition:fill .15s,stroke .15s}.graph-nodes .graph-node:hover circle{fill:#66bb6a}.graph-nodes .graph-node.selected circle{fill:#7c7ef5;stroke:#5456d6}.graph-nodes .graph-node text{fill:#a0a0b8;font-size:11px;pointer-events:none;text-anchor:middle;dominant-baseline:central;transform:translateY(16px)}.node-details-panel{position:absolute;top:16px;right:16px;width:280px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.35);z-index:10}.node-details-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.06)}.node-details-header h3{font-size:13px;font-weight:600;color:#dcdce4;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.node-details-header .close-btn{background:none;border:none;color:#6c6c84;cursor:pointer;font-size:14px;padding:2px 6px;line-height:1}.node-details-header .close-btn:hover{color:#dcdce4}.node-details-content{padding:14px}.node-details-content .node-title{font-size:12px;color:#a0a0b8;margin-bottom:12px}.node-stats{display:flex;gap:16px;margin-bottom:12px}.node-stats .stat{font-size:12px;color:#6c6c84}.node-stats .stat strong{color:#dcdce4}.physics-controls-panel{position:absolute;top:16px;right:16px;width:300px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.35);padding:16px;z-index:10}.physics-controls-panel h4{font-size:13px;font-weight:600;color:#dcdce4;margin:0 0 16px 0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.06)}.physics-controls-panel .btn{width:100%;margin-top:8px}.control-group{margin-bottom:14px}.control-group label{display:block;font-size:11px;font-weight:500;color:#a0a0b8;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.control-group input[type=range]{width:100%;height:4px;border-radius:4px;background:#26263a;outline:none;-webkit-appearance:none}.control-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#7c7ef5;cursor:pointer;transition:transform .1s}.control-group input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.15)}.control-group input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#7c7ef5;cursor:pointer;border:none;transition:transform .1s}.control-group input[type=range]::-moz-range-thumb:hover{transform:scale(1.15)}.control-value{display:inline-block;margin-top:2px;font-size:11px;color:#6c6c84;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace}.theme-light{--bg-0: #f5f5f7;--bg-1: #eeeef0;--bg-2: #fff;--bg-3: #e8e8ec;--border-subtle: rgba(0,0,0,.06);--border: rgba(0,0,0,.1);--border-strong: rgba(0,0,0,.16);--text-0: #1a1a2e;--text-1: #555570;--text-2: #8888a0;--accent: #6366f1;--accent-dim: rgba(99,102,241,.1);--accent-text: #4f52e8;--shadow-sm: 0 1px 3px rgba(0,0,0,.08);--shadow: 0 2px 8px rgba(0,0,0,.1);--shadow-lg: 0 4px 20px rgba(0,0,0,.12)}.theme-light ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12)}.theme-light ::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.08)}.theme-light ::-webkit-scrollbar-track{background:rgba(0,0,0,.06)}.theme-light .graph-nodes .graph-node text{fill:#1a1a2e}.theme-light .graph-edges line{stroke:rgba(0,0,0,.12)}.theme-light .pdf-container{background:#e8e8ec}.skeleton-pulse{animation:skeleton-pulse 1.5s ease-in-out infinite;background:#26263a;border-radius:4px}.skeleton-card{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px;padding:8px}.skeleton-thumb{width:100%;aspect-ratio:1;border-radius:6px}.skeleton-text{height:14px;width:80%}.skeleton-text-short{width:50%}.skeleton-row{display:flex;gap:12px;padding:10px 16px;align-items:center}.skeleton-cell{height:14px;flex:1;border-radius:4px}.skeleton-cell-icon{width:32px;height:32px;flex:none;border-radius:4px}.skeleton-cell-wide{flex:3}.loading-overlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;background:rgba(0,0,0,.3);z-index:100;border-radius:8px}.loading-spinner{width:32px;height:32px;border:3px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .8s linear infinite}.loading-message{color:#a0a0b8;font-size:.9rem}.login-container{display:flex;align-items:center;justify-content:center;height:100vh;background:#111118}.login-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:24px;width:360px;box-shadow:0 4px 20px rgba(0,0,0,.45)}.login-title{font-size:20px;font-weight:700;color:#dcdce4;text-align:center;margin-bottom:2px}.login-subtitle{font-size:13px;color:#6c6c84;text-align:center;margin-bottom:20px}.login-error{background:rgba(228,88,88,.08);border:1px solid rgba(228,88,88,.2);border-radius:3px;padding:8px 12px;margin-bottom:12px;font-size:12px;color:#e45858}.login-form input[type=text],.login-form input[type=password]{width:100%}.login-btn{width:100%;padding:8px 16px;font-size:13px;margin-top:2px}.pagination{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:2px;margin-top:16px;padding:8px 0}.page-btn{min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.page-ellipsis{color:#6c6c84;padding:0 4px;font-size:12px;user-select:none}.help-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:200;animation:fade-in .1s ease-out}.help-dialog{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:16px;min-width:300px;max-width:400px;box-shadow:0 4px 20px rgba(0,0,0,.45)}.help-dialog h3{font-size:16px;font-weight:600;margin-bottom:16px}.help-shortcuts{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px;margin-bottom:16px}.shortcut-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px}.shortcut-row kbd{display:inline-block;padding:2px 8px;background:#111118;border:1px solid rgba(255,255,255,.09);border-radius:3px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:11px;color:#dcdce4;min-width:32px;text-align:center}.shortcut-row span{font-size:13px;color:#a0a0b8}.help-close{display:block;width:100%;padding:6px 12px;background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#dcdce4;font-size:12px;cursor:pointer;text-align:center}.help-close:hover{background:rgba(255,255,255,.06)}.plugin-page{padding:16px 24px;max-width:100%;overflow-x:hidden}.plugin-page-title{font-size:14px;font-weight:600;color:#dcdce4;margin:0 0 16px}.plugin-container{display:flex;flex-direction:column;gap:var(--plugin-gap, 0px);padding:var(--plugin-padding, 0)}.plugin-grid{display:grid;grid-template-columns:repeat(var(--plugin-columns, 1), 1fr);gap:var(--plugin-gap, 0px)}.plugin-flex{display:flex;gap:var(--plugin-gap, 0px)}.plugin-flex[data-direction=row]{flex-direction:row}.plugin-flex[data-direction=column]{flex-direction:column}.plugin-flex[data-justify=flex-start]{justify-content:flex-start}.plugin-flex[data-justify=flex-end]{justify-content:flex-end}.plugin-flex[data-justify=center]{justify-content:center}.plugin-flex[data-justify=space-between]{justify-content:space-between}.plugin-flex[data-justify=space-around]{justify-content:space-around}.plugin-flex[data-justify=space-evenly]{justify-content:space-evenly}.plugin-flex[data-align=flex-start]{align-items:flex-start}.plugin-flex[data-align=flex-end]{align-items:flex-end}.plugin-flex[data-align=center]{align-items:center}.plugin-flex[data-align=stretch]{align-items:stretch}.plugin-flex[data-align=baseline]{align-items:baseline}.plugin-flex[data-wrap=wrap]{flex-wrap:wrap}.plugin-flex[data-wrap=nowrap]{flex-wrap:nowrap}.plugin-split{display:flex}.plugin-split-sidebar{width:var(--plugin-sidebar-width, 200px);flex-shrink:0}.plugin-split-main{flex:1;min-width:0}.plugin-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;overflow:hidden}.plugin-card-header{padding:12px 16px;font-size:12px;font-weight:600;color:#dcdce4;border-bottom:1px solid rgba(255,255,255,.09);background:#26263a}.plugin-card-content{padding:16px}.plugin-card-footer{padding:12px 16px;border-top:1px solid rgba(255,255,255,.09);background:#18181f}.plugin-heading{color:#dcdce4;margin:0;line-height:1.2}.plugin-heading.level-1{font-size:28px;font-weight:700}.plugin-heading.level-2{font-size:18px;font-weight:600}.plugin-heading.level-3{font-size:16px;font-weight:600}.plugin-heading.level-4{font-size:14px;font-weight:500}.plugin-heading.level-5{font-size:13px;font-weight:500}.plugin-heading.level-6{font-size:12px;font-weight:500}.plugin-text{margin:0;font-size:12px;color:#dcdce4;line-height:1.4}.plugin-text.text-secondary{color:#a0a0b8}.plugin-text.text-error{color:#d47070}.plugin-text.text-success{color:#3ec97a}.plugin-text.text-warning{color:#d4a037}.plugin-text.text-bold{font-weight:600}.plugin-text.text-italic{font-style:italic}.plugin-text.text-small{font-size:10px}.plugin-text.text-large{font-size:15px}.plugin-code{background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px 24px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px;color:#dcdce4;overflow-x:auto;white-space:pre}.plugin-code code{font-family:inherit;font-size:inherit;color:inherit}.plugin-tabs{display:flex;flex-direction:column}.plugin-tab-list{display:flex;gap:2px;border-bottom:1px solid rgba(255,255,255,.09);margin-bottom:16px}.plugin-tab{padding:8px 20px;font-size:12px;font-weight:500;color:#a0a0b8;background:rgba(0,0,0,0);border:none;border-bottom:2px solid rgba(0,0,0,0);cursor:pointer;transition:color .1s,border-color .1s}.plugin-tab:hover{color:#dcdce4}.plugin-tab.active{color:#9698f7;border-bottom-color:#7c7ef5}.plugin-tab .tab-icon{margin-right:4px}.plugin-tab-panel:not(.active){display:none}.plugin-description-list-wrapper{width:100%}.plugin-description-list{display:grid;grid-template-columns:max-content 1fr;gap:4px 16px;margin:0;padding:0}.plugin-description-list dt{font-size:10px;font-weight:500;color:#a0a0b8;text-transform:uppercase;letter-spacing:.5px;padding:6px 0;white-space:nowrap}.plugin-description-list dd{font-size:12px;color:#dcdce4;padding:6px 0;margin:0;word-break:break-word}.plugin-description-list.horizontal{display:flex;flex-wrap:wrap;gap:16px 24px;display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr))}.plugin-description-list.horizontal dt{width:auto;padding:0}.plugin-description-list.horizontal dd{width:auto;padding:0}.plugin-description-list.horizontal dt,.plugin-description-list.horizontal dd{display:inline}.plugin-description-list.horizontal dt{font-size:9px;text-transform:uppercase;letter-spacing:.5px;color:#6c6c84;margin-bottom:2px}.plugin-description-list.horizontal dd{font-size:13px;font-weight:600;color:#dcdce4}.plugin-data-table-wrapper{overflow-x:auto}.plugin-data-table{width:100%;border-collapse:collapse;font-size:12px}.plugin-data-table thead tr{border-bottom:1px solid rgba(255,255,255,.14)}.plugin-data-table thead th{padding:8px 12px;text-align:left;font-size:10px;font-weight:600;color:#a0a0b8;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}.plugin-data-table tbody tr{border-bottom:1px solid rgba(255,255,255,.06);transition:background .08s}.plugin-data-table tbody tr:hover{background:rgba(255,255,255,.03)}.plugin-data-table tbody tr:last-child{border-bottom:none}.plugin-data-table tbody td{padding:8px 12px;color:#dcdce4;vertical-align:middle}.plugin-col-constrained{width:var(--plugin-col-width)}.table-filter{margin-bottom:12px}.table-filter input{width:240px;padding:6px 12px;background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;color:#dcdce4;font-size:12px}.table-filter input::placeholder{color:#6c6c84}.table-filter input:focus{outline:none;border-color:#7c7ef5}.table-pagination{display:flex;align-items:center;gap:12px;padding:8px 0;font-size:12px;color:#a0a0b8}.row-actions{white-space:nowrap;width:1%}.row-actions .plugin-button{padding:4px 8px;font-size:10px;margin-right:4px}.plugin-media-grid{display:grid;grid-template-columns:repeat(var(--plugin-columns, 2), 1fr);gap:var(--plugin-gap, 8px)}.media-grid-item{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;overflow:hidden;display:flex;flex-direction:column}.media-grid-img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}.media-grid-no-img{width:100%;aspect-ratio:16/9;background:#26263a;display:flex;align-items:center;justify-content:center;font-size:10px;color:#6c6c84}.media-grid-caption{padding:8px 12px;font-size:10px;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.plugin-list{list-style:none;margin:0;padding:0}.plugin-list-item{padding:8px 0}.plugin-list-divider{border:none;border-top:1px solid rgba(255,255,255,.06);margin:0}.plugin-list-empty{padding:16px;text-align:center;color:#6c6c84;font-size:12px}.plugin-button{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:1px solid rgba(255,255,255,.09);border-radius:5px;font-size:12px;font-weight:500;cursor:pointer;transition:background .08s,border-color .08s,color .08s;background:#1f1f28;color:#dcdce4}.plugin-button:disabled{opacity:.45;cursor:not-allowed}.plugin-button.btn-primary{background:#7c7ef5;border-color:#7c7ef5;color:#fff}.plugin-button.btn-primary:hover:not(:disabled){background:#8b8df7}.plugin-button.btn-secondary{background:#26263a;border-color:rgba(255,255,255,.14);color:#dcdce4}.plugin-button.btn-secondary:hover:not(:disabled){background:rgba(255,255,255,.04)}.plugin-button.btn-tertiary{background:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:#9698f7}.plugin-button.btn-tertiary:hover:not(:disabled){background:rgba(124,126,245,.15)}.plugin-button.btn-danger{background:rgba(0,0,0,0);border-color:rgba(228,88,88,.2);color:#d47070}.plugin-button.btn-danger:hover:not(:disabled){background:rgba(228,88,88,.06)}.plugin-button.btn-success{background:rgba(0,0,0,0);border-color:rgba(62,201,122,.2);color:#3ec97a}.plugin-button.btn-success:hover:not(:disabled){background:rgba(62,201,122,.08)}.plugin-button.btn-ghost{background:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:#a0a0b8}.plugin-button.btn-ghost:hover:not(:disabled){background:rgba(255,255,255,.04)}.plugin-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:50%;font-size:9px;font-weight:600;letter-spacing:.5px;text-transform:uppercase}.plugin-badge.badge-default,.plugin-badge.badge-neutral{background:rgba(255,255,255,.04);color:#a0a0b8}.plugin-badge.badge-primary{background:rgba(124,126,245,.15);color:#9698f7}.plugin-badge.badge-secondary{background:rgba(255,255,255,.03);color:#dcdce4}.plugin-badge.badge-success{background:rgba(62,201,122,.08);color:#3ec97a}.plugin-badge.badge-warning{background:rgba(212,160,55,.06);color:#d4a037}.plugin-badge.badge-error{background:rgba(228,88,88,.06);color:#d47070}.plugin-badge.badge-info{background:rgba(99,102,241,.08);color:#9698f7}.plugin-form{display:flex;flex-direction:column;gap:16px}.form-field{display:flex;flex-direction:column;gap:6px}.form-field label{font-size:12px;font-weight:500;color:#dcdce4}.form-field input,.form-field textarea,.form-field select{padding:8px 12px;background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;color:#dcdce4;font-size:12px;font-family:inherit}.form-field input::placeholder,.form-field textarea::placeholder,.form-field select::placeholder{color:#6c6c84}.form-field input:focus,.form-field textarea:focus,.form-field select:focus{outline:none;border-color:#7c7ef5;box-shadow:0 0 0 2px rgba(124,126,245,.15)}.form-field textarea{min-height:80px;resize:vertical}.form-field select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a0a0b8' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px}.form-help{margin:0;font-size:10px;color:#6c6c84}.form-actions{display:flex;gap:12px;padding-top:8px}.required{color:#e45858}.plugin-link{color:#9698f7;text-decoration:none}.plugin-link:hover{text-decoration:underline}.plugin-link-blocked{color:#6c6c84;text-decoration:line-through;cursor:not-allowed}.plugin-progress{background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;height:8px;overflow:hidden;display:flex;align-items:center;gap:8px}.plugin-progress-bar{height:100%;background:#7c7ef5;border-radius:4px;transition:width .3s ease;width:var(--plugin-progress, 0%)}.plugin-progress-label{font-size:10px;color:#a0a0b8;white-space:nowrap;flex-shrink:0}.plugin-chart{overflow:auto;height:var(--plugin-chart-height, 200px)}.plugin-chart .chart-title{font-size:13px;font-weight:600;color:#dcdce4;margin-bottom:8px}.plugin-chart .chart-x-label,.plugin-chart .chart-y-label{font-size:10px;color:#6c6c84;margin-bottom:4px}.plugin-chart .chart-data-table{overflow-x:auto}.plugin-chart .chart-no-data{padding:24px;text-align:center;color:#6c6c84;font-size:12px}.plugin-loading{padding:16px;color:#a0a0b8;font-size:12px;font-style:italic}.plugin-error{padding:12px 16px;background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);border-radius:5px;color:#d47070;font-size:12px}.plugin-feedback{position:sticky;bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 16px;border-radius:7px;font-size:12px;z-index:300;box-shadow:0 4px 20px rgba(0,0,0,.45)}.plugin-feedback.success{background:rgba(62,201,122,.08);border:1px solid rgba(62,201,122,.2);color:#3ec97a}.plugin-feedback.error{background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);color:#d47070}.plugin-feedback-dismiss{background:rgba(0,0,0,0);border:none;color:inherit;font-size:14px;cursor:pointer;line-height:1;padding:0;opacity:.7}.plugin-feedback-dismiss:hover{opacity:1}.plugin-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;z-index:100}.plugin-modal{position:relative;background:#1f1f28;border:1px solid rgba(255,255,255,.14);border-radius:12px;padding:32px;min-width:380px;max-width:640px;max-height:80vh;overflow-y:auto;box-shadow:0 4px 20px rgba(0,0,0,.45);z-index:200}.plugin-modal-close{position:absolute;top:16px;right:16px;background:rgba(0,0,0,0);border:none;color:#a0a0b8;font-size:14px;cursor:pointer;line-height:1;padding:4px;border-radius:5px}.plugin-modal-close:hover{background:rgba(255,255,255,.04);color:#dcdce4} \ No newline at end of file diff --git a/packages/pinakes-ui/assets/styles/_audit.scss b/crates/pinakes-ui/assets/styles/_audit.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_audit.scss rename to crates/pinakes-ui/assets/styles/_audit.scss diff --git a/packages/pinakes-ui/assets/styles/_base.scss b/crates/pinakes-ui/assets/styles/_base.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_base.scss rename to crates/pinakes-ui/assets/styles/_base.scss diff --git a/packages/pinakes-ui/assets/styles/_components.scss b/crates/pinakes-ui/assets/styles/_components.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_components.scss rename to crates/pinakes-ui/assets/styles/_components.scss diff --git a/packages/pinakes-ui/assets/styles/_graph.scss b/crates/pinakes-ui/assets/styles/_graph.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_graph.scss rename to crates/pinakes-ui/assets/styles/_graph.scss diff --git a/packages/pinakes-ui/assets/styles/_layout.scss b/crates/pinakes-ui/assets/styles/_layout.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_layout.scss rename to crates/pinakes-ui/assets/styles/_layout.scss diff --git a/packages/pinakes-ui/assets/styles/_media.scss b/crates/pinakes-ui/assets/styles/_media.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_media.scss rename to crates/pinakes-ui/assets/styles/_media.scss diff --git a/packages/pinakes-ui/assets/styles/_mixins.scss b/crates/pinakes-ui/assets/styles/_mixins.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_mixins.scss rename to crates/pinakes-ui/assets/styles/_mixins.scss diff --git a/packages/pinakes-ui/assets/styles/_plugins.scss b/crates/pinakes-ui/assets/styles/_plugins.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_plugins.scss rename to crates/pinakes-ui/assets/styles/_plugins.scss diff --git a/packages/pinakes-ui/assets/styles/_sections.scss b/crates/pinakes-ui/assets/styles/_sections.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_sections.scss rename to crates/pinakes-ui/assets/styles/_sections.scss diff --git a/packages/pinakes-ui/assets/styles/_themes.scss b/crates/pinakes-ui/assets/styles/_themes.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_themes.scss rename to crates/pinakes-ui/assets/styles/_themes.scss diff --git a/packages/pinakes-ui/assets/styles/_variables.scss b/crates/pinakes-ui/assets/styles/_variables.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/_variables.scss rename to crates/pinakes-ui/assets/styles/_variables.scss diff --git a/packages/pinakes-ui/assets/styles/main.scss b/crates/pinakes-ui/assets/styles/main.scss similarity index 100% rename from packages/pinakes-ui/assets/styles/main.scss rename to crates/pinakes-ui/assets/styles/main.scss diff --git a/packages/pinakes-ui/src/app.rs b/crates/pinakes-ui/src/app.rs similarity index 100% rename from packages/pinakes-ui/src/app.rs rename to crates/pinakes-ui/src/app.rs diff --git a/packages/pinakes-ui/src/client.rs b/crates/pinakes-ui/src/client.rs similarity index 100% rename from packages/pinakes-ui/src/client.rs rename to crates/pinakes-ui/src/client.rs diff --git a/packages/pinakes-ui/src/components/audit.rs b/crates/pinakes-ui/src/components/audit.rs similarity index 100% rename from packages/pinakes-ui/src/components/audit.rs rename to crates/pinakes-ui/src/components/audit.rs diff --git a/packages/pinakes-ui/src/components/backlinks_panel.rs b/crates/pinakes-ui/src/components/backlinks_panel.rs similarity index 100% rename from packages/pinakes-ui/src/components/backlinks_panel.rs rename to crates/pinakes-ui/src/components/backlinks_panel.rs diff --git a/packages/pinakes-ui/src/components/books.rs b/crates/pinakes-ui/src/components/books.rs similarity index 100% rename from packages/pinakes-ui/src/components/books.rs rename to crates/pinakes-ui/src/components/books.rs diff --git a/packages/pinakes-ui/src/components/breadcrumb.rs b/crates/pinakes-ui/src/components/breadcrumb.rs similarity index 100% rename from packages/pinakes-ui/src/components/breadcrumb.rs rename to crates/pinakes-ui/src/components/breadcrumb.rs diff --git a/packages/pinakes-ui/src/components/collections.rs b/crates/pinakes-ui/src/components/collections.rs similarity index 100% rename from packages/pinakes-ui/src/components/collections.rs rename to crates/pinakes-ui/src/components/collections.rs diff --git a/packages/pinakes-ui/src/components/database.rs b/crates/pinakes-ui/src/components/database.rs similarity index 100% rename from packages/pinakes-ui/src/components/database.rs rename to crates/pinakes-ui/src/components/database.rs diff --git a/packages/pinakes-ui/src/components/detail.rs b/crates/pinakes-ui/src/components/detail.rs similarity index 100% rename from packages/pinakes-ui/src/components/detail.rs rename to crates/pinakes-ui/src/components/detail.rs diff --git a/packages/pinakes-ui/src/components/duplicates.rs b/crates/pinakes-ui/src/components/duplicates.rs similarity index 100% rename from packages/pinakes-ui/src/components/duplicates.rs rename to crates/pinakes-ui/src/components/duplicates.rs diff --git a/packages/pinakes-ui/src/components/graph_view.rs b/crates/pinakes-ui/src/components/graph_view.rs similarity index 100% rename from packages/pinakes-ui/src/components/graph_view.rs rename to crates/pinakes-ui/src/components/graph_view.rs diff --git a/packages/pinakes-ui/src/components/image_viewer.rs b/crates/pinakes-ui/src/components/image_viewer.rs similarity index 100% rename from packages/pinakes-ui/src/components/image_viewer.rs rename to crates/pinakes-ui/src/components/image_viewer.rs diff --git a/packages/pinakes-ui/src/components/import.rs b/crates/pinakes-ui/src/components/import.rs similarity index 100% rename from packages/pinakes-ui/src/components/import.rs rename to crates/pinakes-ui/src/components/import.rs diff --git a/packages/pinakes-ui/src/components/library.rs b/crates/pinakes-ui/src/components/library.rs similarity index 100% rename from packages/pinakes-ui/src/components/library.rs rename to crates/pinakes-ui/src/components/library.rs diff --git a/packages/pinakes-ui/src/components/loading.rs b/crates/pinakes-ui/src/components/loading.rs similarity index 100% rename from packages/pinakes-ui/src/components/loading.rs rename to crates/pinakes-ui/src/components/loading.rs diff --git a/packages/pinakes-ui/src/components/login.rs b/crates/pinakes-ui/src/components/login.rs similarity index 100% rename from packages/pinakes-ui/src/components/login.rs rename to crates/pinakes-ui/src/components/login.rs diff --git a/packages/pinakes-ui/src/components/markdown_viewer.rs b/crates/pinakes-ui/src/components/markdown_viewer.rs similarity index 100% rename from packages/pinakes-ui/src/components/markdown_viewer.rs rename to crates/pinakes-ui/src/components/markdown_viewer.rs diff --git a/packages/pinakes-ui/src/components/media_player.rs b/crates/pinakes-ui/src/components/media_player.rs similarity index 100% rename from packages/pinakes-ui/src/components/media_player.rs rename to crates/pinakes-ui/src/components/media_player.rs diff --git a/packages/pinakes-ui/src/components/mod.rs b/crates/pinakes-ui/src/components/mod.rs similarity index 100% rename from packages/pinakes-ui/src/components/mod.rs rename to crates/pinakes-ui/src/components/mod.rs diff --git a/packages/pinakes-ui/src/components/pagination.rs b/crates/pinakes-ui/src/components/pagination.rs similarity index 100% rename from packages/pinakes-ui/src/components/pagination.rs rename to crates/pinakes-ui/src/components/pagination.rs diff --git a/packages/pinakes-ui/src/components/pdf_viewer.rs b/crates/pinakes-ui/src/components/pdf_viewer.rs similarity index 100% rename from packages/pinakes-ui/src/components/pdf_viewer.rs rename to crates/pinakes-ui/src/components/pdf_viewer.rs diff --git a/packages/pinakes-ui/src/components/playlists.rs b/crates/pinakes-ui/src/components/playlists.rs similarity index 100% rename from packages/pinakes-ui/src/components/playlists.rs rename to crates/pinakes-ui/src/components/playlists.rs diff --git a/packages/pinakes-ui/src/components/search.rs b/crates/pinakes-ui/src/components/search.rs similarity index 100% rename from packages/pinakes-ui/src/components/search.rs rename to crates/pinakes-ui/src/components/search.rs diff --git a/packages/pinakes-ui/src/components/settings.rs b/crates/pinakes-ui/src/components/settings.rs similarity index 100% rename from packages/pinakes-ui/src/components/settings.rs rename to crates/pinakes-ui/src/components/settings.rs diff --git a/packages/pinakes-ui/src/components/statistics.rs b/crates/pinakes-ui/src/components/statistics.rs similarity index 100% rename from packages/pinakes-ui/src/components/statistics.rs rename to crates/pinakes-ui/src/components/statistics.rs diff --git a/packages/pinakes-ui/src/components/tags.rs b/crates/pinakes-ui/src/components/tags.rs similarity index 100% rename from packages/pinakes-ui/src/components/tags.rs rename to crates/pinakes-ui/src/components/tags.rs diff --git a/packages/pinakes-ui/src/components/tasks.rs b/crates/pinakes-ui/src/components/tasks.rs similarity index 100% rename from packages/pinakes-ui/src/components/tasks.rs rename to crates/pinakes-ui/src/components/tasks.rs diff --git a/packages/pinakes-ui/src/components/utils.rs b/crates/pinakes-ui/src/components/utils.rs similarity index 100% rename from packages/pinakes-ui/src/components/utils.rs rename to crates/pinakes-ui/src/components/utils.rs diff --git a/packages/pinakes-ui/src/main.rs b/crates/pinakes-ui/src/main.rs similarity index 100% rename from packages/pinakes-ui/src/main.rs rename to crates/pinakes-ui/src/main.rs diff --git a/packages/pinakes-ui/src/plugin_ui/actions.rs b/crates/pinakes-ui/src/plugin_ui/actions.rs similarity index 100% rename from packages/pinakes-ui/src/plugin_ui/actions.rs rename to crates/pinakes-ui/src/plugin_ui/actions.rs diff --git a/packages/pinakes-ui/src/plugin_ui/data.rs b/crates/pinakes-ui/src/plugin_ui/data.rs similarity index 100% rename from packages/pinakes-ui/src/plugin_ui/data.rs rename to crates/pinakes-ui/src/plugin_ui/data.rs diff --git a/packages/pinakes-ui/src/plugin_ui/expr.rs b/crates/pinakes-ui/src/plugin_ui/expr.rs similarity index 100% rename from packages/pinakes-ui/src/plugin_ui/expr.rs rename to crates/pinakes-ui/src/plugin_ui/expr.rs diff --git a/packages/pinakes-ui/src/plugin_ui/mod.rs b/crates/pinakes-ui/src/plugin_ui/mod.rs similarity index 100% rename from packages/pinakes-ui/src/plugin_ui/mod.rs rename to crates/pinakes-ui/src/plugin_ui/mod.rs diff --git a/packages/pinakes-ui/src/plugin_ui/registry.rs b/crates/pinakes-ui/src/plugin_ui/registry.rs similarity index 100% rename from packages/pinakes-ui/src/plugin_ui/registry.rs rename to crates/pinakes-ui/src/plugin_ui/registry.rs diff --git a/packages/pinakes-ui/src/plugin_ui/renderer.rs b/crates/pinakes-ui/src/plugin_ui/renderer.rs similarity index 100% rename from packages/pinakes-ui/src/plugin_ui/renderer.rs rename to crates/pinakes-ui/src/plugin_ui/renderer.rs diff --git a/packages/pinakes-ui/src/plugin_ui/widget.rs b/crates/pinakes-ui/src/plugin_ui/widget.rs similarity index 100% rename from packages/pinakes-ui/src/plugin_ui/widget.rs rename to crates/pinakes-ui/src/plugin_ui/widget.rs diff --git a/packages/pinakes-ui/src/state.rs b/crates/pinakes-ui/src/state.rs similarity index 100% rename from packages/pinakes-ui/src/state.rs rename to crates/pinakes-ui/src/state.rs diff --git a/packages/pinakes-ui/src/styles.rs b/crates/pinakes-ui/src/styles.rs similarity index 100% rename from packages/pinakes-ui/src/styles.rs rename to crates/pinakes-ui/src/styles.rs diff --git a/docs/HACKING.md b/docs/HACKING.md deleted file mode 100644 index 28eb21c..0000000 --- a/docs/HACKING.md +++ /dev/null @@ -1,276 +0,0 @@ -# Hacking Pinakes - -Pinakes is a lot of things. It also _plans_ to be a lot of things. One of those -things, as you might have noticed, is _complete_. To be complete in features, to -be complete in documentation and to be complete in hackability. This document -covers, very comprehensively, how you may: - -- Develop Pinakes -- Build Pinakes -- Contribute to Pinakes - -for developers as well as: - -- Distribute Pinakes - -for Pinakes maintainers and packagers. - -## Development Environment - -[Nix]: https://nixos.org -[Direnv]: https://direnv.net - -Pinakes uses [Nix] with flakes for pure, reproducible developer environments. -All of the build dependencies that you need, from Rust to GTK and webkit, are -provided in the devshell. In addition to _not_ requiring system libraries, we'd -like to _actively discourage you_ from relying on system libraries. - -[Direnv] may be preferred by some users as an alternative to `nix develop`. - -```bash -# Enter the dev shell -$ nix develop - -# Or with direnv (recommended) -$ direnv allow -``` - -Nix is the only supported way to get all dependencies. Distro package managers -_may_ work for the core crates but are not supported for the UI. - -## Building Pinakes - -Pinakes is a Cargo workspace with multiple crates targeting multiple aspects of -the projects. The user-facing crates are provided in `packages/*`. -`pinakes-server` and `pinakes-tui` can be built normally with `cargo` while the -UI crate, i.e., `pinakes-ui` **must** be built with the Dioxus CLI (`dx`) so -that the SCSS stylesheets are compiled correctly. Everything else should work -perfectly fine with the standard `cargo build`. - -[Just]: https://just.systems - -To make the things a _little_ bit more convenient, this project uses [Just] as -its command runner and provides a few recipes for common tasks. - -```bash -# Build all crates (server + TUI + UI) -$ just build-all - -# Build individual components -$ just build-server # pinakes-server (HTTP API) -$ just build-tui # pinakes-tui (terminal UI) -$ just build-ui # pinakes-ui (Dioxus desktop/web UI, requires dx) -``` - -The dependencies required for the recipes, including Just itself, is provided by -the Nix devshell. You may also run the cargo equivalents of those commands: - -```bash -# Core crates -$ cargo build -p pinakes-server -$ cargo build -p pinakes-tui - -# The UI crate must be built with dx, not cargo. -$ dx build -p pinakes-ui -``` - -Release builds follow the same pattern, but with `--release`. Nothing else -should be required for the most part. - -## Running Pinakes - -For the time being, UI clients require the server to be running before they can -function. Offline sync is planned, but likely come later alongside cross-device -sync. All UIs connect to the server over HTTP. - -```bash -# 1. Copy and edit the example config (first time only) -$ cp pinakes.example.toml pinakes.toml - -# 2. Start the server (defaults to 127.0.0.1:3000) -$ cargo run -p pinakes-server -- pinakes.toml - -# 3a. Run the TUI client -$ cargo run -p pinakes-tui - -# 3b. Run the desktop/web UI -$ cargo run -p pinakes-ui - -# Connect TUI to a non-default server address -$ cargo run -p pinakes-tui -- --server http://localhost:3000 -``` - -The GUI reads `PINAKES_SERVER_URL` if set; otherwise it assumes -`http://127.0.0.1:3000`. You may change the host and port if you require. - -## Testing - -[cargo-nextest]: https://nexte.st - -Pinakes boasts, or well, attempts to boast a very comprehensive testing suite to -ensure no regressions are introduced while in development. For fast parallel -test execution, we use [cargo-nextest] and while you are recommended to use it -over `cargo test` both should be supported. For CI and package tests, only -`cargo-nextest` is supported. - -```bash -# Run all workspace tests (preferred) -$ just test - -# Equivalent without Just -$ cargo nextest run --workspace - -# Tests for a single crate -$ cargo nextest run -p pinakes-core -$ cargo nextest run -p pinakes-server - -# Run a specific test by name and show output -$ cargo test -p pinakes-core -- test_name --nocapture -``` - -## Linting and Formatting - -### Formatting - -There is a treewide formatter provided within the `flake.nix` that invokes the -recommended formatter tooling across the various filetypes, including database -migrations and Markdown sources. While a `just fmt` recipe is provided, it will -not cover most of the files. Still, you may format the Rust sources as such: - -```bash -# Format all code -$ just fmt # or: cargo fmt - -# Check formatting without modifying files -$ cargo fmt --check -``` - -### Linting - -For now lints only cover Rust sources, and are provided by the Clippy tool. You -may, as usual, invoke it with `just lint`. - -```bash -# Run Clippy -$ just lint # or: cargo clippy --workspace - -# Treat Clippy warnings as errors (used in CI) -$ cargo clippy --workspace -- -D warnings # `-D warnings` is recommended -``` - -All Clippy warnings, besides some of the really annoying ones or false -positives, must be resolved at the source. **Do not** use `#[allow(...)]` or -`#[expect(...)]` to silence warnings unless there is a documented reason. - -It may be okay, at times, to suppress lints but you should focus on _resolving_ -them rather than suppressing them. _If_ suppressing, prefer `#[expect]` and -provide the `reason` parameter. - -## Generating API Documentation - -The REST API documentation is generated from OpenAPI annotations in -`pinakes-server` using `cargo xtask`: - -```bash -# Generate the API documentation at docs/api/ -$ just docs # or: cargo xtask docs -``` - -This writes: - -- `docs/api.md` - Index linking all generated files -- `docs/api/.md` - One Markdown file per API tag -- `docs/api/openapi.json` - Full OpenAPI 3.0 specification - -Please do not edit files under `docs/api/` by hand; they are regenerated on each -run. - -## Code Style - -There are many conventions that are in play within the Pinakes codebase. You'll -get used and adjust to most of them naturally, but here are some of the general -conventions written down for convenience. - -### General - -1. **Make illegal states unrepresentable**; ıse ADTs and newtype wrappers to - encode constraints in the type system. Parse and validate at system - boundaries (user input, external APIs); trust internal types everywhere else. -2. This one is kind of obvious, but please use the `tracing` crate with - structured fields. Do not use `println!` or `eprintln!` in library or server - code. -3. All storage operations must be implemented for both the SQLite and PostgreSQL - backends. Neither backend is optional. - -Naming is kind of obvious, but I'd like to remind you that we use `snake_case` -for functions and variables, `PascalCase` for types and traits, -`SCREAMING_SNAKE_CASE` for constants. There is not much else to it. - -### Error Handling - -- **`unwrap()` and `expect()` are banned** in non-test code. Use `?`, - `ok_or_else`, `match`, or `if let` instead. - - The only accepted exception is when a failure is truly impossible (e.g., a - hardcoded regex that is known-valid at compile time). In that case, add - `#[expect(clippy::expect_used, reason = "...")]` with a clear explanation. -- Wrap storage errors with the `db_ctx` helper so error messages include the - operation and relevant ID: - - ```rust - stmt.execute(params).map_err(db_ctx("insert_media", &media_id))?; - ``` - -### Disallowed Types and APIs - -Pinakes uses hashers from `rustc_hash`, i.e., `FxHashMap` and `FxHashSet` over -the `std` equivalents due to their faster nature without random state. This is -enforced by clippy. - -As Pinakes uses Rust 1.90+, the now-deprecated `once_cell` crate is also banned. -Use the stdlib equivalents: - -- `once_cell::unsync::OnceCell` -> `std::cell::OnceCell` -- `once_cell::sync::OnceCell` -> `std::sync::OnceLock` -- `once_cell::unsync::Lazy` -> `std::cell::LazyCell` -- `once_cell::sync::Lazy` -> `std::sync::LazyLock` - -## Contributing - -1. Fork the repository and create a feature branch. -2. Enter the dev shell: `nix develop` (or `direnv allow`). -3. Make your changes; keep commits focused and descriptive. We use **scoped - commits**. -4. Run `just fmt` and `just lint` before opening a pull request. -5. Run `just test` to verify nothing is broken. -6. Open a pull request against `main`. Describe what you changed and why. - -For significant changes, consider opening an issue first to discuss the -approach. - -## Packaging and Distribution - -Pinakes is released under EUPL v1.2. For distributable release builds, please -respect the license. You may create release builds for the user-facing crates -using `cargo build --release` for server and TUI components. Use `dx` instead -for the GUI: - -```bash -# Server -$ cargo build --release -p pinakes-server - -# TUI -$ cargo build --release -p pinakes-tui - -# GUI -$ dx build --release -p pinakes-ui -``` - -The resulting binaries are self-contained except for system libraries required -by the UI (GTK3, libsoup, webkit2gtk). Database migrations run automatically on -first server startup via the `refinery` crate; no manual migration step is -needed. - -SQLite (the default backend) requires no external database server. PostgreSQL -deployments need the `pg_trgm` extension enabled on the target database. It is -generally advisable to document those requirements for the users, even though -they do not require additional packaging steps. diff --git a/docs/README.md b/docs/README.md index d1aa4f2..69145fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,41 +11,19 @@ PostgreSQL (production deployments) as available database backends. ## Building -This project uses [Just](https://just.systems/) as its command runner to help -run cargo with a consistent interface. You are recommended to get it with Nix, -using the default devshell which provides Just. - ```bash -# Build everything (core crates + UI) -$ just build - -# Build only core crates (cargo) -$ just build-core - -# Build only the UI (uses dx - see note below) -$ just build-ui -``` - -> [!IMPORTANT] -> The Dioxus UI (`pinakes-ui`) must be built with `dx` (Dioxus CLI) to compile -> SCSS stylesheets. Using `cargo build -p pinakes-ui` will not work correctly as -> it skips the SCSS compilation step. This was previously "remedied" with an -> intermediate build wrapper, which turned out to be fragile. It is highly -> recommended that you prefer `just` to build. - -Manual build commands if not using Just: - -```bash -# Core crates (standard cargo) +# Build all compilable crates $ cargo build -p pinakes-core -p pinakes-server -p pinakes-tui -# UI (requires Dioxus CLI for SCSS compilation) -$ dx build -p pinakes-ui - -# System dependencies for the UI: +# The Dioxus UI requires GTK3 and libsoup system libraries: # On Debian/Ubuntu: apt install libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev # On Fedora: dnf install gtk3-devel libsoup3-devel webkit2gtk4.1-devel # On Nix: Use the dev shell, everything is provided :) +$ cargo build -p pinakes-ui + +# Alternatively, while app deps are in PATH, you may simply build the entire +# workspace. +$ cargo build --workspace ``` ## Configuration @@ -75,20 +53,17 @@ Key settings: ## Running -All commands are available via Just. Run `just --list` to see all available -recipes. - ### Server To use Pinakes, you will need the server to be running. The GUI on its own will work, but it will not be functional without the server. -```bash -# Using Just -$ just run-server - -# Or manually: +```sh +# Start the server first $ cargo run -p pinakes-server -- pinakes.toml + +# or: +$ cargo run -p pinakes-server -- --config pinakes.toml ``` The server starts on the configured host:port (default `127.0.0.1:3000`). In a @@ -102,11 +77,11 @@ terminal. While the server is running you may connect to it using the `--server` flag. ```bash -# Using Just -$ just run-tui - -# Or manually: +# Using defaults $ cargo run -p pinakes-tui + +# or with a custom server URL: +$ cargo run -p pinakes-tui -- --server http://localhost:3000 ``` #### Keybindings @@ -145,15 +120,9 @@ Pinakes features a fully fledged Desktop and Web UI powered by Dioxus. Those two components are meant as a GUI frontend for the Pinakes server, and are interchangeable in terms of usage. -> [!IMPORTANT] -> The UI must be run with `dx` (Dioxus CLI), not `cargo run`. - ```bash -# Using Just -$ just run-ui - -# Or manually with dx: -$ dx serve -p pinakes-ui +# Build the UI +$ cargo run -p pinakes-ui ``` > [!TIP] @@ -201,11 +170,8 @@ and design. ## Storage Backends Two storage backends are supported. For convenience, SQLite is the default -backend out of the box but for production deployments, with improved search and -scaling capabilities you may choose to prefer PostgreSQL. Both backends are -considered first-class citizens, and will be developed as such going further. If -your needs for Pinakes are modest, or if you are simply testing it out, SQLite -is the recommended database. +backend out of the box but for production deployments you may choose to prefer +PostgreSQL. ### **SQLite** (default) @@ -214,34 +180,6 @@ guarantees FTS5 availability. ### **PostgreSQL** -[pg_trgm]: https://www.postgresql.org/docs/current/pgtrgm.html - Native async with connection pooling (deadpool-postgres). Uses tsvector with -weighted columns for full-text search and [pg_trgm] for fuzzy matching. Requires +weighted columns for full-text search and pg_trgm for fuzzy matching. Requires the `pg_trgm` extension. - -## Contributing - -[HACKING document]: ./HACKING.md - -Pinakes, despite all the work going into it, is still in an early beta. Some of -the features still lack the polish they deserve and there _may_ be breaking -changes to the UI, databases, and the APIs. You may find a comprehensive -introduction to developing Pinakes in the [HACKING document]. - -It is generally advisable that you familiarize yourself with the codebase before -proposing or contributing changes. Pinakes consists of _many_ moving parts, and -it is better for the contributor experience to approach issues slowly and -discuss them beforehand. - -## License - - - -[here]: https://interoperable-europe.ec.europa.eu/sites/default/files/custom-page/attachment/eupl_v1.2_en.pdf - -This project is made available under European Union Public Licence (EUPL) -version 1.2. See [LICENSE](LICENSE) for more details on the exact conditions. An -online copy is provided [here]. - - diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index dbc8c8d..0000000 --- a/docs/api.md +++ /dev/null @@ -1,47 +0,0 @@ -# API Documentation - -This is the index of all generated REST API documentation for Pinakes. - -Documentation is generated from OpenAPI annotations via `cargo xtask docs` (or -`just docs`). Do not edit generated files by hand. - -## Reference - -- [openapi.json](api/openapi.json) - Full OpenAPI 3.0 specification - -## Endpoints by Tag - -- [Analytics](api/analytics.md) - Usage analytics and viewing history -- [Audit](api/audit.md) - Audit log entries -- [Auth](api/auth.md) - Authentication and session management -- [Backup](api/backup.md) - Database backup -- [Books](api/books.md) - Book metadata, series, authors, and reading progress -- [Collections](api/collections.md) - Media collections -- [Config](api/config.md) - Server configuration -- [Database](api/database.md) - Database administration -- [Duplicates](api/duplicates.md) - Duplicate media detection -- [Enrichment](api/enrichment.md) - External metadata enrichment -- [Export](api/export.md) - Media library export -- [Health](api/health.md) - Server health checks -- [Integrity](api/integrity.md) - Library integrity checks and repairs -- [Jobs](api/jobs.md) - Background job management -- [Media](api/media.md) - Media item management -- [Notes](api/notes.md) - Markdown notes link graph -- [Photos](api/photos.md) - Photo timeline and map view -- [Playlists](api/playlists.md) - Media playlists -- [Plugins](api/plugins.md) - Plugin management -- [Saved_searches](api/saved_searches.md) - Saved search queries -- [Scan](api/scan.md) - Directory scanning -- [Scheduled_tasks](api/scheduled_tasks.md) - Scheduled background tasks -- [Search](api/search.md) - Full-text media search -- [Shares](api/shares.md) - Media sharing and notifications -- [Social](api/social.md) - Ratings, comments, favorites, and share links -- [Statistics](api/statistics.md) - Library statistics -- [Streaming](api/streaming.md) - HLS and DASH adaptive streaming -- [Subtitles](api/subtitles.md) - Media subtitle management -- [Sync](api/sync.md) - Multi-device library synchronization -- [Tags](api/tags.md) - Media tag management -- [Transcode](api/transcode.md) - Video transcoding sessions -- [Upload](api/upload.md) - File upload and managed storage -- [Users](api/users.md) - User and library access management -- [Webhooks](api/webhooks.md) - Webhook configuration diff --git a/docs/api/analytics.md b/docs/api/analytics.md index 086e07e..f706d2f 100644 --- a/docs/api/analytics.md +++ b/docs/api/analytics.md @@ -16,11 +16,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Event recorded | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Event recorded | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,18 +30,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ------------------------- | -| `limit` | query | No | Maximum number of results | -| `offset` | query | No | Pagination offset | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `limit` | query | No | Maximum number of results | +| `offset` | query | No | Pagination offset | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Most viewed media | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Most viewed media | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -51,18 +51,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ------------------------- | -| `limit` | query | No | Maximum number of results | -| `offset` | query | No | Pagination offset | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `limit` | query | No | Maximum number of results | +| `offset` | query | No | Pagination offset | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Recently viewed media | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Recently viewed media | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -72,18 +72,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Watch progress | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Watch progress | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -93,9 +93,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -105,12 +105,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Progress updated | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Progress updated | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- + diff --git a/docs/api/audit.md b/docs/api/audit.md index 5385ca8..adcf493 100644 --- a/docs/api/audit.md +++ b/docs/api/audit.md @@ -10,17 +10,18 @@ Audit log entries #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Page size | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Page size | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Audit log entries | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Audit log entries | +| 401 | Unauthorized | +| 500 | Internal server error | --- + diff --git a/docs/api/auth.md b/docs/api/auth.md index edbd3bd..f62c099 100644 --- a/docs/api/auth.md +++ b/docs/api/auth.md @@ -16,12 +16,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Login successful | -| 400 | Bad request | -| 401 | Invalid credentials | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Login successful | +| 400 | Bad request | +| 401 | Invalid credentials | +| 500 | Internal server error | --- @@ -31,11 +31,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Logged out | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Logged out | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -45,27 +45,28 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Current user info | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Current user info | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### POST /api/v1/auth/refresh -Refresh the current session, extending its expiry by the configured duration. +Refresh the current session, extending its expiry by the configured +duration. **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Session refreshed | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Session refreshed | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -77,11 +78,11 @@ Revoke all sessions for the current user #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | All sessions revoked | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | All sessions revoked | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -91,11 +92,12 @@ Revoke all sessions for the current user #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Active sessions | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Active sessions | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- + diff --git a/docs/api/backup.md b/docs/api/backup.md index 86a184f..e7c7884 100644 --- a/docs/api/backup.md +++ b/docs/api/backup.md @@ -6,21 +6,22 @@ Database backup ### POST /api/v1/admin/backup -Create a database backup and return it as a downloadable file. POST -/api/v1/admin/backup +Create a database backup and return it as a downloadable file. +POST /api/v1/admin/backup -For `SQLite`: creates a backup via VACUUM INTO and returns the file. For -`PostgreSQL`: returns unsupported error (use `pg_dump` instead). +For `SQLite`: creates a backup via VACUUM INTO and returns the file. +For `PostgreSQL`: returns unsupported error (use `pg_dump` instead). **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Backup file download | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Backup file download | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- + diff --git a/docs/api/books.md b/docs/api/books.md index 1ab3162..6af55ad 100644 --- a/docs/api/books.md +++ b/docs/api/books.md @@ -12,23 +12,23 @@ List all books with optional search filters #### Parameters -| Name | In | Required | Description | -| ----------- | ----- | -------- | ------------------- | -| `isbn` | query | No | Filter by ISBN | -| `author` | query | No | Filter by author | -| `series` | query | No | Filter by series | -| `publisher` | query | No | Filter by publisher | -| `language` | query | No | Filter by language | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `isbn` | query | No | Filter by ISBN | +| `author` | query | No | Filter by author | +| `series` | query | No | Filter by series | +| `publisher` | query | No | Filter by publisher | +| `language` | query | No | Filter by language | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | List of books | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | List of books | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -40,17 +40,17 @@ List all authors with book counts #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -| ------ | ------------------------ | -| 200 | Authors with book counts | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Authors with book counts | +| 401 | Unauthorized | --- @@ -62,18 +62,18 @@ Get books by a specific author #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `name` | path | Yes | Author name | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `name` | path | Yes | Author name | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | Books by author | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Books by author | +| 401 | Unauthorized | --- @@ -85,16 +85,16 @@ Get user's reading list #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ------------------------------------------------------------------------------ | -| `status` | query | No | Filter by reading status. Valid values: to_read, reading, completed, abandoned | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `status` | query | No | Filter by reading status | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | Reading list | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Reading list | +| 401 | Unauthorized | --- @@ -106,10 +106,10 @@ List all series with book counts #### Responses -| Status | Description | -| ------ | -------------------------- | -| 200 | List of series with counts | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | List of series with counts | +| 401 | Unauthorized | --- @@ -121,16 +121,16 @@ Get books in a specific series #### Parameters -| Name | In | Required | Description | -| ------ | ---- | -------- | ----------- | -| `name` | path | Yes | Series name | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `name` | path | Yes | Series name | #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | Books in series | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Books in series | +| 401 | Unauthorized | --- @@ -142,17 +142,17 @@ Get book metadata by media ID #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ------------- | -| 200 | Book metadata | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Book metadata | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -164,17 +164,17 @@ Get reading progress for a book #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Reading progress | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Reading progress | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -186,9 +186,9 @@ Update reading progress for a book #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -198,10 +198,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ---------------- | -| 204 | Progress updated | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 204 | Progress updated | +| 400 | Bad request | +| 401 | Unauthorized | --- + diff --git a/docs/api/collections.md b/docs/api/collections.md index 9db76ba..f86b5e0 100644 --- a/docs/api/collections.md +++ b/docs/api/collections.md @@ -10,11 +10,11 @@ Media collections #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | List of collections | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | List of collections | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,13 +30,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Collection created | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Collection created | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -46,18 +46,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Collection ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Collection | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Collection | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -67,19 +67,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Collection ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Collection deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Collection deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -89,18 +89,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Collection ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Collection members | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Collection members | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -110,9 +110,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Collection ID | #### Request Body @@ -122,13 +122,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Member added | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Member added | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -138,19 +138,20 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---------- | ---- | -------- | ------------- | -| `id` | path | Yes | Collection ID | -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Collection ID | +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Member removed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Member removed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- + diff --git a/docs/api/config.md b/docs/api/config.md index a05fcd0..f88299f 100644 --- a/docs/api/config.md +++ b/docs/api/config.md @@ -10,12 +10,12 @@ Server configuration #### Responses -| Status | Description | -| ------ | ---------------------------- | -| 200 | Current server configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Current server configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -31,13 +31,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Updated configuration | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Updated configuration | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -53,12 +53,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Updated configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Updated configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -74,12 +74,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Updated configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Updated configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -89,11 +89,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | UI configuration | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | UI configuration | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -109,11 +109,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------------------ | -| 200 | Updated UI configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Updated UI configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- + diff --git a/docs/api/database.md b/docs/api/database.md index bed7ff6..373df31 100644 --- a/docs/api/database.md +++ b/docs/api/database.md @@ -10,12 +10,12 @@ Database administration #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Database cleared | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Database cleared | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -25,12 +25,12 @@ Database administration #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Database statistics | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Database statistics | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -40,11 +40,12 @@ Database administration #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Database vacuumed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Database vacuumed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- + diff --git a/docs/api/duplicates.md b/docs/api/duplicates.md index 63a200d..b1005b7 100644 --- a/docs/api/duplicates.md +++ b/docs/api/duplicates.md @@ -10,10 +10,11 @@ Duplicate media detection #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Duplicate groups | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Duplicate groups | +| 401 | Unauthorized | +| 500 | Internal server error | --- + diff --git a/docs/api/enrichment.md b/docs/api/enrichment.md index f4ec48e..5012641 100644 --- a/docs/api/enrichment.md +++ b/docs/api/enrichment.md @@ -16,13 +16,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------------------ | -| 200 | Enrichment job submitted | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Enrichment job submitted | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -32,19 +32,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ------------------------ | -| 200 | Enrichment job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Enrichment job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -54,17 +54,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | External metadata | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | External metadata | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- + diff --git a/docs/api/export.md b/docs/api/export.md index 593c1aa..3410a9f 100644 --- a/docs/api/export.md +++ b/docs/api/export.md @@ -10,12 +10,12 @@ Media library export #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Export job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Export job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -31,11 +31,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Export job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Export job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- + diff --git a/docs/api/health.md b/docs/api/health.md index 3ed9cf6..44f6d4a 100644 --- a/docs/api/health.md +++ b/docs/api/health.md @@ -12,9 +12,9 @@ Comprehensive health check - includes database, filesystem, and cache status #### Responses -| Status | Description | -| ------ | ------------- | -| 200 | Health status | +| Status | Description | +|--------|-------------| +| 200 | Health status | --- @@ -24,39 +24,40 @@ Comprehensive health check - includes database, filesystem, and cache status #### Responses -| Status | Description | -| ------ | ---------------------- | -| 200 | Detailed health status | +| Status | Description | +|--------|-------------| +| 200 | Detailed health status | --- ### GET /api/v1/health/live -Liveness probe - just checks if the server is running Returns 200 OK if the -server process is alive +Liveness probe - just checks if the server is running +Returns 200 OK if the server process is alive **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | Server is alive | +| Status | Description | +|--------|-------------| +| 200 | Server is alive | --- ### GET /api/v1/health/ready -Readiness probe - checks if the server can serve requests Returns 200 OK if -database is accessible +Readiness probe - checks if the server can serve requests +Returns 200 OK if database is accessible **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Server is ready | -| 503 | Server not ready | +| Status | Description | +|--------|-------------| +| 200 | Server is ready | +| 503 | Server not ready | --- + diff --git a/docs/api/integrity.md b/docs/api/integrity.md index 5c2b4c8..fd22c23 100644 --- a/docs/api/integrity.md +++ b/docs/api/integrity.md @@ -10,12 +10,12 @@ Library integrity checks and repairs #### Responses -| Status | Description | -| ------ | ------------------------------ | -| 200 | Orphan detection job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Orphan detection job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -31,12 +31,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Orphans resolved | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Orphans resolved | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -46,12 +46,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------------------------- | -| 200 | Thumbnail cleanup job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Thumbnail cleanup job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -67,12 +67,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ---------------------------------- | -| 200 | Thumbnail generation job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Thumbnail generation job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -88,11 +88,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------------------------------ | -| 200 | Integrity verification job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Integrity verification job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- + diff --git a/docs/api/jobs.md b/docs/api/jobs.md index 4cf6a2d..a46b7fd 100644 --- a/docs/api/jobs.md +++ b/docs/api/jobs.md @@ -10,11 +10,11 @@ Background job management #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | List of jobs | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | List of jobs | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -24,18 +24,18 @@ Background job management #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Job ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Job ID | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | Job details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Job details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -45,17 +45,18 @@ Background job management #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Job ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Job ID | #### Responses -| Status | Description | -| ------ | ------------- | -| 200 | Job cancelled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Job cancelled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- + diff --git a/docs/api/media.md b/docs/api/media.md index bdd1dd7..b612729 100644 --- a/docs/api/media.md +++ b/docs/api/media.md @@ -10,19 +10,19 @@ Media item management #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Page size | -| `sort` | query | No | Sort field | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Page size | +| `sort` | query | No | Sort field | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | List of media items | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | List of media items | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -32,12 +32,12 @@ Media item management #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | All media deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | All media deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -53,13 +53,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ----------------------- | -| 200 | Batch collection result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Batch collection result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -75,13 +75,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Batch delete result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Batch delete result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -97,13 +97,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Batch move result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Batch move result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -119,13 +119,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Batch tag result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Batch tag result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -141,13 +141,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Batch update result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Batch update result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -157,11 +157,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media count | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media count | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -177,13 +177,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media imported | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media imported | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -199,13 +199,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Batch import results | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Batch import results | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -221,13 +221,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------------------ | -| 200 | Directory import results | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Directory import results | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -243,13 +243,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media imported | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media imported | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -265,13 +265,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Directory preview | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Directory preview | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -281,18 +281,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Page size | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Page size | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Trashed media items | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Trashed media items | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -302,12 +302,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Trash emptied | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Trash emptied | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -317,11 +317,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Trash info | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Trash info | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -331,18 +331,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media item | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media item | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -352,9 +352,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -364,14 +364,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Updated media item | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Updated media item | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -381,19 +381,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -403,9 +403,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -415,14 +415,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Custom field set | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Custom field set | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -432,20 +432,20 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ------ | ---- | -------- | ----------------- | -| `id` | path | Yes | Media item ID | -| `name` | path | Yes | Custom field name | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | +| `name` | path | Yes | Custom field name | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Custom field deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Custom field deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -455,9 +455,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -467,14 +467,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Moved media item | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Moved media item | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -484,18 +484,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media opened | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media opened | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -505,20 +505,20 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ----------- | ----- | -------- | ------------------------------------ | -| `id` | path | Yes | Media item ID | -| `permanent` | query | No | Set to 'true' for permanent deletion | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | +| `permanent` | query | No | Set to 'true' for permanent deletion | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -528,9 +528,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -540,14 +540,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Renamed media item | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Renamed media item | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -557,19 +557,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media restored | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media restored | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -579,19 +579,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media stream | -| 206 | Partial content | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media stream | +| 206 | Partial content | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -601,18 +601,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Thumbnail image | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Thumbnail image | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -622,18 +622,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media moved to trash | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media moved to trash | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- + diff --git a/docs/api/notes.md b/docs/api/notes.md index 0ffc1fe..330c8f3 100644 --- a/docs/api/notes.md +++ b/docs/api/notes.md @@ -14,18 +14,18 @@ GET /api/v1/media/{id}/backlinks #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Backlinks | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Backlinks | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -39,18 +39,18 @@ GET /api/v1/media/{id}/outgoing-links #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Outgoing links | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Outgoing links | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -64,18 +64,18 @@ POST /api/v1/media/{id}/reindex-links #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Links reindexed | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Links reindexed | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -89,18 +89,18 @@ GET /api/v1/notes/graph?center={uuid}&depth={n} #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ---------------------------------- | -| `center` | query | No | Center node ID | -| `depth` | query | No | Traversal depth (max 5, default 2) | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `center` | query | No | Center node ID | +| `depth` | query | No | Traversal depth (max 5, default 2) | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Graph data | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Graph data | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -114,11 +114,11 @@ POST /api/v1/notes/resolve-links #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Links resolved | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Links resolved | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -132,10 +132,11 @@ GET /api/v1/notes/unresolved-count #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Unresolved link count | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Unresolved link count | +| 401 | Unauthorized | +| 500 | Internal server error | --- + diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 7e25e7c..3f86923 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -1275,7 +1275,7 @@ { "name": "status", "in": "query", - "description": "Filter by reading status. Valid values: to_read, reading, completed, abandoned", + "description": "Filter by reading status", "required": false, "schema": { "type": "string" @@ -4704,11 +4704,14 @@ ], "responses": { "200": { - "description": "Subtitles and available embedded tracks", + "description": "Subtitles", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubtitleListResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/SubtitleResponse" + } } } } @@ -8526,7 +8529,6 @@ "integer", "null" ], - "format": "int32", "minimum": 0 } } @@ -11785,28 +11787,6 @@ ], "description": "Response for accessing shared content.\nSingle-media shares return the media object directly (backwards compatible).\nCollection/Tag/SavedSearch shares return a list of items." }, - "SubtitleListResponse": { - "type": "object", - "description": "Response for listing subtitles on a media item.", - "required": [ - "subtitles", - "available_tracks" - ], - "properties": { - "available_tracks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SubtitleTrackInfoResponse" - } - }, - "subtitles": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SubtitleResponse" - } - } - } - }, "SubtitleResponse": { "type": "object", "required": [ @@ -11849,41 +11829,10 @@ "integer", "null" ], - "format": "int32", "minimum": 0 } } }, - "SubtitleTrackInfoResponse": { - "type": "object", - "description": "Information about an embedded subtitle track available for extraction.", - "required": [ - "index", - "format" - ], - "properties": { - "format": { - "type": "string" - }, - "index": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "language": { - "type": [ - "string", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - } - }, "SyncChangeResponse": { "type": "object", "required": [ diff --git a/docs/api/photos.md b/docs/api/photos.md index 3613838..5afdba3 100644 --- a/docs/api/photos.md +++ b/docs/api/photos.md @@ -12,21 +12,21 @@ Get photos in a bounding box for map view #### Parameters -| Name | In | Required | Description | -| ------ | ----- | -------- | ------------------------ | -| `lat1` | query | Yes | Bounding box latitude 1 | -| `lon1` | query | Yes | Bounding box longitude 1 | -| `lat2` | query | Yes | Bounding box latitude 2 | -| `lon2` | query | Yes | Bounding box longitude 2 | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `lat1` | query | Yes | Bounding box latitude 1 | +| `lon1` | query | Yes | Bounding box longitude 1 | +| `lat2` | query | Yes | Bounding box latitude 2 | +| `lon2` | query | Yes | Bounding box longitude 2 | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Map markers | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Map markers | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -38,19 +38,20 @@ Get timeline of photos grouped by date #### Parameters -| Name | In | Required | Description | -| ---------- | ----- | -------- | -------------------------- | -| `group_by` | query | No | Grouping: day, month, year | -| `year` | query | No | Filter by year | -| `month` | query | No | Filter by month | -| `limit` | query | No | Max items (default 10000) | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `group_by` | query | No | Grouping: day, month, year | +| `year` | query | No | Filter by year | +| `month` | query | No | Filter by month | +| `limit` | query | No | Max items (default 10000) | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Photo timeline groups | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Photo timeline groups | +| 401 | Unauthorized | +| 500 | Internal server error | --- + diff --git a/docs/api/playlists.md b/docs/api/playlists.md index fdcbf35..2f97cde 100644 --- a/docs/api/playlists.md +++ b/docs/api/playlists.md @@ -10,11 +10,11 @@ Media playlists #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | List of playlists | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | List of playlists | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,12 +30,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Playlist created | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Playlist created | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -45,18 +45,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Playlist details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Playlist details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -66,9 +66,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | #### Request Body @@ -78,13 +78,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Playlist updated | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Playlist updated | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -94,18 +94,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Playlist deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Playlist deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -115,18 +115,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Playlist items | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Playlist items | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -136,9 +136,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | #### Request Body @@ -148,12 +148,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | Item added | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Item added | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -163,9 +163,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | #### Request Body @@ -175,12 +175,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Item reordered | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Item reordered | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -190,19 +190,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---------- | ---- | -------- | ------------- | -| `id` | path | Yes | Playlist ID | -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | Item removed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Item removed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -212,17 +212,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -| ------ | ----------------------- | -| 200 | Shuffled playlist items | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Shuffled playlist items | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- + diff --git a/docs/api/plugins.md b/docs/api/plugins.md index 683de6c..eaab41e 100644 --- a/docs/api/plugins.md +++ b/docs/api/plugins.md @@ -12,11 +12,11 @@ List all installed plugins #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | List of plugins | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | List of plugins | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -34,12 +34,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Plugin installed | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | Plugin installed | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -58,10 +58,10 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Event received | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Event received | +| 401 | Unauthorized | --- @@ -73,10 +73,10 @@ List all UI pages provided by loaded plugins #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | Plugin UI pages | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Plugin UI pages | +| 401 | Unauthorized | --- @@ -88,10 +88,10 @@ List merged CSS custom property overrides from all enabled plugins #### Responses -| Status | Description | -| ------ | -------------------------- | -| 200 | Plugin UI theme extensions | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Plugin UI theme extensions | +| 401 | Unauthorized | --- @@ -103,10 +103,10 @@ List all UI widgets provided by loaded plugins #### Responses -| Status | Description | -| ------ | ----------------- | -| 200 | Plugin UI widgets | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Plugin UI widgets | +| 401 | Unauthorized | --- @@ -118,17 +118,17 @@ Get a specific plugin by ID #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Plugin ID | #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Plugin details | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Plugin details | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -140,18 +140,18 @@ Uninstall a plugin #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Plugin ID | #### Responses -| Status | Description | -| ------ | ------------------ | -| 200 | Plugin uninstalled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Plugin uninstalled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -163,18 +163,18 @@ Reload a plugin (for development) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Plugin ID | #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | Plugin reloaded | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Plugin reloaded | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -186,9 +186,9 @@ Enable or disable a plugin #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Plugin ID | #### Request Body @@ -198,11 +198,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Plugin toggled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Plugin toggled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- + diff --git a/docs/api/saved_searches.md b/docs/api/saved_searches.md index 3889deb..12e374d 100644 --- a/docs/api/saved_searches.md +++ b/docs/api/saved_searches.md @@ -10,11 +10,11 @@ Saved search queries #### Responses -| Status | Description | -| ------ | ---------------------- | -| 200 | List of saved searches | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | List of saved searches | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,12 +30,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Search saved | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Search saved | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -45,17 +45,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | --------------- | -| `id` | path | Yes | Saved search ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Saved search ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Saved search deleted | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Saved search deleted | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- + diff --git a/docs/api/scan.md b/docs/api/scan.md index 3101bca..9c2af4b 100644 --- a/docs/api/scan.md +++ b/docs/api/scan.md @@ -18,12 +18,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Scan job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Scan job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -33,9 +33,10 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | Scan status | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Scan status | +| 401 | Unauthorized | --- + diff --git a/docs/api/scheduled_tasks.md b/docs/api/scheduled_tasks.md index d357c66..2367493 100644 --- a/docs/api/scheduled_tasks.md +++ b/docs/api/scheduled_tasks.md @@ -10,11 +10,11 @@ Scheduled background tasks #### Responses -| Status | Description | -| ------ | ----------------------- | -| 200 | List of scheduled tasks | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | List of scheduled tasks | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -24,18 +24,18 @@ Scheduled background tasks #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Task ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Task ID | #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Task triggered | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Task triggered | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -45,17 +45,18 @@ Scheduled background tasks #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Task ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Task ID | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | Task toggled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Task toggled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- + diff --git a/docs/api/search.md b/docs/api/search.md index 2ca9429..102d2fb 100644 --- a/docs/api/search.md +++ b/docs/api/search.md @@ -10,21 +10,21 @@ Full-text media search #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `q` | query | Yes | Search query | -| `sort` | query | No | Sort order | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `q` | query | Yes | Search query | +| `sort` | query | No | Sort order | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Search results | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Search results | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -40,11 +40,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Search results | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Search results | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- + diff --git a/docs/api/shares.md b/docs/api/shares.md index cd62b74..9702f41 100644 --- a/docs/api/shares.md +++ b/docs/api/shares.md @@ -6,81 +6,86 @@ Media sharing and notifications ### GET /api/v1/notifications/shares -Get unread share notifications GET /api/notifications/shares +Get unread share notifications +GET /api/notifications/shares **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | -------------------- | -| 200 | Unread notifications | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Unread notifications | +| 401 | Unauthorized | --- ### POST /api/v1/notifications/shares/read-all -Mark all notifications as read POST /api/notifications/shares/read-all +Mark all notifications as read +POST /api/notifications/shares/read-all **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | -------------------------------- | -| 200 | All notifications marked as read | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | All notifications marked as read | +| 401 | Unauthorized | --- ### POST /api/v1/notifications/shares/{id}/read -Mark a notification as read POST /api/notifications/shares/{id}/read +Mark a notification as read +POST /api/notifications/shares/{id}/read **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | --------------- | -| `id` | path | Yes | Notification ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Notification ID | #### Responses -| Status | Description | -| ------ | --------------------------- | -| 200 | Notification marked as read | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Notification marked as read | +| 401 | Unauthorized | --- ### GET /api/v1/shared/{token} -Access a public shared resource GET /api/shared/{token} +Access a public shared resource +GET /api/shared/{token} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---------- | ----- | -------- | -------------------------- | -| `token` | path | Yes | Share token | -| `password` | query | No | Share password if required | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `token` | path | Yes | Share token | +| `password` | query | No | Share password if required | #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Shared content | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Shared content | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/shares -Create a new share POST /api/shares +Create a new share +POST /api/shares **Authentication:** Required (Bearer JWT) @@ -92,18 +97,19 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Share created | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Share created | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### POST /api/v1/shares/batch/delete -Batch delete shares POST /api/shares/batch/delete +Batch delete shares +POST /api/shares/batch/delete **Authentication:** Required (Bearer JWT) @@ -115,93 +121,97 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Shares deleted | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | Shares deleted | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- ### GET /api/v1/shares/incoming -List incoming shares (shares shared with me) GET /api/shares/incoming +List incoming shares (shares shared with me) +GET /api/shares/incoming **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | Incoming shares | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Incoming shares | +| 401 | Unauthorized | --- ### GET /api/v1/shares/outgoing -List outgoing shares (shares I created) GET /api/shares/outgoing +List outgoing shares (shares I created) +GET /api/shares/outgoing **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | Outgoing shares | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Outgoing shares | +| 401 | Unauthorized | --- ### GET /api/v1/shares/{id} -Get share details GET /api/shares/{id} +Get share details +GET /api/shares/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Share ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Share ID | #### Responses -| Status | Description | -| ------ | ------------- | -| 200 | Share details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Share details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### PATCH /api/v1/shares/{id} -Update a share PATCH /api/shares/{id} +Update a share +PATCH /api/shares/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Share ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Share ID | #### Request Body @@ -211,59 +221,62 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------- | -| 200 | Share updated | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Share updated | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### DELETE /api/v1/shares/{id} -Delete (revoke) a share DELETE /api/shares/{id} +Delete (revoke) a share +DELETE /api/shares/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Share ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Share ID | #### Responses -| Status | Description | -| ------ | ------------- | -| 204 | Share deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 204 | Share deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### GET /api/v1/shares/{id}/activity -Get share activity log GET /api/shares/{id}/activity +Get share activity log +GET /api/shares/{id}/activity **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ----------------- | -| `id` | path | Yes | Share ID | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Share ID | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Share activity | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Share activity | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- + diff --git a/docs/api/social.md b/docs/api/social.md index 097a627..e706183 100644 --- a/docs/api/social.md +++ b/docs/api/social.md @@ -10,11 +10,11 @@ Ratings, comments, favorites, and share links #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | User favorites | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | User favorites | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,11 +30,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Added to favorites | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Added to favorites | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -44,17 +44,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---------- | ---- | -------- | ------------- | -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ---------------------- | -| 200 | Removed from favorites | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Removed from favorites | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -70,12 +70,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Share link created | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Share link created | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -85,17 +85,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media comments | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media comments | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -105,9 +105,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -117,12 +117,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Comment added | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Comment added | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -132,9 +132,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -144,12 +144,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Rating saved | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Rating saved | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -159,17 +159,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media ratings | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media ratings | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -179,17 +179,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---------- | ----- | -------- | -------------- | -| `token` | path | Yes | Share token | -| `password` | query | No | Share password | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `token` | path | Yes | Share token | +| `password` | query | No | Share password | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | Shared media | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Shared media | +| 401 | Unauthorized | +| 404 | Not found | --- + diff --git a/docs/api/statistics.md b/docs/api/statistics.md index b74f02f..270ad62 100644 --- a/docs/api/statistics.md +++ b/docs/api/statistics.md @@ -10,10 +10,11 @@ Library statistics #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Library statistics | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Library statistics | +| 401 | Unauthorized | +| 500 | Internal server error | --- + diff --git a/docs/api/streaming.md b/docs/api/streaming.md index 0bd0a35..11a3352 100644 --- a/docs/api/streaming.md +++ b/docs/api/streaming.md @@ -10,18 +10,18 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ------------- | -| 200 | DASH manifest | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | DASH manifest | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -31,20 +31,20 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -| --------- | ---- | -------- | ---------------------- | -| `id` | path | Yes | Media item ID | -| `profile` | path | Yes | Transcode profile name | -| `segment` | path | Yes | Segment filename | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | +| `profile` | path | Yes | Transcode profile name | +| `segment` | path | Yes | Segment filename | #### Responses -| Status | Description | -| ------ | ------------------------- | -| 200 | DASH segment data | -| 202 | Segment not yet available | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | DASH segment data | +| 202 | Segment not yet available | +| 400 | Bad request | +| 401 | Unauthorized | --- @@ -54,17 +54,17 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ------------------- | -| 200 | HLS master playlist | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | HLS master playlist | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -74,19 +74,19 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -| --------- | ---- | -------- | ---------------------- | -| `id` | path | Yes | Media item ID | -| `profile` | path | Yes | Transcode profile name | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | +| `profile` | path | Yes | Transcode profile name | #### Responses -| Status | Description | -| ------ | -------------------- | -| 200 | HLS variant playlist | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | HLS variant playlist | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -96,19 +96,20 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -| --------- | ---- | -------- | ---------------------- | -| `id` | path | Yes | Media item ID | -| `profile` | path | Yes | Transcode profile name | -| `segment` | path | Yes | Segment filename | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | +| `profile` | path | Yes | Transcode profile name | +| `segment` | path | Yes | Segment filename | #### Responses -| Status | Description | -| ------ | ------------------------- | -| 200 | HLS segment data | -| 202 | Segment not yet available | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | HLS segment data | +| 202 | Segment not yet available | +| 400 | Bad request | +| 401 | Unauthorized | --- + diff --git a/docs/api/subtitles.md b/docs/api/subtitles.md index 8387484..ce36e05 100644 --- a/docs/api/subtitles.md +++ b/docs/api/subtitles.md @@ -10,17 +10,17 @@ Media subtitle management #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------------------------- | -| 200 | Subtitles and available embedded tracks | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Subtitles | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -30,9 +30,9 @@ Media subtitle management #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -42,12 +42,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Subtitle added | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Subtitle added | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -57,18 +57,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ------------- | ---- | -------- | ------------- | -| `media_id` | path | Yes | Media item ID | -| `subtitle_id` | path | Yes | Subtitle ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `media_id` | path | Yes | Media item ID | +| `subtitle_id` | path | Yes | Subtitle ID | #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Subtitle content | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Subtitle content | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -78,17 +78,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Subtitle ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Subtitle ID | #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Subtitle deleted | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Subtitle deleted | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -98,9 +98,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Subtitle ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Subtitle ID | #### Request Body @@ -110,10 +110,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Offset updated | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Offset updated | +| 401 | Unauthorized | +| 404 | Not found | --- + diff --git a/docs/api/sync.md b/docs/api/sync.md index fd1aa5b..165d4c0 100644 --- a/docs/api/sync.md +++ b/docs/api/sync.md @@ -6,7 +6,8 @@ Multi-device library synchronization ### POST /api/v1/sync/ack -Acknowledge processed changes POST /api/sync/ack +Acknowledge processed changes +POST /api/sync/ack **Authentication:** Required (Bearer JWT) @@ -18,63 +19,66 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------------- | -| 200 | Changes acknowledged | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Changes acknowledged | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/changes -Get changes since cursor GET /api/sync/changes +Get changes since cursor +GET /api/sync/changes **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| -------- | ----- | -------- | ---------------------- | -| `cursor` | query | No | Sync cursor | -| `limit` | query | No | Max changes (max 1000) | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `cursor` | query | No | Sync cursor | +| `limit` | query | No | Max changes (max 1000) | #### Responses -| Status | Description | -| ------ | -------------------- | -| 200 | Changes since cursor | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Changes since cursor | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/conflicts -List unresolved conflicts GET /api/sync/conflicts +List unresolved conflicts +GET /api/sync/conflicts **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | -------------------- | -| 200 | Unresolved conflicts | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Unresolved conflicts | +| 401 | Unauthorized | --- ### POST /api/v1/sync/conflicts/{id}/resolve -Resolve a sync conflict POST /api/sync/conflicts/{id}/resolve +Resolve a sync conflict +POST /api/sync/conflicts/{id}/resolve **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Conflict ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Conflict ID | #### Request Body @@ -84,32 +88,34 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ----------------- | -| 200 | Conflict resolved | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Conflict resolved | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/devices -List user's sync devices GET /api/sync/devices +List user's sync devices +GET /api/sync/devices **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | List of devices | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | List of devices | +| 401 | Unauthorized | --- ### POST /api/v1/sync/devices -Register a new sync device POST /api/sync/devices +Register a new sync device +POST /api/sync/devices **Authentication:** Required (Bearer JWT) @@ -121,49 +127,51 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Device registered | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Device registered | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### GET /api/v1/sync/devices/{id} -Get device details GET /api/sync/devices/{id} +Get device details +GET /api/sync/devices/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Device ID | #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Device details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Device details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### PUT /api/v1/sync/devices/{id} -Update a device PUT /api/sync/devices/{id} +Update a device +PUT /api/sync/devices/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Device ID | #### Request Body @@ -173,87 +181,91 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Device updated | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Device updated | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### DELETE /api/v1/sync/devices/{id} -Delete a device DELETE /api/sync/devices/{id} +Delete a device +DELETE /api/sync/devices/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Device ID | #### Responses -| Status | Description | -| ------ | -------------- | -| 204 | Device deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 204 | Device deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### POST /api/v1/sync/devices/{id}/token -Regenerate device token POST /api/sync/devices/{id}/token +Regenerate device token +POST /api/sync/devices/{id}/token **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Device ID | #### Responses -| Status | Description | -| ------ | ----------------- | -| 200 | Token regenerated | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Token regenerated | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### GET /api/v1/sync/download/{path} -Download a file for sync (supports Range header) GET /api/sync/download/{*path} +Download a file for sync (supports Range header) +GET /api/sync/download/{*path} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ------ | ---- | -------- | ----------- | -| `path` | path | Yes | File path | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `path` | path | Yes | File path | #### Responses -| Status | Description | -| ------ | --------------- | -| 200 | File content | -| 206 | Partial content | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | File content | +| 206 | Partial content | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/sync/report -Report local changes from client POST /api/sync/report +Report local changes from client +POST /api/sync/report **Authentication:** Required (Bearer JWT) @@ -265,17 +277,18 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ----------------- | -| 200 | Changes processed | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Changes processed | +| 400 | Bad request | +| 401 | Unauthorized | --- ### POST /api/v1/sync/upload -Create an upload session for chunked upload POST /api/sync/upload +Create an upload session for chunked upload +POST /api/sync/upload **Authentication:** Required (Bearer JWT) @@ -287,107 +300,113 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ---------------------- | -| 200 | Upload session created | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | Upload session created | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/upload/{id} -Get upload session status GET /api/sync/upload/{id} +Get upload session status +GET /api/sync/upload/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------------- | -| `id` | path | Yes | Upload session ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Upload session ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Upload session status | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Upload session status | +| 401 | Unauthorized | +| 404 | Not found | --- ### DELETE /api/v1/sync/upload/{id} -Cancel an upload session DELETE /api/sync/upload/{id} +Cancel an upload session +DELETE /api/sync/upload/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------------- | -| `id` | path | Yes | Upload session ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Upload session ID | #### Responses -| Status | Description | -| ------ | ---------------- | -| 204 | Upload cancelled | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 204 | Upload cancelled | +| 401 | Unauthorized | +| 404 | Not found | --- ### PUT /api/v1/sync/upload/{id}/chunks/{index} -Upload a chunk PUT /api/sync/upload/{id}/chunks/{index} +Upload a chunk +PUT /api/sync/upload/{id}/chunks/{index} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ------- | ---- | -------- | ----------------- | -| `id` | path | Yes | Upload session ID | -| `index` | path | Yes | Chunk index | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Upload session ID | +| `index` | path | Yes | Chunk index | #### Request Body -Chunk binary data `Content-Type: application/octet-stream` +Chunk binary data +`Content-Type: application/octet-stream` See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Chunk received | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Chunk received | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/sync/upload/{id}/complete -Complete an upload session POST /api/sync/upload/{id}/complete +Complete an upload session +POST /api/sync/upload/{id}/complete **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------------- | -| `id` | path | Yes | Upload session ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Upload session ID | #### Responses -| Status | Description | -| ------ | ---------------- | -| 200 | Upload completed | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Upload completed | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- + diff --git a/docs/api/tags.md b/docs/api/tags.md index 6bdefc4..a9a71c0 100644 --- a/docs/api/tags.md +++ b/docs/api/tags.md @@ -10,18 +10,18 @@ Media tag management #### Parameters -| Name | In | Required | Description | -| ---------- | ---- | -------- | ------------- | -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Media tags | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Media tags | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -31,9 +31,9 @@ Media tag management #### Parameters -| Name | In | Required | Description | -| ---------- | ---- | -------- | ------------- | -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `media_id` | path | Yes | Media item ID | #### Request Body @@ -43,13 +43,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Tag applied | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Tag applied | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -59,20 +59,20 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---------- | ---- | -------- | ------------- | -| `media_id` | path | Yes | Media item ID | -| `tag_id` | path | Yes | Tag ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `media_id` | path | Yes | Media item ID | +| `tag_id` | path | Yes | Tag ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Tag removed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Tag removed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -82,11 +82,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | List of tags | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | List of tags | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -102,13 +102,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Tag created | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Tag created | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -118,18 +118,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Tag ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Tag ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Tag | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Tag | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -139,18 +139,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | Tag ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Tag ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | Tag deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Tag deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- + diff --git a/docs/api/transcode.md b/docs/api/transcode.md index 3cbb7f9..126135e 100644 --- a/docs/api/transcode.md +++ b/docs/api/transcode.md @@ -10,9 +10,9 @@ Video transcoding sessions #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Request Body @@ -22,12 +22,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ----------------------- | -| 200 | Transcode job submitted | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Transcode job submitted | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -37,10 +37,10 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------------------- | -| 200 | List of transcode sessions | -| 401 | Unauthorized | +| Status | Description | +|--------|-------------| +| 200 | List of transcode sessions | +| 401 | Unauthorized | --- @@ -50,17 +50,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | -------------------- | -| `id` | path | Yes | Transcode session ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Transcode session ID | #### Responses -| Status | Description | -| ------ | ------------------------- | -| 200 | Transcode session details | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Transcode session details | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -70,16 +70,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | -------------------- | -| `id` | path | Yes | Transcode session ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Transcode session ID | #### Responses -| Status | Description | -| ------ | --------------------------- | -| 200 | Transcode session cancelled | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | Transcode session cancelled | +| 401 | Unauthorized | +| 404 | Not found | --- + diff --git a/docs/api/upload.md b/docs/api/upload.md index 698b963..da8a61b 100644 --- a/docs/api/upload.md +++ b/docs/api/upload.md @@ -6,79 +6,84 @@ File upload and managed storage ### GET /api/v1/managed/stats -Get managed storage statistics GET /api/managed/stats +Get managed storage statistics +GET /api/managed/stats **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | -------------------------- | -| 200 | Managed storage statistics | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | Managed storage statistics | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### GET /api/v1/media/{id}/download -Download a managed file GET /api/media/{id}/download +Download a managed file +GET /api/media/{id}/download **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | File content | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | File content | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/media/{id}/move-to-managed -Migrate an external file to managed storage POST /api/media/{id}/move-to-managed +Migrate an external file to managed storage +POST /api/media/{id}/move-to-managed **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ------------- | -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -| ------ | --------------------- | -| 204 | File migrated | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 204 | File migrated | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### POST /api/v1/upload -Upload a file to managed storage POST /api/upload +Upload a file to managed storage +POST /api/upload **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | File uploaded | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | File uploaded | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- + diff --git a/docs/api/users.md b/docs/api/users.md index c76a411..0cd7087 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -12,11 +12,11 @@ List all users (admin only) #### Responses -| Status | Description | -| ------ | ------------- | -| 200 | List of users | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | List of users | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -35,13 +35,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | --------------------- | -| 200 | User created | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +|--------|-------------| +| 200 | User created | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -53,18 +53,18 @@ Get a specific user by ID #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | User ID | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | User details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | User details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -76,9 +76,9 @@ Update a user #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | User ID | #### Request Body @@ -89,13 +89,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | User updated | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | User updated | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -107,18 +107,18 @@ Delete a user (admin only) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | User ID | #### Responses -| Status | Description | -| ------ | ------------ | -| 200 | User deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +|--------|-------------| +| 200 | User deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -130,17 +130,17 @@ Get user's accessible libraries #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | User ID | #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | User libraries | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | User libraries | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -152,9 +152,9 @@ Grant library access to a user (admin only) #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | User ID | #### Request Body @@ -164,12 +164,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Access granted | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | Access granted | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -184,9 +184,9 @@ slashes that conflict with URL routing. #### Parameters -| Name | In | Required | Description | -| ---- | ---- | -------- | ----------- | -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +|------|----|----------|-------------| +| `id` | path | Yes | User ID | #### Request Body @@ -196,11 +196,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -| ------ | -------------- | -| 200 | Access revoked | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | Access revoked | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- + diff --git a/docs/api/webhooks.md b/docs/api/webhooks.md index da0e1f4..9005323 100644 --- a/docs/api/webhooks.md +++ b/docs/api/webhooks.md @@ -10,11 +10,11 @@ Webhook configuration #### Responses -| Status | Description | -| ------ | --------------------------- | -| 200 | List of configured webhooks | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | List of configured webhooks | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -24,10 +24,11 @@ Webhook configuration #### Responses -| Status | Description | -| ------ | ----------------- | -| 200 | Test webhook sent | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +|--------|-------------| +| 200 | Test webhook sent | +| 401 | Unauthorized | +| 403 | Forbidden | --- + diff --git a/examples/plugins/README.md b/examples/plugins/README.md new file mode 100644 index 0000000..12c5217 --- /dev/null +++ b/examples/plugins/README.md @@ -0,0 +1,518 @@ +# Pinakes Plugin Examples + +This directory contains example plugins demonstrating the Pinakes plugin system. + +## Overview + +Pinakes supports extensibility through a WASM-based plugin system. Plugins can +extend Pinakes functionality by: + +- **Media Type Providers**: Add support for new file formats +- **Metadata Extractors**: Extract metadata from files +- **Thumbnail Generators**: Generate thumbnails for media types +- **Search Backends**: Implement custom search algorithms +- **Event Handlers**: React to system events +- **Theme Providers**: Provide custom UI themes + +## Example Plugins + +### 1. Markdown Metadata Extractor + +**Directory**: `markdown-metadata/` + +Enhances markdown file support with advanced frontmatter parsing. + +**Demonstrates**: + +- Metadata extraction from files +- Plugin configuration via `plugin.toml` +- Minimal capability requirements + +**Plugin Kind**: `metadata_extractor` + +### 2. HEIF/HEIC Support + +**Directory**: `heif-support/` + +Adds support for HEIF and HEIC image formats. + +**Demonstrates**: + +- Media type registration +- Metadata extraction from binary formats +- Thumbnail generation +- Resource limits (memory, CPU time) + +**Plugin Kinds**: `media_type`, `metadata_extractor`, `thumbnail_generator` + +## Plugin Architecture + +### Plugin Structure + +``` +my-plugin/ +├── plugin.toml # Plugin manifest +├── Cargo.toml # Rust project configuration +├── src/ +│ └── lib.rs # Plugin implementation +└── README.md # Plugin documentation +``` + +### Plugin Manifest (plugin.toml) + +```toml +[plugin] +name = "my-plugin" +version = "1.0.0" +api_version = "1.0" +author = "Your Name" +description = "Description of your plugin" +kind = ["metadata_extractor"] + +[plugin.binary] +wasm = "my_plugin.wasm" + +[capabilities] +network = false + +[capabilities.filesystem] +read = ["/path/to/read"] +write = ["/path/to/write"] + +[config] +# Plugin-specific configuration +option1 = "value1" +option2 = 42 +``` + +### Manifest Fields + +#### [plugin] Section + +- `name`: Plugin identifier (must be unique) +- `version`: Semantic version (e.g., "1.0.0") +- `api_version`: Pinakes Plugin API version (currently "1.0") +- `author`: Plugin author (optional) +- `description`: Short description (optional) +- `homepage`: Plugin homepage URL (optional) +- `license`: License identifier (optional) +- `kind`: Array of plugin kinds +- `dependencies`: Array of plugin names this plugin depends on (optional) + +#### [plugin.binary] Section + +- `wasm`: Path to WASM binary (relative to manifest) +- `entrypoint`: Custom entrypoint function name (optional, default: "_start") + +#### [capabilities] Section + +Capabilities define what the plugin can access: + +**Filesystem**: + +```toml +[capabilities.filesystem] +read = ["/tmp/cache", "/var/data"] +write = ["/tmp/output"] +``` + +**Network**: + +```toml +[capabilities] +network = true # or false +``` + +**Environment**: + +```toml +[capabilities] +environment = ["PATH", "HOME"] # or omit for no access +``` + +**Resource Limits**: + +```toml +[capabilities] +max_memory_mb = 128 +max_cpu_time_secs = 10 +``` + +### Plugin Kinds + +#### media_type + +Register new media types with file extensions and MIME types. + +**Trait**: `MediaTypeProvider` + +**Methods**: + +- `supported_media_types()`: Returns list of media type definitions +- `can_handle(path, mime_type)`: Check if plugin can handle a file + +#### metadata_extractor + +Extract metadata from files. + +**Trait**: `MetadataExtractor` + +**Methods**: + +- `extract_metadata(path)`: Extract metadata from file +- `supported_types()`: Returns list of supported media type IDs + +#### thumbnail_generator + +Generate thumbnails for media files. + +**Trait**: `ThumbnailGenerator` + +**Methods**: + +- `generate_thumbnail(path, output_path, options)`: Generate thumbnail +- `supported_types()`: Returns list of supported media type IDs + +#### search_backend + +Implement custom search algorithms. + +**Trait**: `SearchBackend` + +**Methods**: + +- `index_item(item)`: Index a media item +- `remove_item(item_id)`: Remove item from index +- `search(query)`: Perform search +- `get_stats()`: Get index statistics + +#### event_handler + +React to system events. + +**Trait**: `EventHandler` + +**Methods**: + +- `handle_event(event)`: Handle an event +- `interested_events()`: Returns list of event types to receive + +#### theme_provider + +Provide UI themes. + +**Trait**: `ThemeProvider` + +**Methods**: + +- `get_themes()`: List available themes +- `load_theme(theme_id)`: Load theme data + +## Creating a Plugin + +### Step 1: Set Up Project + +```bash +# Create new Rust library project +cargo new --lib my-plugin +cd my-plugin + +# Add dependencies +cat >> Cargo.toml <, +} + +#[async_trait] +impl Plugin for MyPlugin { + fn metadata(&self) -> &PluginMetadata { + // Return plugin metadata + } + + async fn initialize(&mut self, context: PluginContext) -> PluginResult<()> { + self.context = Some(context); + Ok(()) + } + + async fn shutdown(&mut self) -> PluginResult<()> { + Ok(()) + } + + async fn health_check(&self) -> PluginResult { + Ok(HealthStatus { + healthy: true, + message: None, + metrics: Default::default(), + }) + } +} + +#[async_trait] +impl MetadataExtractor for MyPlugin { + async fn extract_metadata(&self, path: &PathBuf) -> PluginResult { + // Extract metadata from file + } + + fn supported_types(&self) -> Vec { + vec!["my_type".to_string()] + } +} +``` + +### Step 3: Build to WASM + +```bash +# Install WASM target +rustup target add wasm32-wasi + +# Build +cargo build --target wasm32-wasi --release + +# Optimize (optional, wasm-tools provides wasm-strip functionality) +cargo install wasm-tools +wasm-tools strip target/wasm32-wasi/release/my_plugin.wasm -o target/wasm32-wasi/release/my_plugin.wasm + +# Copy to plugin directory +cp target/wasm32-wasi/release/my_plugin.wasm . +``` + +### Step 4: Create Manifest + +Create `plugin.toml` with appropriate configuration (see examples above). + +### Step 5: Install Plugin + +```bash +# Copy to plugins directory +cp -r my-plugin ~/.config/pinakes/plugins/ + +# Or use API +curl -X POST http://localhost:3000/api/v1/plugins/install \ + -d '{"source": "/path/to/my-plugin"}' +``` + +## Testing Plugins + +### Unit Tests + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_metadata_extraction() { + let mut plugin = MyPlugin::default(); + let context = PluginContext { + data_dir: PathBuf::from("/tmp/data"), + cache_dir: PathBuf::from("/tmp/cache"), + config: Default::default(), + capabilities: Default::default(), + }; + + plugin.initialize(context).await.unwrap(); + + let metadata = plugin + .extract_metadata(&PathBuf::from("test.txt")) + .await + .unwrap(); + + assert!(metadata.title.is_some()); + } +} +``` + +### Integration Tests + +```bash +# Load plugin in test Pinakes instance +pinakes --config test-config.toml plugin load /path/to/plugin + +# Verify plugin is loaded +pinakes plugin list + +# Test plugin functionality +pinakes scan /path/to/test/files +``` + +## Security Considerations + +### Capability-Based Security + +Plugins operate in a sandbox with explicit capabilities. Only request the +minimum capabilities needed: + +**Good**: + +```toml +[capabilities.filesystem] +read = ["/tmp/cache"] +``` + +**Bad**: + +```toml +[capabilities.filesystem] +read = ["/", "/home", "/etc"] # Too broad! +``` + +### Resource Limits + +Always set appropriate resource limits: + +```toml +[capabilities] +max_memory_mb = 128 # Reasonable for image processing +max_cpu_time_secs = 10 # Prevent runaway operations +``` + +### Input Validation + +Always validate input in your plugin: + +```rust +async fn extract_metadata(&self, path: &PathBuf) -> PluginResult { + // Check file exists + if !path.exists() { + return Err(PluginError::InvalidInput("File not found".to_string())); + } + + // Check file size + let metadata = std::fs::metadata(path) + .map_err(|e| PluginError::IoError(e.to_string()))?; + if metadata.len() > 10_000_000 { // 10MB limit + return Err(PluginError::InvalidInput("File too large".to_string())); + } + + // Process file... +} +``` + +## Best Practices + +### Error Handling + +- Use descriptive error messages +- Return appropriate `PluginError` variants +- Don't panic - return errors instead + +### Performance + +- Avoid blocking operations in async functions +- Use streaming for large files +- Implement timeouts for external operations +- Cache results when appropriate + +### Configuration + +- Provide sensible defaults +- Document all configuration options +- Validate configuration during initialization + +### Documentation + +- Write clear README with examples +- Document all configuration options +- Include troubleshooting section +- Provide integration examples + +## API Reference + +See the +[pinakes-plugin-api documentation](../../crates/pinakes-plugin-api/README.md) +for detailed API reference. + +## Plugin Distribution + +### Plugin Registry (Future) + +A centralized plugin registry is planned for future releases: + +```bash +# Install from registry +pinakes plugin install markdown-metadata + +# Search plugins +pinakes plugin search heif + +# Update all plugins +pinakes plugin update --all +``` + +### Manual Distribution + +Currently, plugins are distributed as directories containing: + +- `plugin.toml` manifest +- WASM binary +- README and documentation + +## Troubleshooting + +### Plugin Won't Load + +**Check manifest syntax**: + +```bash +# Validate TOML syntax +taplo check plugin.toml +``` + +**Check API version**: Ensure `api_version = "1.0"` in manifest. + +**Check binary path**: Verify WASM binary exists at path specified in +`plugin.binary.wasm`. + +### Plugin Crashes + +**Check resource limits**: Increase `max_memory_mb` or `max_cpu_time_secs` if +operations are timing out. + +**Check capabilities**: Ensure plugin has necessary filesystem/network +capabilities. + +**Check logs**: + +```bash +# View plugin logs +tail -f ~/.local/share/pinakes/logs/plugin.log +``` + +### Permission Denied Errors + +**Filesystem capabilities**: Add required paths to +`capabilities.filesystem.read` or `.write`. + +**Network capabilities**: Set `capabilities.network = true` if plugin needs +network access. + +## Support + +- **Issues**: https://github.com/notashelf/pinakes/issues +- **Discussions**: https://github.com/notashelf/pinakes/discussions +- **Documentation**: https://pinakes.readthedocs.io + +## License + +All example plugins are licensed under MIT. diff --git a/examples/plugins/heif-support/README.md b/examples/plugins/heif-support/README.md new file mode 100644 index 0000000..b64a002 --- /dev/null +++ b/examples/plugins/heif-support/README.md @@ -0,0 +1,257 @@ +# HEIF/HEIC Support Plugin + +This example plugin adds support for HEIF (High Efficiency Image Format) and HEIC (HEIF Container) to Pinakes. + +## Overview + +HEIF is a modern image format that provides better compression than JPEG while maintaining higher quality. This plugin enables Pinakes to: +- Recognize HEIF/HEIC files as a media type +- Extract metadata from HEIF images +- Generate thumbnails from HEIF images + +## Features + +- **Media Type Registration**: Registers `.heif`, `.heic`, `.hif` extensions as image media types +- **EXIF Extraction**: Extracts EXIF metadata including camera info, GPS coordinates, timestamps +- **Thumbnail Generation**: Generates thumbnails in JPEG, PNG, or WebP format +- **Resource Limits**: Configurable memory and CPU limits for safe processing +- **Large Image Support**: Handles images up to 8192x8192 pixels + +## Supported Formats + +- **HEIF**: High Efficiency Image Format (`.heif`, `.hif`) +- **HEIC**: HEIF Container format used by Apple devices (`.heic`) +- **HEIF Sequences**: Multi-image HEIF files +- **HEIF with Alpha**: HEIF images with transparency + +## Implementation + +The plugin implements three traits: + +### MediaTypeProvider + +```rust +#[async_trait] +impl MediaTypeProvider for HeifPlugin { + fn supported_media_types(&self) -> Vec { + vec![MediaTypeDefinition { + id: "heif".to_string(), + name: "HEIF Image".to_string(), + category: "image".to_string(), + extensions: vec!["heif".to_string(), "heic".to_string(), "hif".to_string()], + mime_types: vec!["image/heif".to_string(), "image/heic".to_string()], + icon: Some("image".to_string()), + }] + } + + async fn can_handle(&self, path: &PathBuf, mime_type: Option<&str>) -> PluginResult { + // Check file extension and/or MIME type + } +} +``` + +### MetadataExtractor + +```rust +#[async_trait] +impl MetadataExtractor for HeifPlugin { + async fn extract_metadata(&self, path: &PathBuf) -> PluginResult { + // 1. Parse HEIF file structure + // 2. Extract EXIF metadata + // 3. Get image dimensions + // 4. Return ExtractedMetadata + } + + fn supported_types(&self) -> Vec { + vec!["heif".to_string()] + } +} +``` + +### ThumbnailGenerator + +```rust +#[async_trait] +impl ThumbnailGenerator for HeifPlugin { + async fn generate_thumbnail( + &self, + path: &PathBuf, + output_path: &PathBuf, + options: ThumbnailOptions, + ) -> PluginResult { + // 1. Decode HEIF image + // 2. Resize to thumbnail dimensions + // 3. Encode to output format + // 4. Save to output_path + // 5. Return ThumbnailInfo + } + + fn supported_types(&self) -> Vec { + vec!["heif".to_string()] + } +} +``` + +## Dependencies + +The plugin uses the following Rust crates (compiled to WASM): +- `libheif-rs`: HEIF decoding and encoding +- `image`: Image processing and thumbnail generation +- `kamadak-exif`: EXIF metadata parsing + +## Building + +### Prerequisites + +```bash +# Install WASM target +rustup target add wasm32-wasi + +# Install wasm-tools for optimization (provides strip functionality) +cargo install wasm-tools +``` + +### Build Process + +```bash +# Build the plugin +cargo build --target wasm32-wasi --release + +# Strip debug symbols to reduce size +wasm-tools strip target/wasm32-wasi/release/heif_support.wasm -o target/wasm32-wasi/release/heif_support.wasm + +# Copy to plugin directory +cp target/wasm32-wasi/release/heif_support.wasm . +``` + +### Size Optimization + +```bash +# Use wasm-opt for further optimization +wasm-opt -Oz heif_support.wasm -o heif_support_optimized.wasm +``` + +## Installation + +### Manual Installation + +```bash +# Copy plugin directory to Pinakes plugins directory +cp -r examples/plugins/heif-support ~/.config/pinakes/plugins/ +``` + +### Via API + +```bash +curl -X POST http://localhost:3000/api/v1/plugins/install \ + -H "Content-Type: application/json" \ + -d '{"source": "/path/to/heif-support"}' +``` + +### Via Plugin Manager + +```bash +pinakes plugin install /path/to/heif-support +``` + +## Configuration + +The plugin can be configured through the `config` section in `plugin.toml`: + +### EXIF Extraction +- `extract_exif`: Enable EXIF metadata extraction (default: true) + +### Thumbnail Generation +- `generate_thumbnails`: Enable thumbnail generation (default: true) +- `thumbnail_quality`: JPEG quality for thumbnails, 1-100 (default: 85) +- `thumbnail_format`: Output format - "jpeg", "png", or "webp" (default: "jpeg") + +### Resource Limits +- `max_memory_mb`: Maximum memory the plugin can use in megabytes (default: 256, set in `[capabilities]`) +- `max_width`: Maximum image width to process (default: 8192) +- `max_height`: Maximum image height to process (default: 8192) + +## Security + +### Capabilities + +- **Filesystem Read**: Only files being processed (via Pinakes) +- **Filesystem Write**: Thumbnail directory only +- **Network**: Disabled +- **Environment**: No access + +### Resource Limits + +- **Memory**: 256 MB maximum +- **CPU Time**: 30 seconds maximum per operation + +### Sandboxing + +The plugin runs in a WASM sandbox with: +- No access to host filesystem beyond granted paths +- No network access +- No arbitrary code execution +- Memory and CPU time limits enforced by runtime + +## Performance + +### Typical Performance + +- **Metadata Extraction**: ~50-100ms for typical HEIF files +- **Thumbnail Generation**: ~200-500ms depending on source image size +- **Memory Usage**: 50-150 MB typical, 256 MB maximum + +### Optimization Tips + +1. Keep source images below 8192x8192 for best performance +2. Use JPEG thumbnail format for smaller file sizes +3. Adjust thumbnail quality vs file size tradeoff with `thumbnail_quality` + +## Error Handling + +The plugin handles: +- **Corrupted Files**: Returns descriptive error +- **Unsupported Variants**: Gracefully skips unsupported HEIF features +- **Memory Limits**: Fails safely if image too large +- **Timeout**: Returns error if processing exceeds CPU time limit + +## Testing + +```bash +# Run unit tests +cargo test + +# Test with sample HEIF files +cargo test --test integration -- --nocapture +``` + +## Troubleshooting + +### Plugin Fails to Load + +- Check that `heif_support.wasm` exists in plugin directory +- Verify `plugin.toml` is valid TOML +- Check Pinakes logs for detailed error messages + +### Thumbnails Not Generated + +- Verify `generate_thumbnails = true` in config +- Check filesystem write permissions for thumbnail directory +- Ensure source image is below size limits + +### Out of Memory Errors + +- Reduce `max_width` and `max_height` in config +- Increase `max_memory_mb` if source images are large +- Check that source files aren't corrupted + +## Future Enhancements + +- Support for HEIF image sequences (burst photos) +- HDR metadata extraction +- Live Photo support +- AVIF format support (similar to HEIF) + +## License + +MIT diff --git a/examples/plugins/heif-support/plugin.toml b/examples/plugins/heif-support/plugin.toml new file mode 100644 index 0000000..6f30eb1 --- /dev/null +++ b/examples/plugins/heif-support/plugin.toml @@ -0,0 +1,29 @@ +[plugin] +name = "heif-support" +version = "1.0.0" +api_version = "1.0" +author = "Pinakes Contributors" +description = "HEIF/HEIC image format support for Pinakes" +homepage = "https://github.com/notashelf/pinakes" +license = "MIT" +kind = ["media_type", "metadata_extractor", "thumbnail_generator"] + +[plugin.binary] +wasm = "heif_support.wasm" + +[capabilities] +network = false +max_memory_mb = 256 +max_cpu_time_secs = 30 + +[capabilities.filesystem] +read = ["/media"] +write = ["/tmp/pinakes"] + +[config] +extract_exif = true +generate_thumbnails = true +thumbnail_quality = 85 +thumbnail_format = "jpeg" +max_width = 8192 +max_height = 8192 diff --git a/examples/plugins/markdown-metadata/README.md b/examples/plugins/markdown-metadata/README.md new file mode 100644 index 0000000..91f2f4c --- /dev/null +++ b/examples/plugins/markdown-metadata/README.md @@ -0,0 +1,103 @@ +# Markdown Metadata Extractor Plugin + +This example plugin demonstrates how to create a metadata extractor plugin for Pinakes. + +## Overview + +The Markdown Metadata Extractor enhances Pinakes' built-in markdown support by: +- Parsing YAML and TOML frontmatter +- Extracting metadata from frontmatter fields +- Converting frontmatter tags to Pinakes media tags +- Extracting custom fields from frontmatter + +## Features + +- **Frontmatter Parsing**: Supports both YAML (`---`) and TOML (`+++`) frontmatter formats +- **Tag Extraction**: Automatically extracts tags from frontmatter and applies them to media items +- **Custom Fields**: Preserves all frontmatter fields as custom metadata +- **Configuration**: Configurable via `plugin.toml` config section + +## Frontmatter Example + +```markdown +--- +title: "My Document" +author: "John Doe" +date: "2024-01-15" +tags: ["documentation", "example", "markdown"] +category: "tutorials" +draft: false +--- + +# My Document + +Content goes here... +``` + +## Implementation + +The plugin implements the `MetadataExtractor` trait from `pinakes-plugin-api`: + +```rust +#[async_trait] +impl MetadataExtractor for MarkdownMetadataPlugin { + async fn extract_metadata(&self, path: &PathBuf) -> PluginResult { + // 1. Read the file + // 2. Parse frontmatter + // 3. Extract metadata fields + // 4. Return ExtractedMetadata + } + + fn supported_types(&self) -> Vec { + vec!["markdown".to_string()] + } +} +``` + +## Building + +The plugin is compiled to WebAssembly: + +```bash +cargo build --target wasm32-wasi --release +wasm-strip target/wasm32-wasi/release/markdown_metadata.wasm +cp target/wasm32-wasi/release/markdown_metadata.wasm . +``` + +## Installation + +```bash +# Copy plugin directory to Pinakes plugins directory +cp -r examples/plugins/markdown-metadata ~/.config/pinakes/plugins/ + +# Or via API +curl -X POST http://localhost:3000/api/v1/plugins/install \ + -H "Content-Type: application/json" \ + -d '{"source": "/path/to/markdown-metadata"}' +``` + +## Configuration + +The plugin can be configured through the `config` section in `plugin.toml`: + +- `extract_tags`: Extract tags from frontmatter (default: true) +- `parse_yaml`: Parse YAML frontmatter (default: true) +- `parse_toml`: Parse TOML frontmatter (default: true) +- `max_file_size`: Maximum file size to process in bytes (default: 10MB) + +## Security + +This plugin has minimal capabilities: +- **Filesystem**: No write access, read access only to files being processed +- **Network**: Disabled +- **Environment**: No access + +## Testing + +```bash +cargo test +``` + +## License + +MIT diff --git a/examples/plugins/markdown-metadata/plugin.toml b/examples/plugins/markdown-metadata/plugin.toml new file mode 100644 index 0000000..a558095 --- /dev/null +++ b/examples/plugins/markdown-metadata/plugin.toml @@ -0,0 +1,25 @@ +[plugin] +name = "markdown-metadata" +version = "1.0.0" +api_version = "1.0" +author = "Pinakes Contributors" +description = "Extract metadata from Markdown files with YAML frontmatter" +homepage = "https://github.com/notashelf/pinakes" +license = "MIT" +kind = ["metadata_extractor"] + +[plugin.binary] +wasm = "markdown_metadata.wasm" + +[capabilities] +network = false + +[capabilities.filesystem] +read = [] +write = [] + +[config] +extract_tags = true +parse_yaml = true +parse_toml = true +max_file_size = 10485760 diff --git a/examples/plugins/media-stats-ui/Cargo.lock b/examples/plugins/media-stats-ui/Cargo.lock new file mode 100644 index 0000000..882e3ef Binary files /dev/null and b/examples/plugins/media-stats-ui/Cargo.lock differ diff --git a/examples/plugins/media-stats-ui/Cargo.toml b/examples/plugins/media-stats-ui/Cargo.toml new file mode 100644 index 0000000..f3004cc --- /dev/null +++ b/examples/plugins/media-stats-ui/Cargo.toml @@ -0,0 +1,20 @@ +[workspace] + +[package] +name = "media-stats-ui" +version = "1.0.0" +edition = "2024" +description = "Library statistics dashboard and tag manager, a UI-only Pinakes plugin" +license = "EUPL-1.2" + +[lib] +name = "media_stats_ui" +crate-type = ["cdylib"] + +[dependencies] +dlmalloc = { version = "0.2.12", features = ["global"] } + +[profile.release] +opt-level = "s" +lto = true +strip = true diff --git a/examples/plugins/media-stats-ui/pages/stats.json b/examples/plugins/media-stats-ui/pages/stats.json new file mode 100644 index 0000000..6f860c5 --- /dev/null +++ b/examples/plugins/media-stats-ui/pages/stats.json @@ -0,0 +1,132 @@ +{ + "id": "stats", + "title": "Library Statistics", + "route": "/plugins/media-stats-ui/stats", + "icon": "chart-bar", + "layout": { + "type": "tabs", + "default_tab": 0, + "tabs": [ + { + "label": "Overview", + "content": { + "type": "container", + "gap": 24, + "children": [ + { + "type": "heading", + "level": 2, + "content": "Library Statistics" + }, + { + "type": "text", + "content": "Live summary of your media library. Refreshes every 30 seconds.", + "variant": "secondary" + }, + { + "type": "card", + "title": "Summary", + "content": [ + { + "type": "description_list", + "data": "stats", + "horizontal": true + } + ] + }, + { + "type": "chart", + "chart_type": "bar", + "data": "type-breakdown", + "title": "Files by Type", + "x_axis_label": "Media Type", + "y_axis_label": "Count", + "height": 280 + } + ] + } + }, + { + "label": "Recent Files", + "content": { + "type": "container", + "gap": 16, + "children": [ + { + "type": "heading", + "level": 2, + "content": "Recently Added" + }, + { + "type": "data_table", + "data": "recent", + "sortable": true, + "filterable": true, + "page_size": 10, + "columns": [ + { + "key": "file_name", + "header": "Filename" + }, + { + "key": "title", + "header": "Title" + }, + { + "key": "media_type", + "header": "Type" + }, + { + "key": "file_size", + "header": "Size", + "data_type": "file_size" + }, + { + "key": "created_at", + "header": "Added", + "data_type": "date_time" + } + ] + } + ] + } + }, + { + "label": "Media Grid", + "content": { + "type": "container", + "gap": 16, + "children": [ + { + "type": "heading", + "level": 2, + "content": "Browse Media" + }, + { + "type": "media_grid", + "data": "recent", + "columns": 4, + "gap": 12 + } + ] + } + } + ] + }, + "data_sources": { + "stats": { + "type": "endpoint", + "path": "/api/v1/statistics", + "poll_interval": 30 + }, + "recent": { + "type": "endpoint", + "path": "/api/v1/media" + }, + "type-breakdown": { + "type": "transform", + "source": "stats", + "expression": "stats.media_by_type" + } + } +} diff --git a/examples/plugins/media-stats-ui/pages/tag-manager.json b/examples/plugins/media-stats-ui/pages/tag-manager.json new file mode 100644 index 0000000..30b3c2f --- /dev/null +++ b/examples/plugins/media-stats-ui/pages/tag-manager.json @@ -0,0 +1,126 @@ +{ + "id": "tag-manager", + "title": "Tag Manager", + "route": "/plugins/media-stats-ui/tag-manager", + "icon": "tag", + "layout": { + "type": "tabs", + "default_tab": 0, + "tabs": [ + { + "label": "All Tags", + "content": { + "type": "container", + "gap": 16, + "children": [ + { + "type": "heading", + "level": 2, + "content": "Manage Tags" + }, + { + "type": "conditional", + "condition": { + "op": "eq", + "left": { "function": "len", "args": ["tags"] }, + "right": 0 + }, + "then": { + "type": "text", + "content": "No tags yet. Use the 'Create Tag' tab to add one.", + "variant": "secondary" + }, + "else": { + "type": "data_table", + "data": "tags", + "sortable": true, + "filterable": true, + "page_size": 20, + "columns": [ + { "key": "name", "header": "Tag Name" }, + { "key": "color", "header": "Color" }, + { "key": "item_count", "header": "Items", "data_type": "number" } + ] + } + } + ] + } + }, + { + "label": "Create Tag", + "content": { + "type": "container", + "gap": 24, + "children": [ + { + "type": "heading", + "level": 2, + "content": "Create New Tag" + }, + { + "type": "text", + "content": "Tags are used to organise media items. Choose a name and an optional colour.", + "variant": "secondary" + }, + { + "type": "form", + "submit_label": "Create Tag", + "submit_action": "create-tag", + "cancel_label": "Reset", + "fields": [ + { + "id": "name", + "label": "Tag Name", + "type": { "type": "text", "max_length": 64 }, + "required": true, + "placeholder": "e.g. favourite, to-watch, archived", + "help_text": "Must be unique. Alphanumeric characters, spaces, and hyphens.", + "validation": [ + { "type": "min_length", "value": 1 }, + { "type": "max_length", "value": 64 }, + { "type": "pattern", "regex": "^[a-zA-Z0-9 \\-]+$" } + ] + }, + { + "id": "color", + "label": "Colour", + "type": { + "type": "select", + "options": [ + { "value": "#ef4444", "label": "Red" }, + { "value": "#f97316", "label": "Orange" }, + { "value": "#eab308", "label": "Yellow" }, + { "value": "#22c55e", "label": "Green" }, + { "value": "#3b82f6", "label": "Blue" }, + { "value": "#8b5cf6", "label": "Purple" }, + { "value": "#ec4899", "label": "Pink" }, + { "value": "#6b7280", "label": "Grey" } + ] + }, + "required": false, + "default_value": "#3b82f6", + "help_text": "Optional accent colour shown beside the tag." + } + ] + } + ] + } + } + ] + }, + "data_sources": { + "tags": { + "type": "endpoint", + "path": "/api/v1/tags", + "poll_interval": 0 + } + }, + "actions": { + "create-tag": { + "method": "POST", + "path": "/api/v1/tags", + "success_message": "Tag created successfully!", + "error_message": "Failed to create tag: the name may already be in use." + } + } +} diff --git a/examples/plugins/media-stats-ui/plugin.toml b/examples/plugins/media-stats-ui/plugin.toml new file mode 100644 index 0000000..0e8116a --- /dev/null +++ b/examples/plugins/media-stats-ui/plugin.toml @@ -0,0 +1,39 @@ +[plugin] +name = "media-stats-ui" +version = "1.0.0" +api_version = "1.0" +author = "Pinakes Contributors" +description = "Library statistics dashboard and tag manager UI plugin" +homepage = "https://github.com/notashelf/pinakes" +license = "EUPL-1.2" +kind = ["ui_page"] + +[plugin.binary] +wasm = "target/wasm32-unknown-unknown/release/media_stats_ui.wasm" + +[capabilities] +network = false + +[capabilities.filesystem] +read = [] +write = [] + +[ui] +required_endpoints = ["/api/v1/statistics", "/api/v1/media", "/api/v1/tags"] + +# UI pages +[[ui.pages]] +file = "pages/stats.json" + +[[ui.pages]] +file = "pages/tag-manager.json" + +# Widgets injected into host views +[[ui.widgets]] +id = "stats-badge" +target = "library_header" + +[ui.widgets.content] +type = "badge" +text = "Stats" +variant = "info" diff --git a/examples/plugins/media-stats-ui/src/lib.rs b/examples/plugins/media-stats-ui/src/lib.rs new file mode 100644 index 0000000..c11a346 --- /dev/null +++ b/examples/plugins/media-stats-ui/src/lib.rs @@ -0,0 +1,101 @@ +//! Media Stats UI - Pinakes plugin +//! +//! A UI-only plugin that adds a library statistics dashboard and a tag manager +//! page. All UI definitions live in `pages/stats.json` and +//! `pages/tag-manager.json`; this WASM binary provides the minimum lifecycle +//! surface the host runtime requires. +//! +//! This plugin is kind = ["ui_page"]: no media-type, metadata, thumbnail, or +//! event-handler extension points are needed. The host will never call them, +//! but exporting them avoids linker warnings if the host performs capability +//! discovery via symbol inspection. + +#![no_std] + +extern crate alloc; + +use core::alloc::Layout; + +#[global_allocator] +static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; + +#[panic_handler] +fn panic(_: &core::panic::PanicInfo) -> ! { + core::arch::wasm32::unreachable() +} + +// Host functions provided by the Pinakes runtime. +unsafe extern "C" { + // Write a result value back to the host (ptr + byte length). + fn host_set_result(ptr: i32, len: i32); + + // Emit a structured log message to the host logger. + // `level` mirrors tracing severity: 0=trace 1=debug 2=info 3=warn 4=error + fn host_log(level: i32, ptr: i32, len: i32); +} + +/// # Safety +/// +/// `json` is a valid slice; the host copies the bytes before +/// returning so there are no lifetime concerns. +fn set_response(json: &[u8]) { + unsafe { host_set_result(json.as_ptr() as i32, json.len() as i32) } +} + +/// # Safety +/// +/// Same as [`set_response`] +fn log_info(msg: &[u8]) { + unsafe { host_log(2, msg.as_ptr() as i32, msg.len() as i32) } +} + +/// Allocate a buffer for the host to write request data into. +/// +/// # Returns +/// +/// The byte offset of the allocation, or -1 on failure. +/// +/// # Safety +/// +/// Size is positive; Layout construction cannot fail for align=1. +#[unsafe(no_mangle)] +pub extern "C" fn alloc(size: i32) -> i32 { + if size <= 0 { + return 0; + } + unsafe { + let layout = Layout::from_size_align_unchecked(size as usize, 1); + let ptr = alloc::alloc::alloc(layout); + if ptr.is_null() { -1 } else { ptr as i32 } + } +} + +/// Called once after the plugin is loaded. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn initialize() -> i32 { + log_info(b"media-stats-ui: initialized"); + 0 +} + +/// Called before the plugin is unloaded. Returns 0 on success. +#[unsafe(no_mangle)] +pub extern "C" fn shutdown() -> i32 { + log_info(b"media-stats-ui: shutdown"); + 0 +} + +/// # Returns +/// +/// an empty JSON array; this plugin adds no custom media types. +#[unsafe(no_mangle)] +pub extern "C" fn supported_media_types(_ptr: i32, _len: i32) { + set_response(b"[]"); +} + +/// # Returns +/// +/// An empty JSON array; this plugin handles no event types. +#[unsafe(no_mangle)] +pub extern "C" fn interested_events(_ptr: i32, _len: i32) { + set_response(b"[]"); +} diff --git a/flake.lock b/flake.lock index c1c549c..52fc03b 100644 Binary files a/flake.lock and b/flake.lock differ diff --git a/flake.nix b/flake.nix index d9ca783..5e4017d 100644 --- a/flake.nix +++ b/flake.nix @@ -21,38 +21,5 @@ in { default = pkgs.callPackage ./nix/shell.nix {}; }); - - formatter = forEachSystem (system: let - pkgs = nixpkgs.legacyPackages.${system}; - in - pkgs.writeShellApplication { - name = "nix3-fmt-wrapper"; - - runtimeInputs = [ - pkgs.alejandra - pkgs.fd - pkgs.prettier - pkgs.deno - pkgs.taplo - pkgs.sql-formatter - ]; - - text = '' - # Format Nix with Alejandra - fd "$@" -t f -e nix -x alejandra -q '{}' - - # Format TOML with Taplo - fd "$@" -t f -e toml -x taplo fmt '{}' - - # Format CSS with Prettier - fd "$@" -t f -e css -x prettier --write '{}' - - # Format SQL with sql-format - fd "$@" -t f -e sql -x sql-formatter --fix '{}' -l postgresql - - # Format Markdown with Deno - fd "$@" -t f -e md -x deno fmt -q '{}' - ''; - }); }; } diff --git a/justfile b/justfile deleted file mode 100644 index 563e545..0000000 --- a/justfile +++ /dev/null @@ -1,40 +0,0 @@ -# Default recipe to show help -@default: - just --list - -# Build all crates -build-all: build-server build-tui build-ui - -# Build the server crate -build-server: - cargo build -p pinakes-server - -# Build the TUI crate -build-tui: - cargo build -p pinakes-tui - -# Build the UI using Dioxus CLI. The UI *has* to be built with `dx`, because CSS -# is not correctly embedded otherwise. -build-ui: - dx build -p pinakes-ui - -# Generate REST API documentation using cargo xtask -@docs: - cargo xtask docs - -# Run all tests -@test: - cargo nextest run --workspace - -# Format code -@fmt: - cargo fmt - -# Run clippy linting -@lint: - cargo clippy --workspace - -# Clean build artifacts -@clean: - cargo clean - rm -rf target/dx/ diff --git a/migrations/postgres/V10__incremental_scan.sql b/migrations/postgres/V10__incremental_scan.sql index 85b196e..8fdc2cb 100644 --- a/migrations/postgres/V10__incremental_scan.sql +++ b/migrations/postgres/V10__incremental_scan.sql @@ -1,19 +1,19 @@ -- Add file_mtime column to media_items table for incremental scanning -- Stores Unix timestamp in seconds of the file's modification time -ALTER TABLE media_items -ADD COLUMN file_mtime BIGINT; + +ALTER TABLE media_items ADD COLUMN file_mtime BIGINT; -- Create index for quick mtime lookups -CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items (file_mtime); +CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items(file_mtime); -- Create a scan_history table to track when each directory was last scanned CREATE TABLE IF NOT EXISTS scan_history ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - directory TEXT NOT NULL UNIQUE, - last_scan_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - files_scanned INTEGER NOT NULL DEFAULT 0, - files_changed INTEGER NOT NULL DEFAULT 0, - scan_duration_ms INTEGER + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directory TEXT NOT NULL UNIQUE, + last_scan_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + files_scanned INTEGER NOT NULL DEFAULT 0, + files_changed INTEGER NOT NULL DEFAULT 0, + scan_duration_ms INTEGER ); -CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history (directory); +CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history(directory); diff --git a/migrations/postgres/V11__session_persistence.sql b/migrations/postgres/V11__session_persistence.sql index d9b5f69..8603d0b 100644 --- a/migrations/postgres/V11__session_persistence.sql +++ b/migrations/postgres/V11__session_persistence.sql @@ -1,17 +1,18 @@ -- Session persistence for database-backed sessions -- Replaces in-memory session storage + CREATE TABLE IF NOT EXISTS sessions ( - session_token TEXT PRIMARY KEY NOT NULL, - user_id TEXT, - username TEXT NOT NULL, - role TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL, - expires_at TIMESTAMPTZ NOT NULL, - last_accessed TIMESTAMPTZ NOT NULL + session_token TEXT PRIMARY KEY NOT NULL, + user_id TEXT, + username TEXT NOT NULL, + role TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + last_accessed TIMESTAMPTZ NOT NULL ); -- Index for efficient cleanup of expired sessions -CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions (expires_at); +CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); -- Index for listing sessions by username -CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions (username); +CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions(username); diff --git a/migrations/postgres/V12__book_management.sql b/migrations/postgres/V12__book_management.sql index 71e29f5..2452032 100644 --- a/migrations/postgres/V12__book_management.sql +++ b/migrations/postgres/V12__book_management.sql @@ -1,61 +1,60 @@ -- V12: Book Management Schema (PostgreSQL) -- Adds comprehensive book metadata tracking, authors, and identifiers + -- Book metadata (supplements media_items for EPUB/PDF/MOBI) CREATE TABLE book_metadata ( - media_id UUID PRIMARY KEY REFERENCES media_items (id) ON DELETE CASCADE, - isbn TEXT, - isbn13 TEXT, -- Normalized ISBN-13 for lookups - publisher TEXT, - language TEXT, -- ISO 639-1 code - page_count INTEGER, - publication_date DATE, - series_name TEXT, - series_index DOUBLE PRECISION, -- Supports 1.5, etc. - format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + media_id UUID PRIMARY KEY REFERENCES media_items(id) ON DELETE CASCADE, + isbn TEXT, + isbn13 TEXT, -- Normalized ISBN-13 for lookups + publisher TEXT, + language TEXT, -- ISO 639-1 code + page_count INTEGER, + publication_date DATE, + series_name TEXT, + series_index DOUBLE PRECISION, -- Supports 1.5, etc. + format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX idx_book_isbn13 ON book_metadata (isbn13); - -CREATE INDEX idx_book_series ON book_metadata (series_name, series_index); - -CREATE INDEX idx_book_publisher ON book_metadata (publisher); - -CREATE INDEX idx_book_language ON book_metadata (language); +CREATE INDEX idx_book_isbn13 ON book_metadata(isbn13); +CREATE INDEX idx_book_series ON book_metadata(series_name, series_index); +CREATE INDEX idx_book_publisher ON book_metadata(publisher); +CREATE INDEX idx_book_language ON book_metadata(language); -- Multiple authors per book (many-to-many) CREATE TABLE book_authors ( - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - author_name TEXT NOT NULL, - author_sort TEXT, -- "Last, First" for sorting - role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator - position INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (media_id, author_name, role) + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + author_name TEXT NOT NULL, + author_sort TEXT, -- "Last, First" for sorting + role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator + position INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (media_id, author_name, role) ); -CREATE INDEX idx_book_authors_name ON book_authors (author_name); - -CREATE INDEX idx_book_authors_sort ON book_authors (author_sort); +CREATE INDEX idx_book_authors_name ON book_authors(author_name); +CREATE INDEX idx_book_authors_sort ON book_authors(author_sort); -- Multiple identifiers (ISBN variants, ASIN, DOI, etc.) CREATE TABLE book_identifiers ( - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc - identifier_value TEXT NOT NULL, - PRIMARY KEY (media_id, identifier_type, identifier_value) + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc + identifier_value TEXT NOT NULL, + PRIMARY KEY (media_id, identifier_type, identifier_value) ); -CREATE INDEX idx_book_identifiers ON book_identifiers (identifier_type, identifier_value); +CREATE INDEX idx_book_identifiers ON book_identifiers(identifier_type, identifier_value); -- Trigger to update updated_at on book_metadata changes -CREATE OR REPLACE FUNCTION update_book_metadata_timestamp () RETURNS TRIGGER AS $$ +CREATE OR REPLACE FUNCTION update_book_metadata_timestamp() +RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -CREATE TRIGGER update_book_metadata_timestamp BEFORE -UPDATE ON book_metadata FOR EACH ROW -EXECUTE FUNCTION update_book_metadata_timestamp (); +CREATE TRIGGER update_book_metadata_timestamp + BEFORE UPDATE ON book_metadata + FOR EACH ROW + EXECUTE FUNCTION update_book_metadata_timestamp(); diff --git a/migrations/postgres/V13__photo_metadata.sql b/migrations/postgres/V13__photo_metadata.sql index 7c66bb8..f1365cd 100644 --- a/migrations/postgres/V13__photo_metadata.sql +++ b/migrations/postgres/V13__photo_metadata.sql @@ -1,40 +1,15 @@ -- V13: Enhanced photo metadata support -- Add photo-specific fields to media_items table -ALTER TABLE media_items -ADD COLUMN date_taken TIMESTAMPTZ; -ALTER TABLE media_items -ADD COLUMN latitude DOUBLE PRECISION; - -ALTER TABLE media_items -ADD COLUMN longitude DOUBLE PRECISION; - -ALTER TABLE media_items -ADD COLUMN camera_make TEXT; - -ALTER TABLE media_items -ADD COLUMN camera_model TEXT; - -ALTER TABLE media_items -ADD COLUMN rating INTEGER CHECK ( - rating >= 0 - AND rating <= 5 -); +ALTER TABLE media_items ADD COLUMN date_taken TIMESTAMPTZ; +ALTER TABLE media_items ADD COLUMN latitude DOUBLE PRECISION; +ALTER TABLE media_items ADD COLUMN longitude DOUBLE PRECISION; +ALTER TABLE media_items ADD COLUMN camera_make TEXT; +ALTER TABLE media_items ADD COLUMN camera_model TEXT; +ALTER TABLE media_items ADD COLUMN rating INTEGER CHECK (rating >= 0 AND rating <= 5); -- Indexes for photo queries -CREATE INDEX idx_media_date_taken ON media_items (date_taken) -WHERE - date_taken IS NOT NULL; - -CREATE INDEX idx_media_location ON media_items (latitude, longitude) -WHERE - latitude IS NOT NULL - AND longitude IS NOT NULL; - -CREATE INDEX idx_media_camera ON media_items (camera_make) -WHERE - camera_make IS NOT NULL; - -CREATE INDEX idx_media_rating ON media_items (rating) -WHERE - rating IS NOT NULL; +CREATE INDEX idx_media_date_taken ON media_items(date_taken) WHERE date_taken IS NOT NULL; +CREATE INDEX idx_media_location ON media_items(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL; +CREATE INDEX idx_media_camera ON media_items(camera_make) WHERE camera_make IS NOT NULL; +CREATE INDEX idx_media_rating ON media_items(rating) WHERE rating IS NOT NULL; diff --git a/migrations/postgres/V14__perceptual_hash.sql b/migrations/postgres/V14__perceptual_hash.sql index 1d3c634..4bdc677 100644 --- a/migrations/postgres/V14__perceptual_hash.sql +++ b/migrations/postgres/V14__perceptual_hash.sql @@ -1,9 +1,7 @@ -- V14: Perceptual hash for duplicate detection -- Add perceptual hash column for image similarity detection -ALTER TABLE media_items -ADD COLUMN perceptual_hash TEXT; + +ALTER TABLE media_items ADD COLUMN perceptual_hash TEXT; -- Index for perceptual hash lookups -CREATE INDEX idx_media_phash ON media_items (perceptual_hash) -WHERE - perceptual_hash IS NOT NULL; +CREATE INDEX idx_media_phash ON media_items(perceptual_hash) WHERE perceptual_hash IS NOT NULL; diff --git a/migrations/postgres/V15__managed_storage.sql b/migrations/postgres/V15__managed_storage.sql index e3fb615..56ef8f4 100644 --- a/migrations/postgres/V15__managed_storage.sql +++ b/migrations/postgres/V15__managed_storage.sql @@ -1,33 +1,30 @@ -- V15: Managed File Storage -- Adds server-side content-addressable storage for uploaded files + -- Add storage mode to media_items (external = file on disk, managed = in content-addressable storage) -ALTER TABLE media_items -ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; +ALTER TABLE media_items ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; -- Original filename for managed uploads (preserved separately from file_name which may be normalized) -ALTER TABLE media_items -ADD COLUMN original_filename TEXT; +ALTER TABLE media_items ADD COLUMN original_filename TEXT; -- When the file was uploaded to managed storage -ALTER TABLE media_items -ADD COLUMN uploaded_at TIMESTAMPTZ; +ALTER TABLE media_items ADD COLUMN uploaded_at TIMESTAMPTZ; -- Storage key for looking up the blob (usually same as content_hash for deduplication) -ALTER TABLE media_items -ADD COLUMN storage_key TEXT; +ALTER TABLE media_items ADD COLUMN storage_key TEXT; -- Managed blobs table - tracks deduplicated file storage CREATE TABLE managed_blobs ( - content_hash TEXT PRIMARY KEY NOT NULL, - file_size BIGINT NOT NULL, - mime_type TEXT NOT NULL, - reference_count INTEGER NOT NULL DEFAULT 1, - stored_at TIMESTAMPTZ NOT NULL, - last_verified TIMESTAMPTZ + content_hash TEXT PRIMARY KEY NOT NULL, + file_size BIGINT NOT NULL, + mime_type TEXT NOT NULL, + reference_count INTEGER NOT NULL DEFAULT 1, + stored_at TIMESTAMPTZ NOT NULL, + last_verified TIMESTAMPTZ ); -- Index for finding managed media items -CREATE INDEX idx_media_storage_mode ON media_items (storage_mode); +CREATE INDEX idx_media_storage_mode ON media_items(storage_mode); -- Index for finding orphaned blobs (reference_count = 0) -CREATE INDEX idx_blobs_reference_count ON managed_blobs (reference_count); +CREATE INDEX idx_blobs_reference_count ON managed_blobs(reference_count); diff --git a/migrations/postgres/V16__sync_system.sql b/migrations/postgres/V16__sync_system.sql index 8647e12..8a87823 100644 --- a/migrations/postgres/V16__sync_system.sql +++ b/migrations/postgres/V16__sync_system.sql @@ -1,100 +1,91 @@ -- V16: Cross-Device Sync System -- Adds device registration, change tracking, and chunked upload support + -- Sync devices table CREATE TABLE sync_devices ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - name TEXT NOT NULL, - device_type TEXT NOT NULL, - client_version TEXT NOT NULL, - os_info TEXT, - device_token_hash TEXT NOT NULL UNIQUE, - last_sync_at TIMESTAMPTZ, - last_seen_at TIMESTAMPTZ NOT NULL, - sync_cursor BIGINT DEFAULT 0, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + device_type TEXT NOT NULL, + client_version TEXT NOT NULL, + os_info TEXT, + device_token_hash TEXT NOT NULL UNIQUE, + last_sync_at TIMESTAMPTZ, + last_seen_at TIMESTAMPTZ NOT NULL, + sync_cursor BIGINT DEFAULT 0, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_sync_devices_user ON sync_devices (user_id); - -CREATE INDEX idx_sync_devices_token ON sync_devices (device_token_hash); +CREATE INDEX idx_sync_devices_user ON sync_devices(user_id); +CREATE INDEX idx_sync_devices_token ON sync_devices(device_token_hash); -- Sync log table - tracks all changes for sync CREATE TABLE sync_log ( - id TEXT PRIMARY KEY NOT NULL, - sequence BIGSERIAL UNIQUE NOT NULL, - change_type TEXT NOT NULL, - media_id TEXT REFERENCES media_items (id) ON DELETE SET NULL, - path TEXT NOT NULL, - content_hash TEXT, - file_size BIGINT, - metadata_json TEXT, - changed_by_device TEXT REFERENCES sync_devices (id) ON DELETE SET NULL, - timestamp TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + sequence BIGSERIAL UNIQUE NOT NULL, + change_type TEXT NOT NULL, + media_id TEXT REFERENCES media_items(id) ON DELETE SET NULL, + path TEXT NOT NULL, + content_hash TEXT, + file_size BIGINT, + metadata_json TEXT, + changed_by_device TEXT REFERENCES sync_devices(id) ON DELETE SET NULL, + timestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_sync_log_sequence ON sync_log (sequence); - -CREATE INDEX idx_sync_log_path ON sync_log (path); - -CREATE INDEX idx_sync_log_timestamp ON sync_log (timestamp); +CREATE INDEX idx_sync_log_sequence ON sync_log(sequence); +CREATE INDEX idx_sync_log_path ON sync_log(path); +CREATE INDEX idx_sync_log_timestamp ON sync_log(timestamp); -- Sequence counter for sync log CREATE TABLE sync_sequence ( - id INTEGER PRIMARY KEY CHECK (id = 1), - current_value BIGINT NOT NULL DEFAULT 0 + id INTEGER PRIMARY KEY CHECK (id = 1), + current_value BIGINT NOT NULL DEFAULT 0 ); - -INSERT INTO - sync_sequence (id, current_value) -VALUES - (1, 0); +INSERT INTO sync_sequence (id, current_value) VALUES (1, 0); -- Device sync state - tracks sync status per device per file CREATE TABLE device_sync_state ( - device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE, - path TEXT NOT NULL, - local_hash TEXT, - server_hash TEXT, - local_mtime BIGINT, - server_mtime BIGINT, - sync_status TEXT NOT NULL, - last_synced_at TIMESTAMPTZ, - conflict_info_json TEXT, - PRIMARY KEY (device_id, path) + device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE, + path TEXT NOT NULL, + local_hash TEXT, + server_hash TEXT, + local_mtime BIGINT, + server_mtime BIGINT, + sync_status TEXT NOT NULL, + last_synced_at TIMESTAMPTZ, + conflict_info_json TEXT, + PRIMARY KEY (device_id, path) ); -CREATE INDEX idx_device_sync_status ON device_sync_state (device_id, sync_status); +CREATE INDEX idx_device_sync_status ON device_sync_state(device_id, sync_status); -- Upload sessions for chunked uploads CREATE TABLE upload_sessions ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE, - target_path TEXT NOT NULL, - expected_hash TEXT NOT NULL, - expected_size BIGINT NOT NULL, - chunk_size BIGINT NOT NULL, - chunk_count BIGINT NOT NULL, - status TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL, - expires_at TIMESTAMPTZ NOT NULL, - last_activity TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE, + target_path TEXT NOT NULL, + expected_hash TEXT NOT NULL, + expected_size BIGINT NOT NULL, + chunk_size BIGINT NOT NULL, + chunk_count BIGINT NOT NULL, + status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + last_activity TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_upload_sessions_device ON upload_sessions (device_id); - -CREATE INDEX idx_upload_sessions_status ON upload_sessions (status); - -CREATE INDEX idx_upload_sessions_expires ON upload_sessions (expires_at); +CREATE INDEX idx_upload_sessions_device ON upload_sessions(device_id); +CREATE INDEX idx_upload_sessions_status ON upload_sessions(status); +CREATE INDEX idx_upload_sessions_expires ON upload_sessions(expires_at); -- Upload chunks - tracks received chunks CREATE TABLE upload_chunks ( - upload_id TEXT NOT NULL REFERENCES upload_sessions (id) ON DELETE CASCADE, - chunk_index BIGINT NOT NULL, - offset - BIGINT NOT NULL, + upload_id TEXT NOT NULL REFERENCES upload_sessions(id) ON DELETE CASCADE, + chunk_index BIGINT NOT NULL, + offset BIGINT NOT NULL, size BIGINT NOT NULL, hash TEXT NOT NULL, received_at TIMESTAMPTZ NOT NULL, @@ -103,20 +94,17 @@ CREATE TABLE upload_chunks ( -- Sync conflicts CREATE TABLE sync_conflicts ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE, - path TEXT NOT NULL, - local_hash TEXT NOT NULL, - local_mtime BIGINT NOT NULL, - server_hash TEXT NOT NULL, - server_mtime BIGINT NOT NULL, - detected_at TIMESTAMPTZ NOT NULL, - resolved_at TIMESTAMPTZ, - resolution TEXT + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE, + path TEXT NOT NULL, + local_hash TEXT NOT NULL, + local_mtime BIGINT NOT NULL, + server_hash TEXT NOT NULL, + server_mtime BIGINT NOT NULL, + detected_at TIMESTAMPTZ NOT NULL, + resolved_at TIMESTAMPTZ, + resolution TEXT ); -CREATE INDEX idx_sync_conflicts_device ON sync_conflicts (device_id); - -CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts (device_id) -WHERE - resolved_at IS NULL; +CREATE INDEX idx_sync_conflicts_device ON sync_conflicts(device_id); +CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts(device_id) WHERE resolved_at IS NULL; diff --git a/migrations/postgres/V17__enhanced_sharing.sql b/migrations/postgres/V17__enhanced_sharing.sql index b068ae7..2107b5c 100644 --- a/migrations/postgres/V17__enhanced_sharing.sql +++ b/migrations/postgres/V17__enhanced_sharing.sql @@ -1,85 +1,68 @@ -- V17: Enhanced Sharing System -- Replaces simple share_links with comprehensive sharing capabilities + -- Enhanced shares table CREATE TABLE shares ( - id TEXT PRIMARY KEY NOT NULL, - target_type TEXT NOT NULL CHECK ( - target_type IN ('media', 'collection', 'tag', 'saved_search') - ), - target_id TEXT NOT NULL, - owner_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - recipient_type TEXT NOT NULL CHECK ( - recipient_type IN ('public_link', 'user', 'group', 'federated') - ), - recipient_user_id TEXT REFERENCES users (id) ON DELETE CASCADE, - recipient_group_id TEXT, - recipient_federated_handle TEXT, - recipient_federated_server TEXT, - public_token TEXT UNIQUE, - public_password_hash TEXT, - perm_view BOOLEAN NOT NULL DEFAULT TRUE, - perm_download BOOLEAN NOT NULL DEFAULT FALSE, - perm_edit BOOLEAN NOT NULL DEFAULT FALSE, - perm_delete BOOLEAN NOT NULL DEFAULT FALSE, - perm_reshare BOOLEAN NOT NULL DEFAULT FALSE, - perm_add BOOLEAN NOT NULL DEFAULT FALSE, - note TEXT, - expires_at TIMESTAMPTZ, - access_count BIGINT NOT NULL DEFAULT 0, - last_accessed TIMESTAMPTZ, - inherit_to_children BOOLEAN NOT NULL DEFAULT TRUE, - parent_share_id TEXT REFERENCES shares (id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - UNIQUE ( - owner_id, - target_type, - target_id, - recipient_type, - recipient_user_id - ) + id TEXT PRIMARY KEY NOT NULL, + target_type TEXT NOT NULL CHECK (target_type IN ('media', 'collection', 'tag', 'saved_search')), + target_id TEXT NOT NULL, + owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + recipient_type TEXT NOT NULL CHECK (recipient_type IN ('public_link', 'user', 'group', 'federated')), + recipient_user_id TEXT REFERENCES users(id) ON DELETE CASCADE, + recipient_group_id TEXT, + recipient_federated_handle TEXT, + recipient_federated_server TEXT, + public_token TEXT UNIQUE, + public_password_hash TEXT, + perm_view BOOLEAN NOT NULL DEFAULT TRUE, + perm_download BOOLEAN NOT NULL DEFAULT FALSE, + perm_edit BOOLEAN NOT NULL DEFAULT FALSE, + perm_delete BOOLEAN NOT NULL DEFAULT FALSE, + perm_reshare BOOLEAN NOT NULL DEFAULT FALSE, + perm_add BOOLEAN NOT NULL DEFAULT FALSE, + note TEXT, + expires_at TIMESTAMPTZ, + access_count BIGINT NOT NULL DEFAULT 0, + last_accessed TIMESTAMPTZ, + inherit_to_children BOOLEAN NOT NULL DEFAULT TRUE, + parent_share_id TEXT REFERENCES shares(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE(owner_id, target_type, target_id, recipient_type, recipient_user_id) ); -CREATE INDEX idx_shares_owner ON shares (owner_id); - -CREATE INDEX idx_shares_recipient_user ON shares (recipient_user_id); - -CREATE INDEX idx_shares_target ON shares (target_type, target_id); - -CREATE INDEX idx_shares_token ON shares (public_token); - -CREATE INDEX idx_shares_expires ON shares (expires_at); +CREATE INDEX idx_shares_owner ON shares(owner_id); +CREATE INDEX idx_shares_recipient_user ON shares(recipient_user_id); +CREATE INDEX idx_shares_target ON shares(target_type, target_id); +CREATE INDEX idx_shares_token ON shares(public_token); +CREATE INDEX idx_shares_expires ON shares(expires_at); -- Share activity log CREATE TABLE share_activity ( - id TEXT PRIMARY KEY NOT NULL, - share_id TEXT NOT NULL REFERENCES shares (id) ON DELETE CASCADE, - actor_id TEXT REFERENCES users (id) ON DELETE SET NULL, - actor_ip TEXT, - action TEXT NOT NULL, - details TEXT, - timestamp TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + share_id TEXT NOT NULL REFERENCES shares(id) ON DELETE CASCADE, + actor_id TEXT REFERENCES users(id) ON DELETE SET NULL, + actor_ip TEXT, + action TEXT NOT NULL, + details TEXT, + timestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_share_activity_share ON share_activity (share_id); - -CREATE INDEX idx_share_activity_timestamp ON share_activity (timestamp); +CREATE INDEX idx_share_activity_share ON share_activity(share_id); +CREATE INDEX idx_share_activity_timestamp ON share_activity(timestamp); -- Share notifications CREATE TABLE share_notifications ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - share_id TEXT NOT NULL REFERENCES shares (id) ON DELETE CASCADE, - notification_type TEXT NOT NULL, - is_read BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + share_id TEXT NOT NULL REFERENCES shares(id) ON DELETE CASCADE, + notification_type TEXT NOT NULL, + is_read BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_share_notifications_user ON share_notifications (user_id); - -CREATE INDEX idx_share_notifications_unread ON share_notifications (user_id) -WHERE - is_read = FALSE; +CREATE INDEX idx_share_notifications_user ON share_notifications(user_id); +CREATE INDEX idx_share_notifications_unread ON share_notifications(user_id) WHERE is_read = FALSE; -- Migrate existing share_links to new shares table DO $$ diff --git a/migrations/postgres/V18__file_management.sql b/migrations/postgres/V18__file_management.sql index 6059e65..30403c3 100644 --- a/migrations/postgres/V18__file_management.sql +++ b/migrations/postgres/V18__file_management.sql @@ -1,13 +1,11 @@ -- V18: File Management (Rename, Move, Trash) -- Adds soft delete support for trash/recycle bin functionality + -- Add deleted_at column for soft delete (trash) -ALTER TABLE media_items -ADD COLUMN deleted_at TIMESTAMPTZ; +ALTER TABLE media_items ADD COLUMN deleted_at TIMESTAMPTZ; -- Index for efficient trash queries -CREATE INDEX idx_media_deleted_at ON media_items (deleted_at); +CREATE INDEX idx_media_deleted_at ON media_items(deleted_at); -- Partial index for listing non-deleted items (most common query pattern) -CREATE INDEX idx_media_not_deleted ON media_items (id) -WHERE - deleted_at IS NULL; +CREATE INDEX idx_media_not_deleted ON media_items(id) WHERE deleted_at IS NULL; diff --git a/migrations/postgres/V19__markdown_links.sql b/migrations/postgres/V19__markdown_links.sql index 084726e..b0d5475 100644 --- a/migrations/postgres/V19__markdown_links.sql +++ b/migrations/postgres/V19__markdown_links.sql @@ -1,35 +1,35 @@ -- V19: Markdown Links (Obsidian-style bidirectional links) -- Adds support for wikilinks, markdown links, embeds, and backlink tracking + -- Table for storing extracted markdown links CREATE TABLE IF NOT EXISTS markdown_links ( - id TEXT PRIMARY KEY NOT NULL, - source_media_id TEXT NOT NULL, - target_path TEXT NOT NULL, -- raw link target (wikilink or path) - target_media_id TEXT, -- resolved media_id (nullable if unresolved) - link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' - link_text TEXT, -- display text for the link - line_number INTEGER, -- line number in source file - context TEXT, -- surrounding text for preview - created_at TIMESTAMPTZ NOT NULL, - FOREIGN KEY (source_media_id) REFERENCES media_items (id) ON DELETE CASCADE, - FOREIGN KEY (target_media_id) REFERENCES media_items (id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + source_media_id TEXT NOT NULL, + target_path TEXT NOT NULL, -- raw link target (wikilink or path) + target_media_id TEXT, -- resolved media_id (nullable if unresolved) + link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' + link_text TEXT, -- display text for the link + line_number INTEGER, -- line number in source file + context TEXT, -- surrounding text for preview + created_at TIMESTAMPTZ NOT NULL, + FOREIGN KEY (source_media_id) REFERENCES media_items(id) ON DELETE CASCADE, + FOREIGN KEY (target_media_id) REFERENCES media_items(id) ON DELETE SET NULL ); -- Index for efficient outgoing link queries (what does this note link to?) -CREATE INDEX idx_links_source ON markdown_links (source_media_id); +CREATE INDEX idx_links_source ON markdown_links(source_media_id); -- Index for efficient backlink queries (what links to this note?) -CREATE INDEX idx_links_target ON markdown_links (target_media_id); +CREATE INDEX idx_links_target ON markdown_links(target_media_id); -- Index for path-based resolution (finding unresolved links) -CREATE INDEX idx_links_target_path ON markdown_links (target_path); +CREATE INDEX idx_links_target_path ON markdown_links(target_path); -- Index for link type filtering -CREATE INDEX idx_links_type ON markdown_links (link_type); +CREATE INDEX idx_links_type ON markdown_links(link_type); -- Track when links were last extracted from a media item -ALTER TABLE media_items -ADD COLUMN links_extracted_at TIMESTAMPTZ; +ALTER TABLE media_items ADD COLUMN links_extracted_at TIMESTAMPTZ; -- Index for finding media items that need link extraction -CREATE INDEX idx_media_links_extracted ON media_items (links_extracted_at); +CREATE INDEX idx_media_links_extracted ON media_items(links_extracted_at); diff --git a/migrations/postgres/V1__initial_schema.sql b/migrations/postgres/V1__initial_schema.sql index c1c49af..cd6a0c8 100644 --- a/migrations/postgres/V1__initial_schema.sql +++ b/migrations/postgres/V1__initial_schema.sql @@ -1,75 +1,73 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE TABLE IF NOT EXISTS root_dirs (path TEXT PRIMARY KEY NOT NULL); +CREATE TABLE IF NOT EXISTS root_dirs ( + path TEXT PRIMARY KEY NOT NULL +); CREATE TABLE IF NOT EXISTS media_items ( - id UUID PRIMARY KEY NOT NULL, - path TEXT NOT NULL UNIQUE, - file_name TEXT NOT NULL, - media_type TEXT NOT NULL, - content_hash TEXT NOT NULL UNIQUE, - file_size BIGINT NOT NULL, - title TEXT, - artist TEXT, - album TEXT, - genre TEXT, - year INTEGER, - duration_secs DOUBLE PRECISION, - description TEXT, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + path TEXT NOT NULL UNIQUE, + file_name TEXT NOT NULL, + media_type TEXT NOT NULL, + content_hash TEXT NOT NULL UNIQUE, + file_size BIGINT NOT NULL, + title TEXT, + artist TEXT, + album TEXT, + genre TEXT, + year INTEGER, + duration_secs DOUBLE PRECISION, + description TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS tags ( - id UUID PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - parent_id UUID REFERENCES tags (id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + parent_id UUID REFERENCES tags(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags ( - name, - COALESCE(parent_id, '00000000-0000-0000-0000-000000000000') -); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags(name, COALESCE(parent_id, '00000000-0000-0000-0000-000000000000')); CREATE TABLE IF NOT EXISTS media_tags ( - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - tag_id UUID NOT NULL REFERENCES tags (id) ON DELETE CASCADE, - PRIMARY KEY (media_id, tag_id) + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (media_id, tag_id) ); CREATE TABLE IF NOT EXISTS collections ( - id UUID PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - description TEXT, - kind TEXT NOT NULL, - filter_query TEXT, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + kind TEXT NOT NULL, + filter_query TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS collection_members ( - collection_id UUID NOT NULL REFERENCES collections (id) ON DELETE CASCADE, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - position INTEGER NOT NULL DEFAULT 0, - added_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (collection_id, media_id) + collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + position INTEGER NOT NULL DEFAULT 0, + added_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (collection_id, media_id) ); CREATE TABLE IF NOT EXISTS audit_log ( - id UUID PRIMARY KEY NOT NULL, - media_id UUID REFERENCES media_items (id) ON DELETE SET NULL, - action TEXT NOT NULL, - details TEXT, - timestamp TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + media_id UUID REFERENCES media_items(id) ON DELETE SET NULL, + action TEXT NOT NULL, + details TEXT, + timestamp TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS custom_fields ( - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - field_name TEXT NOT NULL, - field_type TEXT NOT NULL, - field_value TEXT NOT NULL, - PRIMARY KEY (media_id, field_name) + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + field_name TEXT NOT NULL, + field_type TEXT NOT NULL, + field_value TEXT NOT NULL, + PRIMARY KEY (media_id, field_name) ); diff --git a/migrations/postgres/V2__fts_indexes.sql b/migrations/postgres/V2__fts_indexes.sql index 543fdce..510fce6 100644 --- a/migrations/postgres/V2__fts_indexes.sql +++ b/migrations/postgres/V2__fts_indexes.sql @@ -1,12 +1,11 @@ -ALTER TABLE media_items -ADD COLUMN IF NOT EXISTS search_vector tsvector GENERATED ALWAYS AS ( - setweight(to_tsvector('english', COALESCE(title, '')), 'A') || setweight(to_tsvector('english', COALESCE(artist, '')), 'B') || setweight(to_tsvector('english', COALESCE(album, '')), 'B') || setweight(to_tsvector('english', COALESCE(genre, '')), 'C') || setweight( - to_tsvector('english', COALESCE(description, '')), - 'C' - ) || setweight( - to_tsvector('english', COALESCE(file_name, '')), - 'D' - ) -) STORED; +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS search_vector tsvector + GENERATED ALWAYS AS ( + setweight(to_tsvector('english', COALESCE(title, '')), 'A') || + setweight(to_tsvector('english', COALESCE(artist, '')), 'B') || + setweight(to_tsvector('english', COALESCE(album, '')), 'B') || + setweight(to_tsvector('english', COALESCE(genre, '')), 'C') || + setweight(to_tsvector('english', COALESCE(description, '')), 'C') || + setweight(to_tsvector('english', COALESCE(file_name, '')), 'D') + ) STORED; -CREATE INDEX IF NOT EXISTS idx_media_search ON media_items USING GIN (search_vector); +CREATE INDEX IF NOT EXISTS idx_media_search ON media_items USING GIN(search_vector); diff --git a/migrations/postgres/V3__audit_indexes.sql b/migrations/postgres/V3__audit_indexes.sql index 85ffa06..d8c423a 100644 --- a/migrations/postgres/V3__audit_indexes.sql +++ b/migrations/postgres/V3__audit_indexes.sql @@ -1,15 +1,8 @@ -CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log (media_id); - -CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log (timestamp); - -CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action); - -CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items (content_hash); - -CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items (media_type); - -CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items (created_at); - -CREATE INDEX IF NOT EXISTS idx_media_title_trgm ON media_items USING GIN (title gin_trgm_ops); - -CREATE INDEX IF NOT EXISTS idx_media_artist_trgm ON media_items USING GIN (artist gin_trgm_ops); +CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log(media_id); +CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp); +CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action); +CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items(content_hash); +CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items(media_type); +CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items(created_at); +CREATE INDEX IF NOT EXISTS idx_media_title_trgm ON media_items USING GIN(title gin_trgm_ops); +CREATE INDEX IF NOT EXISTS idx_media_artist_trgm ON media_items USING GIN(artist gin_trgm_ops); diff --git a/migrations/postgres/V4__thumbnail_path.sql b/migrations/postgres/V4__thumbnail_path.sql index 4c23b5b..9021884 100644 --- a/migrations/postgres/V4__thumbnail_path.sql +++ b/migrations/postgres/V4__thumbnail_path.sql @@ -1,2 +1 @@ -ALTER TABLE media_items -ADD COLUMN thumbnail_path TEXT; +ALTER TABLE media_items ADD COLUMN thumbnail_path TEXT; diff --git a/migrations/postgres/V5__integrity_and_saved_searches.sql b/migrations/postgres/V5__integrity_and_saved_searches.sql index fd62baf..f2807f4 100644 --- a/migrations/postgres/V5__integrity_and_saved_searches.sql +++ b/migrations/postgres/V5__integrity_and_saved_searches.sql @@ -1,15 +1,12 @@ -- Integrity tracking columns -ALTER TABLE media_items -ADD COLUMN last_verified_at TIMESTAMPTZ; - -ALTER TABLE media_items -ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; +ALTER TABLE media_items ADD COLUMN last_verified_at TIMESTAMPTZ; +ALTER TABLE media_items ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; -- Saved searches CREATE TABLE IF NOT EXISTS saved_searches ( - id UUID PRIMARY KEY, - name TEXT NOT NULL, - query TEXT NOT NULL, - sort_order TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + name TEXT NOT NULL, + query TEXT NOT NULL, + sort_order TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/migrations/postgres/V6__plugin_system.sql b/migrations/postgres/V6__plugin_system.sql index b40cf41..bd52a1a 100644 --- a/migrations/postgres/V6__plugin_system.sql +++ b/migrations/postgres/V6__plugin_system.sql @@ -1,16 +1,15 @@ -- Plugin registry table CREATE TABLE plugin_registry ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - version TEXT NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - config_json TEXT, - manifest_json TEXT, - installed_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + version TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + config_json TEXT, + manifest_json TEXT, + installed_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL ); -- Index for quick lookups -CREATE INDEX idx_plugin_registry_enabled ON plugin_registry (enabled); - -CREATE INDEX idx_plugin_registry_name ON plugin_registry (name); +CREATE INDEX idx_plugin_registry_enabled ON plugin_registry(enabled); +CREATE INDEX idx_plugin_registry_name ON plugin_registry(name); diff --git a/migrations/postgres/V7__user_management.sql b/migrations/postgres/V7__user_management.sql index c405c80..a9eb12f 100644 --- a/migrations/postgres/V7__user_management.sql +++ b/migrations/postgres/V7__user_management.sql @@ -1,37 +1,35 @@ -- Users table CREATE TABLE users ( - id UUID PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - role JSONB NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL + id UUID PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL ); -- User profiles table CREATE TABLE user_profiles ( - user_id UUID PRIMARY KEY, - avatar_path TEXT, - bio TEXT, - preferences_json JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) + user_id UUID PRIMARY KEY, + avatar_path TEXT, + bio TEXT, + preferences_json JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ); -- User library access table CREATE TABLE user_libraries ( - user_id UUID NOT NULL, - root_path TEXT NOT NULL, - permission JSONB NOT NULL, - granted_at TIMESTAMP WITH TIME ZONE NOT NULL, - PRIMARY KEY (user_id, root_path), - FOREIGN KEY (user_id) REFERENCES users (id) + user_id UUID NOT NULL, + root_path TEXT NOT NULL, + permission JSONB NOT NULL, + granted_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (user_id, root_path), + FOREIGN KEY (user_id) REFERENCES users(id) ); -- Indexes for efficient lookups -CREATE INDEX idx_users_username ON users (username); - -CREATE INDEX idx_user_libraries_user_id ON user_libraries (user_id); - -CREATE INDEX idx_user_libraries_root_path ON user_libraries (root_path); +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_user_libraries_user_id ON user_libraries(user_id); +CREATE INDEX idx_user_libraries_root_path ON user_libraries(root_path); diff --git a/migrations/postgres/V8__media_server_features.sql b/migrations/postgres/V8__media_server_features.sql index 2594bd4..7d22838 100644 --- a/migrations/postgres/V8__media_server_features.sql +++ b/migrations/postgres/V8__media_server_features.sql @@ -1,136 +1,131 @@ -- Ratings CREATE TABLE IF NOT EXISTS ratings ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - stars INTEGER NOT NULL CHECK ( - stars >= 1 - AND stars <= 5 - ), - review_text TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE (user_id, media_id) + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + stars INTEGER NOT NULL CHECK (stars >= 1 AND stars <= 5), + review_text TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(user_id, media_id) ); -- Comments CREATE TABLE IF NOT EXISTS comments ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - parent_comment_id UUID REFERENCES comments (id) ON DELETE CASCADE, - text TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + parent_comment_id UUID REFERENCES comments(id) ON DELETE CASCADE, + text TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Favorites CREATE TABLE IF NOT EXISTS favorites ( - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (user_id, media_id) + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, media_id) ); -- Share links CREATE TABLE IF NOT EXISTS share_links ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - created_by UUID NOT NULL, - token TEXT NOT NULL UNIQUE, - password_hash TEXT, - expires_at TIMESTAMPTZ, - view_count INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + created_by UUID NOT NULL, + token TEXT NOT NULL UNIQUE, + password_hash TEXT, + expires_at TIMESTAMPTZ, + view_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Playlists CREATE TABLE IF NOT EXISTS playlists ( - id UUID PRIMARY KEY, - owner_id UUID NOT NULL, - name TEXT NOT NULL, - description TEXT, - is_public BOOLEAN NOT NULL DEFAULT FALSE, - is_smart BOOLEAN NOT NULL DEFAULT FALSE, - filter_query TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + owner_id UUID NOT NULL, + name TEXT NOT NULL, + description TEXT, + is_public BOOLEAN NOT NULL DEFAULT FALSE, + is_smart BOOLEAN NOT NULL DEFAULT FALSE, + filter_query TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Playlist items CREATE TABLE IF NOT EXISTS playlist_items ( - playlist_id UUID NOT NULL REFERENCES playlists (id) ON DELETE CASCADE, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - position INTEGER NOT NULL DEFAULT 0, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (playlist_id, media_id) + playlist_id UUID NOT NULL REFERENCES playlists(id) ON DELETE CASCADE, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + position INTEGER NOT NULL DEFAULT 0, + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (playlist_id, media_id) ); -- Usage events CREATE TABLE IF NOT EXISTS usage_events ( - id UUID PRIMARY KEY, - media_id UUID REFERENCES media_items (id) ON DELETE SET NULL, - user_id UUID, - event_type TEXT NOT NULL, - timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), - duration_secs DOUBLE PRECISION, - context_json JSONB + id UUID PRIMARY KEY, + media_id UUID REFERENCES media_items(id) ON DELETE SET NULL, + user_id UUID, + event_type TEXT NOT NULL, + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + duration_secs DOUBLE PRECISION, + context_json JSONB ); -CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events (media_id); - -CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events (user_id); - -CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events (timestamp); +CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events(media_id); +CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events(user_id); +CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events(timestamp); -- Watch history / progress CREATE TABLE IF NOT EXISTS watch_history ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - progress_secs DOUBLE PRECISION NOT NULL DEFAULT 0, - last_watched TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE (user_id, media_id) + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + progress_secs DOUBLE PRECISION NOT NULL DEFAULT 0, + last_watched TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(user_id, media_id) ); -- Subtitles CREATE TABLE IF NOT EXISTS subtitles ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - language TEXT, - format TEXT NOT NULL, - file_path TEXT, - is_embedded BOOLEAN NOT NULL DEFAULT FALSE, - track_index INTEGER, - offset_ms INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + language TEXT, + format TEXT NOT NULL, + file_path TEXT, + is_embedded BOOLEAN NOT NULL DEFAULT FALSE, + track_index INTEGER, + offset_ms INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles (media_id); +CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles(media_id); -- External metadata (enrichment) CREATE TABLE IF NOT EXISTS external_metadata ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - source TEXT NOT NULL, - external_id TEXT, - metadata_json JSONB NOT NULL DEFAULT '{}', - confidence DOUBLE PRECISION NOT NULL DEFAULT 0.0, - last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + source TEXT NOT NULL, + external_id TEXT, + metadata_json JSONB NOT NULL DEFAULT '{}', + confidence DOUBLE PRECISION NOT NULL DEFAULT 0.0, + last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata (media_id); +CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata(media_id); -- Transcode sessions CREATE TABLE IF NOT EXISTS transcode_sessions ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - user_id UUID, - profile TEXT NOT NULL, - cache_path TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - progress DOUBLE PRECISION NOT NULL DEFAULT 0.0, - error_message TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - expires_at TIMESTAMPTZ + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + user_id UUID, + profile TEXT NOT NULL, + cache_path TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + progress DOUBLE PRECISION NOT NULL DEFAULT 0.0, + error_message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ ); -CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions (media_id); +CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions(media_id); diff --git a/migrations/postgres/V9__fix_indexes_and_constraints.sql b/migrations/postgres/V9__fix_indexes_and_constraints.sql index 9446a94..a65fda3 100644 --- a/migrations/postgres/V9__fix_indexes_and_constraints.sql +++ b/migrations/postgres/V9__fix_indexes_and_constraints.sql @@ -1,26 +1,19 @@ -- Drop redundant indexes (already covered by UNIQUE constraints) DROP INDEX IF EXISTS idx_users_username; - DROP INDEX IF EXISTS idx_user_libraries_user_id; -- Add missing indexes for comments table -CREATE INDEX IF NOT EXISTS idx_comments_media ON comments (media_id); - -CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments (parent_comment_id); +CREATE INDEX IF NOT EXISTS idx_comments_media ON comments(media_id); +CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_comment_id); -- Remove duplicates before adding unique constraint DELETE FROM external_metadata e1 -WHERE - EXISTS ( - SELECT - 1 - FROM - external_metadata e2 - WHERE - e1.media_id = e2.media_id - AND e1.source = e2.source - AND e1.ctid < e2.ctid - ); +WHERE EXISTS ( + SELECT 1 FROM external_metadata e2 + WHERE e1.media_id = e2.media_id + AND e1.source = e2.source + AND e1.ctid < e2.ctid +); -- Add unique constraint for external_metadata (idempotent) DO $$ diff --git a/migrations/sqlite/V10__incremental_scan.sql b/migrations/sqlite/V10__incremental_scan.sql index a070c7f..76db5c9 100644 --- a/migrations/sqlite/V10__incremental_scan.sql +++ b/migrations/sqlite/V10__incremental_scan.sql @@ -1,19 +1,19 @@ -- Add file_mtime column to media_items table for incremental scanning -- Stores Unix timestamp in seconds of the file's modification time -ALTER TABLE media_items -ADD COLUMN file_mtime INTEGER; + +ALTER TABLE media_items ADD COLUMN file_mtime INTEGER; -- Create index for quick mtime lookups -CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items (file_mtime); +CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items(file_mtime); -- Create a scan_history table to track when each directory was last scanned CREATE TABLE IF NOT EXISTS scan_history ( - id TEXT PRIMARY KEY, - directory TEXT NOT NULL UNIQUE, - last_scan_at TEXT NOT NULL, - files_scanned INTEGER NOT NULL DEFAULT 0, - files_changed INTEGER NOT NULL DEFAULT 0, - scan_duration_ms INTEGER + id TEXT PRIMARY KEY, + directory TEXT NOT NULL UNIQUE, + last_scan_at TEXT NOT NULL, + files_scanned INTEGER NOT NULL DEFAULT 0, + files_changed INTEGER NOT NULL DEFAULT 0, + scan_duration_ms INTEGER ); -CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history (directory); +CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history(directory); diff --git a/migrations/sqlite/V11__session_persistence.sql b/migrations/sqlite/V11__session_persistence.sql index e5e7a94..b4e2753 100644 --- a/migrations/sqlite/V11__session_persistence.sql +++ b/migrations/sqlite/V11__session_persistence.sql @@ -1,17 +1,18 @@ -- Session persistence for database-backed sessions -- Replaces in-memory session storage + CREATE TABLE IF NOT EXISTS sessions ( - session_token TEXT PRIMARY KEY NOT NULL, - user_id TEXT, - username TEXT NOT NULL, - role TEXT NOT NULL, - created_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - last_accessed TEXT NOT NULL + session_token TEXT PRIMARY KEY NOT NULL, + user_id TEXT, + username TEXT NOT NULL, + role TEXT NOT NULL, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + last_accessed TEXT NOT NULL ); -- Index for efficient cleanup of expired sessions -CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions (expires_at); +CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); -- Index for listing sessions by username -CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions (username); +CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions(username); diff --git a/migrations/sqlite/V12__book_management.sql b/migrations/sqlite/V12__book_management.sql index 9b18100..9823b87 100644 --- a/migrations/sqlite/V12__book_management.sql +++ b/migrations/sqlite/V12__book_management.sql @@ -1,62 +1,54 @@ -- V12: Book Management Schema -- Adds comprehensive book metadata tracking, authors, and identifiers + -- Book metadata (supplements media_items for EPUB/PDF/MOBI) CREATE TABLE book_metadata ( - media_id TEXT PRIMARY KEY REFERENCES media_items (id) ON DELETE CASCADE, - isbn TEXT, - isbn13 TEXT, -- Normalized ISBN-13 for lookups - publisher TEXT, - language TEXT, -- ISO 639-1 code - page_count INTEGER, - publication_date TEXT, -- ISO 8601 date string - series_name TEXT, - series_index REAL, -- Supports 1.5, etc. - format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - updated_at TEXT NOT NULL DEFAULT (datetime ('now')) + media_id TEXT PRIMARY KEY REFERENCES media_items(id) ON DELETE CASCADE, + isbn TEXT, + isbn13 TEXT, -- Normalized ISBN-13 for lookups + publisher TEXT, + language TEXT, -- ISO 639-1 code + page_count INTEGER, + publication_date TEXT, -- ISO 8601 date string + series_name TEXT, + series_index REAL, -- Supports 1.5, etc. + format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) STRICT; -CREATE INDEX idx_book_isbn13 ON book_metadata (isbn13); - -CREATE INDEX idx_book_series ON book_metadata (series_name, series_index); - -CREATE INDEX idx_book_publisher ON book_metadata (publisher); - -CREATE INDEX idx_book_language ON book_metadata (language); +CREATE INDEX idx_book_isbn13 ON book_metadata(isbn13); +CREATE INDEX idx_book_series ON book_metadata(series_name, series_index); +CREATE INDEX idx_book_publisher ON book_metadata(publisher); +CREATE INDEX idx_book_language ON book_metadata(language); -- Multiple authors per book (many-to-many) CREATE TABLE book_authors ( - media_id TEXT NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - author_name TEXT NOT NULL, - author_sort TEXT, -- "Last, First" for sorting - role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator - position INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (media_id, author_name, role) + media_id TEXT NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + author_name TEXT NOT NULL, + author_sort TEXT, -- "Last, First" for sorting + role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator + position INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (media_id, author_name, role) ) STRICT; -CREATE INDEX idx_book_authors_name ON book_authors (author_name); - -CREATE INDEX idx_book_authors_sort ON book_authors (author_sort); +CREATE INDEX idx_book_authors_name ON book_authors(author_name); +CREATE INDEX idx_book_authors_sort ON book_authors(author_sort); -- Multiple identifiers (ISBN variants, ASIN, DOI, etc.) CREATE TABLE book_identifiers ( - media_id TEXT NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, - identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc - identifier_value TEXT NOT NULL, - PRIMARY KEY (media_id, identifier_type, identifier_value) + media_id TEXT NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc + identifier_value TEXT NOT NULL, + PRIMARY KEY (media_id, identifier_type, identifier_value) ) STRICT; -CREATE INDEX idx_book_identifiers ON book_identifiers (identifier_type, identifier_value); +CREATE INDEX idx_book_identifiers ON book_identifiers(identifier_type, identifier_value); -- Trigger to update updated_at on book_metadata changes CREATE TRIGGER update_book_metadata_timestamp -AFTER -UPDATE ON book_metadata FOR EACH ROW + AFTER UPDATE ON book_metadata + FOR EACH ROW BEGIN -UPDATE book_metadata -SET - updated_at = datetime ('now') -WHERE - media_id = NEW.media_id; - + UPDATE book_metadata SET updated_at = datetime('now') WHERE media_id = NEW.media_id; END; diff --git a/migrations/sqlite/V13__photo_metadata.sql b/migrations/sqlite/V13__photo_metadata.sql index 640374f..616b2fa 100644 --- a/migrations/sqlite/V13__photo_metadata.sql +++ b/migrations/sqlite/V13__photo_metadata.sql @@ -1,40 +1,15 @@ -- V13: Enhanced photo metadata support -- Add photo-specific fields to media_items table -ALTER TABLE media_items -ADD COLUMN date_taken TIMESTAMP; -ALTER TABLE media_items -ADD COLUMN latitude REAL; - -ALTER TABLE media_items -ADD COLUMN longitude REAL; - -ALTER TABLE media_items -ADD COLUMN camera_make TEXT; - -ALTER TABLE media_items -ADD COLUMN camera_model TEXT; - -ALTER TABLE media_items -ADD COLUMN rating INTEGER CHECK ( - rating >= 0 - AND rating <= 5 -); +ALTER TABLE media_items ADD COLUMN date_taken TIMESTAMP; +ALTER TABLE media_items ADD COLUMN latitude REAL; +ALTER TABLE media_items ADD COLUMN longitude REAL; +ALTER TABLE media_items ADD COLUMN camera_make TEXT; +ALTER TABLE media_items ADD COLUMN camera_model TEXT; +ALTER TABLE media_items ADD COLUMN rating INTEGER CHECK (rating >= 0 AND rating <= 5); -- Indexes for photo queries -CREATE INDEX idx_media_date_taken ON media_items (date_taken) -WHERE - date_taken IS NOT NULL; - -CREATE INDEX idx_media_location ON media_items (latitude, longitude) -WHERE - latitude IS NOT NULL - AND longitude IS NOT NULL; - -CREATE INDEX idx_media_camera ON media_items (camera_make) -WHERE - camera_make IS NOT NULL; - -CREATE INDEX idx_media_rating ON media_items (rating) -WHERE - rating IS NOT NULL; +CREATE INDEX idx_media_date_taken ON media_items(date_taken) WHERE date_taken IS NOT NULL; +CREATE INDEX idx_media_location ON media_items(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL; +CREATE INDEX idx_media_camera ON media_items(camera_make) WHERE camera_make IS NOT NULL; +CREATE INDEX idx_media_rating ON media_items(rating) WHERE rating IS NOT NULL; diff --git a/migrations/sqlite/V14__perceptual_hash.sql b/migrations/sqlite/V14__perceptual_hash.sql index 1d3c634..4bdc677 100644 --- a/migrations/sqlite/V14__perceptual_hash.sql +++ b/migrations/sqlite/V14__perceptual_hash.sql @@ -1,9 +1,7 @@ -- V14: Perceptual hash for duplicate detection -- Add perceptual hash column for image similarity detection -ALTER TABLE media_items -ADD COLUMN perceptual_hash TEXT; + +ALTER TABLE media_items ADD COLUMN perceptual_hash TEXT; -- Index for perceptual hash lookups -CREATE INDEX idx_media_phash ON media_items (perceptual_hash) -WHERE - perceptual_hash IS NOT NULL; +CREATE INDEX idx_media_phash ON media_items(perceptual_hash) WHERE perceptual_hash IS NOT NULL; diff --git a/migrations/sqlite/V15__managed_storage.sql b/migrations/sqlite/V15__managed_storage.sql index 1f10c7b..b7f2a9d 100644 --- a/migrations/sqlite/V15__managed_storage.sql +++ b/migrations/sqlite/V15__managed_storage.sql @@ -1,33 +1,30 @@ -- V15: Managed File Storage -- Adds server-side content-addressable storage for uploaded files + -- Add storage mode to media_items (external = file on disk, managed = in content-addressable storage) -ALTER TABLE media_items -ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; +ALTER TABLE media_items ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; -- Original filename for managed uploads (preserved separately from file_name which may be normalized) -ALTER TABLE media_items -ADD COLUMN original_filename TEXT; +ALTER TABLE media_items ADD COLUMN original_filename TEXT; -- When the file was uploaded to managed storage -ALTER TABLE media_items -ADD COLUMN uploaded_at TEXT; +ALTER TABLE media_items ADD COLUMN uploaded_at TEXT; -- Storage key for looking up the blob (usually same as content_hash for deduplication) -ALTER TABLE media_items -ADD COLUMN storage_key TEXT; +ALTER TABLE media_items ADD COLUMN storage_key TEXT; -- Managed blobs table - tracks deduplicated file storage CREATE TABLE managed_blobs ( - content_hash TEXT PRIMARY KEY NOT NULL, - file_size INTEGER NOT NULL, - mime_type TEXT NOT NULL, - reference_count INTEGER NOT NULL DEFAULT 1, - stored_at TEXT NOT NULL, - last_verified TEXT + content_hash TEXT PRIMARY KEY NOT NULL, + file_size INTEGER NOT NULL, + mime_type TEXT NOT NULL, + reference_count INTEGER NOT NULL DEFAULT 1, + stored_at TEXT NOT NULL, + last_verified TEXT ); -- Index for finding managed media items -CREATE INDEX idx_media_storage_mode ON media_items (storage_mode); +CREATE INDEX idx_media_storage_mode ON media_items(storage_mode); -- Index for finding orphaned blobs (reference_count = 0) -CREATE INDEX idx_blobs_reference_count ON managed_blobs (reference_count); +CREATE INDEX idx_blobs_reference_count ON managed_blobs(reference_count); diff --git a/migrations/sqlite/V16__sync_system.sql b/migrations/sqlite/V16__sync_system.sql index 6787034..8941500 100644 --- a/migrations/sqlite/V16__sync_system.sql +++ b/migrations/sqlite/V16__sync_system.sql @@ -1,129 +1,117 @@ -- V16: Cross-Device Sync System -- Adds device registration, change tracking, and chunked upload support + -- Sync devices table CREATE TABLE sync_devices ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - device_type TEXT NOT NULL, - client_version TEXT NOT NULL, - os_info TEXT, - device_token_hash TEXT NOT NULL UNIQUE, - last_sync_at TEXT, - last_seen_at TEXT NOT NULL, - sync_cursor INTEGER DEFAULT 0, - enabled INTEGER NOT NULL DEFAULT 1, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + device_type TEXT NOT NULL, + client_version TEXT NOT NULL, + os_info TEXT, + device_token_hash TEXT NOT NULL UNIQUE, + last_sync_at TEXT, + last_seen_at TEXT NOT NULL, + sync_cursor INTEGER DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -CREATE INDEX idx_sync_devices_user ON sync_devices (user_id); - -CREATE INDEX idx_sync_devices_token ON sync_devices (device_token_hash); +CREATE INDEX idx_sync_devices_user ON sync_devices(user_id); +CREATE INDEX idx_sync_devices_token ON sync_devices(device_token_hash); -- Sync log table - tracks all changes for sync CREATE TABLE sync_log ( - id TEXT PRIMARY KEY NOT NULL, - sequence INTEGER NOT NULL UNIQUE, - change_type TEXT NOT NULL, - media_id TEXT, - path TEXT NOT NULL, - content_hash TEXT, - file_size INTEGER, - metadata_json TEXT, - changed_by_device TEXT, - timestamp TEXT NOT NULL, - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL, - FOREIGN KEY (changed_by_device) REFERENCES sync_devices (id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + sequence INTEGER NOT NULL UNIQUE, + change_type TEXT NOT NULL, + media_id TEXT, + path TEXT NOT NULL, + content_hash TEXT, + file_size INTEGER, + metadata_json TEXT, + changed_by_device TEXT, + timestamp TEXT NOT NULL, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE SET NULL, + FOREIGN KEY (changed_by_device) REFERENCES sync_devices(id) ON DELETE SET NULL ); -CREATE INDEX idx_sync_log_sequence ON sync_log (sequence); - -CREATE INDEX idx_sync_log_path ON sync_log (path); - -CREATE INDEX idx_sync_log_timestamp ON sync_log (timestamp); +CREATE INDEX idx_sync_log_sequence ON sync_log(sequence); +CREATE INDEX idx_sync_log_path ON sync_log(path); +CREATE INDEX idx_sync_log_timestamp ON sync_log(timestamp); -- Sequence counter for sync log CREATE TABLE sync_sequence ( - id INTEGER PRIMARY KEY CHECK (id = 1), - current_value INTEGER NOT NULL DEFAULT 0 + id INTEGER PRIMARY KEY CHECK (id = 1), + current_value INTEGER NOT NULL DEFAULT 0 ); - -INSERT INTO - sync_sequence (id, current_value) -VALUES - (1, 0); +INSERT INTO sync_sequence (id, current_value) VALUES (1, 0); -- Device sync state - tracks sync status per device per file CREATE TABLE device_sync_state ( - device_id TEXT NOT NULL, - path TEXT NOT NULL, - local_hash TEXT, - server_hash TEXT, - local_mtime INTEGER, - server_mtime INTEGER, - sync_status TEXT NOT NULL, - last_synced_at TEXT, - conflict_info_json TEXT, - PRIMARY KEY (device_id, path), - FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE + device_id TEXT NOT NULL, + path TEXT NOT NULL, + local_hash TEXT, + server_hash TEXT, + local_mtime INTEGER, + server_mtime INTEGER, + sync_status TEXT NOT NULL, + last_synced_at TEXT, + conflict_info_json TEXT, + PRIMARY KEY (device_id, path), + FOREIGN KEY (device_id) REFERENCES sync_devices(id) ON DELETE CASCADE ); -CREATE INDEX idx_device_sync_status ON device_sync_state (device_id, sync_status); +CREATE INDEX idx_device_sync_status ON device_sync_state(device_id, sync_status); -- Upload sessions for chunked uploads CREATE TABLE upload_sessions ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL, - target_path TEXT NOT NULL, - expected_hash TEXT NOT NULL, - expected_size INTEGER NOT NULL, - chunk_size INTEGER NOT NULL, - chunk_count INTEGER NOT NULL, - status TEXT NOT NULL, - created_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - last_activity TEXT NOT NULL, - FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL, + target_path TEXT NOT NULL, + expected_hash TEXT NOT NULL, + expected_size INTEGER NOT NULL, + chunk_size INTEGER NOT NULL, + chunk_count INTEGER NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + last_activity TEXT NOT NULL, + FOREIGN KEY (device_id) REFERENCES sync_devices(id) ON DELETE CASCADE ); -CREATE INDEX idx_upload_sessions_device ON upload_sessions (device_id); - -CREATE INDEX idx_upload_sessions_status ON upload_sessions (status); - -CREATE INDEX idx_upload_sessions_expires ON upload_sessions (expires_at); +CREATE INDEX idx_upload_sessions_device ON upload_sessions(device_id); +CREATE INDEX idx_upload_sessions_status ON upload_sessions(status); +CREATE INDEX idx_upload_sessions_expires ON upload_sessions(expires_at); -- Upload chunks - tracks received chunks CREATE TABLE upload_chunks ( - upload_id TEXT NOT NULL, - chunk_index INTEGER NOT NULL, - offset - INTEGER NOT NULL, + upload_id TEXT NOT NULL, + chunk_index INTEGER NOT NULL, + offset INTEGER NOT NULL, size INTEGER NOT NULL, hash TEXT NOT NULL, received_at TEXT NOT NULL, PRIMARY KEY (upload_id, chunk_index), - FOREIGN KEY (upload_id) REFERENCES upload_sessions (id) ON DELETE CASCADE + FOREIGN KEY (upload_id) REFERENCES upload_sessions(id) ON DELETE CASCADE ); -- Sync conflicts CREATE TABLE sync_conflicts ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL, - path TEXT NOT NULL, - local_hash TEXT NOT NULL, - local_mtime INTEGER NOT NULL, - server_hash TEXT NOT NULL, - server_mtime INTEGER NOT NULL, - detected_at TEXT NOT NULL, - resolved_at TEXT, - resolution TEXT, - FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL, + path TEXT NOT NULL, + local_hash TEXT NOT NULL, + local_mtime INTEGER NOT NULL, + server_hash TEXT NOT NULL, + server_mtime INTEGER NOT NULL, + detected_at TEXT NOT NULL, + resolved_at TEXT, + resolution TEXT, + FOREIGN KEY (device_id) REFERENCES sync_devices(id) ON DELETE CASCADE ); -CREATE INDEX idx_sync_conflicts_device ON sync_conflicts (device_id); - -CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts (device_id, resolved_at) -WHERE - resolved_at IS NULL; +CREATE INDEX idx_sync_conflicts_device ON sync_conflicts(device_id); +CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts(device_id, resolved_at) WHERE resolved_at IS NULL; diff --git a/migrations/sqlite/V17__enhanced_sharing.sql b/migrations/sqlite/V17__enhanced_sharing.sql index ac7d93e..1cd17f3 100644 --- a/migrations/sqlite/V17__enhanced_sharing.sql +++ b/migrations/sqlite/V17__enhanced_sharing.sql @@ -1,133 +1,85 @@ -- V17: Enhanced Sharing System -- Replaces simple share_links with comprehensive sharing capabilities + -- Enhanced shares table CREATE TABLE shares ( - id TEXT PRIMARY KEY NOT NULL, - target_type TEXT NOT NULL CHECK ( - target_type IN ('media', 'collection', 'tag', 'saved_search') - ), - target_id TEXT NOT NULL, - owner_id TEXT NOT NULL, - recipient_type TEXT NOT NULL CHECK ( - recipient_type IN ('public_link', 'user', 'group', 'federated') - ), - recipient_user_id TEXT, - recipient_group_id TEXT, - recipient_federated_handle TEXT, - recipient_federated_server TEXT, - public_token TEXT UNIQUE, - public_password_hash TEXT, - perm_view INTEGER NOT NULL DEFAULT 1, - perm_download INTEGER NOT NULL DEFAULT 0, - perm_edit INTEGER NOT NULL DEFAULT 0, - perm_delete INTEGER NOT NULL DEFAULT 0, - perm_reshare INTEGER NOT NULL DEFAULT 0, - perm_add INTEGER NOT NULL DEFAULT 0, - note TEXT, - expires_at TEXT, - access_count INTEGER NOT NULL DEFAULT 0, - last_accessed TEXT, - inherit_to_children INTEGER NOT NULL DEFAULT 1, - parent_share_id TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (recipient_user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (parent_share_id) REFERENCES shares (id) ON DELETE CASCADE, - UNIQUE ( - owner_id, - target_type, - target_id, - recipient_type, - recipient_user_id - ) + id TEXT PRIMARY KEY NOT NULL, + target_type TEXT NOT NULL CHECK (target_type IN ('media', 'collection', 'tag', 'saved_search')), + target_id TEXT NOT NULL, + owner_id TEXT NOT NULL, + recipient_type TEXT NOT NULL CHECK (recipient_type IN ('public_link', 'user', 'group', 'federated')), + recipient_user_id TEXT, + recipient_group_id TEXT, + recipient_federated_handle TEXT, + recipient_federated_server TEXT, + public_token TEXT UNIQUE, + public_password_hash TEXT, + perm_view INTEGER NOT NULL DEFAULT 1, + perm_download INTEGER NOT NULL DEFAULT 0, + perm_edit INTEGER NOT NULL DEFAULT 0, + perm_delete INTEGER NOT NULL DEFAULT 0, + perm_reshare INTEGER NOT NULL DEFAULT 0, + perm_add INTEGER NOT NULL DEFAULT 0, + note TEXT, + expires_at TEXT, + access_count INTEGER NOT NULL DEFAULT 0, + last_accessed TEXT, + inherit_to_children INTEGER NOT NULL DEFAULT 1, + parent_share_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (recipient_user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (parent_share_id) REFERENCES shares(id) ON DELETE CASCADE, + UNIQUE(owner_id, target_type, target_id, recipient_type, recipient_user_id) ); -CREATE INDEX idx_shares_owner ON shares (owner_id); - -CREATE INDEX idx_shares_recipient_user ON shares (recipient_user_id); - -CREATE INDEX idx_shares_target ON shares (target_type, target_id); - -CREATE INDEX idx_shares_token ON shares (public_token); - -CREATE INDEX idx_shares_expires ON shares (expires_at); +CREATE INDEX idx_shares_owner ON shares(owner_id); +CREATE INDEX idx_shares_recipient_user ON shares(recipient_user_id); +CREATE INDEX idx_shares_target ON shares(target_type, target_id); +CREATE INDEX idx_shares_token ON shares(public_token); +CREATE INDEX idx_shares_expires ON shares(expires_at); -- Share activity log CREATE TABLE share_activity ( - id TEXT PRIMARY KEY NOT NULL, - share_id TEXT NOT NULL, - actor_id TEXT, - actor_ip TEXT, - action TEXT NOT NULL, - details TEXT, - timestamp TEXT NOT NULL, - FOREIGN KEY (share_id) REFERENCES shares (id) ON DELETE CASCADE, - FOREIGN KEY (actor_id) REFERENCES users (id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + share_id TEXT NOT NULL, + actor_id TEXT, + actor_ip TEXT, + action TEXT NOT NULL, + details TEXT, + timestamp TEXT NOT NULL, + FOREIGN KEY (share_id) REFERENCES shares(id) ON DELETE CASCADE, + FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL ); -CREATE INDEX idx_share_activity_share ON share_activity (share_id); - -CREATE INDEX idx_share_activity_timestamp ON share_activity (timestamp); +CREATE INDEX idx_share_activity_share ON share_activity(share_id); +CREATE INDEX idx_share_activity_timestamp ON share_activity(timestamp); -- Share notifications CREATE TABLE share_notifications ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL, - share_id TEXT NOT NULL, - notification_type TEXT NOT NULL, - is_read INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (share_id) REFERENCES shares (id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + share_id TEXT NOT NULL, + notification_type TEXT NOT NULL, + is_read INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (share_id) REFERENCES shares(id) ON DELETE CASCADE ); -CREATE INDEX idx_share_notifications_user ON share_notifications (user_id); - -CREATE INDEX idx_share_notifications_unread ON share_notifications (user_id, is_read) -WHERE - is_read = 0; +CREATE INDEX idx_share_notifications_user ON share_notifications(user_id); +CREATE INDEX idx_share_notifications_unread ON share_notifications(user_id, is_read) WHERE is_read = 0; -- Migrate existing share_links to new shares table (if share_links exists) -INSERT -OR IGNORE INTO shares ( - id, - target_type, - target_id, - owner_id, - recipient_type, - public_token, - public_password_hash, - perm_view, - perm_download, - access_count, - expires_at, - created_at, - updated_at +INSERT OR IGNORE INTO shares ( + id, target_type, target_id, owner_id, recipient_type, + public_token, public_password_hash, perm_view, perm_download, + access_count, expires_at, created_at, updated_at ) SELECT - id, - 'media', - media_id, - created_by, - 'public_link', - token, - password_hash, - 1, - 1, - view_count, - expires_at, - created_at, - created_at -FROM - share_links -WHERE - EXISTS ( - SELECT - 1 - FROM - sqlite_master - WHERE - type = 'table' - AND name = 'share_links' - ); + id, 'media', media_id, created_by, 'public_link', + token, password_hash, 1, 1, + view_count, expires_at, created_at, created_at +FROM share_links +WHERE EXISTS (SELECT 1 FROM sqlite_master WHERE type='table' AND name='share_links'); diff --git a/migrations/sqlite/V18__file_management.sql b/migrations/sqlite/V18__file_management.sql index 08599dd..0af0738 100644 --- a/migrations/sqlite/V18__file_management.sql +++ b/migrations/sqlite/V18__file_management.sql @@ -1,13 +1,11 @@ -- V18: File Management (Rename, Move, Trash) -- Adds soft delete support for trash/recycle bin functionality + -- Add deleted_at column for soft delete (trash) -ALTER TABLE media_items -ADD COLUMN deleted_at TEXT; +ALTER TABLE media_items ADD COLUMN deleted_at TEXT; -- Index for efficient trash queries -CREATE INDEX idx_media_deleted_at ON media_items (deleted_at); +CREATE INDEX idx_media_deleted_at ON media_items(deleted_at); -- Index for listing non-deleted items (most common query pattern) -CREATE INDEX idx_media_not_deleted ON media_items (id) -WHERE - deleted_at IS NULL; +CREATE INDEX idx_media_not_deleted ON media_items(id) WHERE deleted_at IS NULL; diff --git a/migrations/sqlite/V19__markdown_links.sql b/migrations/sqlite/V19__markdown_links.sql index 214e40a..7cbdda3 100644 --- a/migrations/sqlite/V19__markdown_links.sql +++ b/migrations/sqlite/V19__markdown_links.sql @@ -1,35 +1,35 @@ -- V19: Markdown Links (Obsidian-style bidirectional links) -- Adds support for wikilinks, markdown links, embeds, and backlink tracking + -- Table for storing extracted markdown links CREATE TABLE IF NOT EXISTS markdown_links ( - id TEXT PRIMARY KEY NOT NULL, - source_media_id TEXT NOT NULL, - target_path TEXT NOT NULL, -- raw link target (wikilink or path) - target_media_id TEXT, -- resolved media_id (nullable if unresolved) - link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' - link_text TEXT, -- display text for the link - line_number INTEGER, -- line number in source file - context TEXT, -- surrounding text for preview - created_at TEXT NOT NULL, - FOREIGN KEY (source_media_id) REFERENCES media_items (id) ON DELETE CASCADE, - FOREIGN KEY (target_media_id) REFERENCES media_items (id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + source_media_id TEXT NOT NULL, + target_path TEXT NOT NULL, -- raw link target (wikilink or path) + target_media_id TEXT, -- resolved media_id (nullable if unresolved) + link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' + link_text TEXT, -- display text for the link + line_number INTEGER, -- line number in source file + context TEXT, -- surrounding text for preview + created_at TEXT NOT NULL, + FOREIGN KEY (source_media_id) REFERENCES media_items(id) ON DELETE CASCADE, + FOREIGN KEY (target_media_id) REFERENCES media_items(id) ON DELETE SET NULL ); -- Index for efficient outgoing link queries (what does this note link to?) -CREATE INDEX idx_links_source ON markdown_links (source_media_id); +CREATE INDEX idx_links_source ON markdown_links(source_media_id); -- Index for efficient backlink queries (what links to this note?) -CREATE INDEX idx_links_target ON markdown_links (target_media_id); +CREATE INDEX idx_links_target ON markdown_links(target_media_id); -- Index for path-based resolution (finding unresolved links) -CREATE INDEX idx_links_target_path ON markdown_links (target_path); +CREATE INDEX idx_links_target_path ON markdown_links(target_path); -- Index for link type filtering -CREATE INDEX idx_links_type ON markdown_links (link_type); +CREATE INDEX idx_links_type ON markdown_links(link_type); -- Track when links were last extracted from a media item -ALTER TABLE media_items -ADD COLUMN links_extracted_at TEXT; +ALTER TABLE media_items ADD COLUMN links_extracted_at TEXT; -- Index for finding media items that need link extraction -CREATE INDEX idx_media_links_extracted ON media_items (links_extracted_at); +CREATE INDEX idx_media_links_extracted ON media_items(links_extracted_at); diff --git a/migrations/sqlite/V1__initial_schema.sql b/migrations/sqlite/V1__initial_schema.sql index b6ccd0e..5b16abf 100644 --- a/migrations/sqlite/V1__initial_schema.sql +++ b/migrations/sqlite/V1__initial_schema.sql @@ -1,75 +1,77 @@ -CREATE TABLE IF NOT EXISTS root_dirs (path TEXT PRIMARY KEY NOT NULL); +CREATE TABLE IF NOT EXISTS root_dirs ( + path TEXT PRIMARY KEY NOT NULL +); CREATE TABLE IF NOT EXISTS media_items ( - id TEXT PRIMARY KEY NOT NULL, - path TEXT NOT NULL UNIQUE, - file_name TEXT NOT NULL, - media_type TEXT NOT NULL, - content_hash TEXT NOT NULL UNIQUE, - file_size INTEGER NOT NULL, - title TEXT, - artist TEXT, - album TEXT, - genre TEXT, - year INTEGER, - duration_secs REAL, - description TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY NOT NULL, + path TEXT NOT NULL UNIQUE, + file_name TEXT NOT NULL, + media_type TEXT NOT NULL, + content_hash TEXT NOT NULL UNIQUE, + file_size INTEGER NOT NULL, + title TEXT, + artist TEXT, + album TEXT, + genre TEXT, + year INTEGER, + duration_secs REAL, + description TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tags ( - id TEXT PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - parent_id TEXT, - created_at TEXT NOT NULL, - FOREIGN KEY (parent_id) REFERENCES tags (id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + parent_id TEXT, + created_at TEXT NOT NULL, + FOREIGN KEY (parent_id) REFERENCES tags(id) ON DELETE SET NULL ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags (name, parent_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags(name, parent_id); CREATE TABLE IF NOT EXISTS media_tags ( - media_id TEXT NOT NULL, - tag_id TEXT NOT NULL, - PRIMARY KEY (media_id, tag_id), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE, - FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE + media_id TEXT NOT NULL, + tag_id TEXT NOT NULL, + PRIMARY KEY (media_id, tag_id), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS collections ( - id TEXT PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - description TEXT, - kind TEXT NOT NULL, - filter_query TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + kind TEXT NOT NULL, + filter_query TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS collection_members ( - collection_id TEXT NOT NULL, - media_id TEXT NOT NULL, - position INTEGER NOT NULL DEFAULT 0, - added_at TEXT NOT NULL, - PRIMARY KEY (collection_id, media_id), - FOREIGN KEY (collection_id) REFERENCES collections (id) ON DELETE CASCADE, - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + collection_id TEXT NOT NULL, + media_id TEXT NOT NULL, + position INTEGER NOT NULL DEFAULT 0, + added_at TEXT NOT NULL, + PRIMARY KEY (collection_id, media_id), + FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS audit_log ( - id TEXT PRIMARY KEY NOT NULL, - media_id TEXT, - action TEXT NOT NULL, - details TEXT, - timestamp TEXT NOT NULL, - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + media_id TEXT, + action TEXT NOT NULL, + details TEXT, + timestamp TEXT NOT NULL, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS custom_fields ( - media_id TEXT NOT NULL, - field_name TEXT NOT NULL, - field_type TEXT NOT NULL, - field_value TEXT NOT NULL, - PRIMARY KEY (media_id, field_name), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + media_id TEXT NOT NULL, + field_name TEXT NOT NULL, + field_type TEXT NOT NULL, + field_value TEXT NOT NULL, + PRIMARY KEY (media_id, field_name), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); diff --git a/migrations/sqlite/V2__fts5_indexes.sql b/migrations/sqlite/V2__fts5_indexes.sql index 01270a0..00c5597 100644 --- a/migrations/sqlite/V2__fts5_indexes.sql +++ b/migrations/sqlite/V2__fts5_indexes.sql @@ -1,114 +1,27 @@ -CREATE VIRTUAL TABLE IF NOT EXISTS media_fts USING fts5 ( - title, - artist, - album, - genre, - description, - file_name, - content = 'media_items', - content_rowid = 'rowid' +CREATE VIRTUAL TABLE IF NOT EXISTS media_fts USING fts5( + title, + artist, + album, + genre, + description, + file_name, + content='media_items', + content_rowid='rowid' ); -CREATE TRIGGER IF NOT EXISTS media_fts_insert -AFTER INSERT ON media_items -BEGIN -INSERT INTO - media_fts ( - rowid, - title, - artist, - album, - genre, - description, - file_name - ) -VALUES - ( - new.rowid, - new.title, - new.artist, - new.album, - new.genre, - new.description, - new.file_name - ); - +CREATE TRIGGER IF NOT EXISTS media_fts_insert AFTER INSERT ON media_items BEGIN + INSERT INTO media_fts(rowid, title, artist, album, genre, description, file_name) + VALUES (new.rowid, new.title, new.artist, new.album, new.genre, new.description, new.file_name); END; -CREATE TRIGGER IF NOT EXISTS media_fts_update -AFTER -UPDATE ON media_items -BEGIN -INSERT INTO - media_fts ( - media_fts, - rowid, - title, - artist, - album, - genre, - description, - file_name - ) -VALUES - ( - 'delete', - old.rowid, - old.title, - old.artist, - old.album, - old.genre, - old.description, - old.file_name - ); - -INSERT INTO - media_fts ( - rowid, - title, - artist, - album, - genre, - description, - file_name - ) -VALUES - ( - new.rowid, - new.title, - new.artist, - new.album, - new.genre, - new.description, - new.file_name - ); - +CREATE TRIGGER IF NOT EXISTS media_fts_update AFTER UPDATE ON media_items BEGIN + INSERT INTO media_fts(media_fts, rowid, title, artist, album, genre, description, file_name) + VALUES ('delete', old.rowid, old.title, old.artist, old.album, old.genre, old.description, old.file_name); + INSERT INTO media_fts(rowid, title, artist, album, genre, description, file_name) + VALUES (new.rowid, new.title, new.artist, new.album, new.genre, new.description, new.file_name); END; -CREATE TRIGGER IF NOT EXISTS media_fts_delete -AFTER DELETE ON media_items -BEGIN -INSERT INTO - media_fts ( - media_fts, - rowid, - title, - artist, - album, - genre, - description, - file_name - ) -VALUES - ( - 'delete', - old.rowid, - old.title, - old.artist, - old.album, - old.genre, - old.description, - old.file_name - ); - +CREATE TRIGGER IF NOT EXISTS media_fts_delete AFTER DELETE ON media_items BEGIN + INSERT INTO media_fts(media_fts, rowid, title, artist, album, genre, description, file_name) + VALUES ('delete', old.rowid, old.title, old.artist, old.album, old.genre, old.description, old.file_name); END; diff --git a/migrations/sqlite/V3__audit_indexes.sql b/migrations/sqlite/V3__audit_indexes.sql index 5372307..1c741fe 100644 --- a/migrations/sqlite/V3__audit_indexes.sql +++ b/migrations/sqlite/V3__audit_indexes.sql @@ -1,11 +1,6 @@ -CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log (media_id); - -CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log (timestamp); - -CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action); - -CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items (content_hash); - -CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items (media_type); - -CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items (created_at); +CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log(media_id); +CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp); +CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action); +CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items(content_hash); +CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items(media_type); +CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items(created_at); diff --git a/migrations/sqlite/V4__thumbnail_path.sql b/migrations/sqlite/V4__thumbnail_path.sql index 4c23b5b..9021884 100644 --- a/migrations/sqlite/V4__thumbnail_path.sql +++ b/migrations/sqlite/V4__thumbnail_path.sql @@ -1,2 +1 @@ -ALTER TABLE media_items -ADD COLUMN thumbnail_path TEXT; +ALTER TABLE media_items ADD COLUMN thumbnail_path TEXT; diff --git a/migrations/sqlite/V5__integrity_and_saved_searches.sql b/migrations/sqlite/V5__integrity_and_saved_searches.sql index a8b05ea..650da16 100644 --- a/migrations/sqlite/V5__integrity_and_saved_searches.sql +++ b/migrations/sqlite/V5__integrity_and_saved_searches.sql @@ -1,15 +1,12 @@ -- Integrity tracking columns -ALTER TABLE media_items -ADD COLUMN last_verified_at TEXT; - -ALTER TABLE media_items -ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; +ALTER TABLE media_items ADD COLUMN last_verified_at TEXT; +ALTER TABLE media_items ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; -- Saved searches CREATE TABLE IF NOT EXISTS saved_searches ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - query TEXT NOT NULL, - sort_order TEXT, - created_at TEXT NOT NULL + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + query TEXT NOT NULL, + sort_order TEXT, + created_at TEXT NOT NULL ); diff --git a/migrations/sqlite/V6__plugin_system.sql b/migrations/sqlite/V6__plugin_system.sql index c675177..f4e7790 100644 --- a/migrations/sqlite/V6__plugin_system.sql +++ b/migrations/sqlite/V6__plugin_system.sql @@ -1,16 +1,15 @@ -- Plugin registry table CREATE TABLE plugin_registry ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - version TEXT NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - config_json TEXT, - manifest_json TEXT, - installed_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + version TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + config_json TEXT, + manifest_json TEXT, + installed_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); -- Index for quick lookups -CREATE INDEX idx_plugin_registry_enabled ON plugin_registry (enabled); - -CREATE INDEX idx_plugin_registry_name ON plugin_registry (name); +CREATE INDEX idx_plugin_registry_enabled ON plugin_registry(enabled); +CREATE INDEX idx_plugin_registry_name ON plugin_registry(name); diff --git a/migrations/sqlite/V7__user_management.sql b/migrations/sqlite/V7__user_management.sql index 8042e10..6584f03 100644 --- a/migrations/sqlite/V7__user_management.sql +++ b/migrations/sqlite/V7__user_management.sql @@ -1,37 +1,35 @@ -- Users table CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - role TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); -- User profiles table CREATE TABLE user_profiles ( - user_id TEXT PRIMARY KEY, - avatar_path TEXT, - bio TEXT, - preferences_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) + user_id TEXT PRIMARY KEY, + avatar_path TEXT, + bio TEXT, + preferences_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ); -- User library access table CREATE TABLE user_libraries ( - user_id TEXT NOT NULL, - root_path TEXT NOT NULL, - permission TEXT NOT NULL, - granted_at TEXT NOT NULL, - PRIMARY KEY (user_id, root_path), - FOREIGN KEY (user_id) REFERENCES users (id) + user_id TEXT NOT NULL, + root_path TEXT NOT NULL, + permission TEXT NOT NULL, + granted_at TEXT NOT NULL, + PRIMARY KEY (user_id, root_path), + FOREIGN KEY (user_id) REFERENCES users(id) ); -- Indexes for efficient lookups -CREATE INDEX idx_users_username ON users (username); - -CREATE INDEX idx_user_libraries_user_id ON user_libraries (user_id); - -CREATE INDEX idx_user_libraries_root_path ON user_libraries (root_path); +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_user_libraries_user_id ON user_libraries(user_id); +CREATE INDEX idx_user_libraries_root_path ON user_libraries(root_path); diff --git a/migrations/sqlite/V8__media_server_features.sql b/migrations/sqlite/V8__media_server_features.sql index ee3dc04..50040c3 100644 --- a/migrations/sqlite/V8__media_server_features.sql +++ b/migrations/sqlite/V8__media_server_features.sql @@ -1,148 +1,143 @@ -- Ratings CREATE TABLE IF NOT EXISTS ratings ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - stars INTEGER NOT NULL CHECK ( - stars >= 1 - AND stars <= 5 - ), - review_text TEXT, - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - UNIQUE (user_id, media_id), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + stars INTEGER NOT NULL CHECK (stars >= 1 AND stars <= 5), + review_text TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -- Comments CREATE TABLE IF NOT EXISTS comments ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - parent_comment_id TEXT, - text TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE, - FOREIGN KEY (parent_comment_id) REFERENCES comments (id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + parent_comment_id TEXT, + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE, + FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE ); -- Favorites CREATE TABLE IF NOT EXISTS favorites ( - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - PRIMARY KEY (user_id, media_id), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -- Share links CREATE TABLE IF NOT EXISTS share_links ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - created_by TEXT NOT NULL, - token TEXT NOT NULL UNIQUE, - password_hash TEXT, - expires_at TEXT, - view_count INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + created_by TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + password_hash TEXT, + expires_at TEXT, + view_count INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -- Playlists CREATE TABLE IF NOT EXISTS playlists ( - id TEXT PRIMARY KEY, - owner_id TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - is_public INTEGER NOT NULL DEFAULT 0, - is_smart INTEGER NOT NULL DEFAULT 0, - filter_query TEXT, - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - updated_at TEXT NOT NULL DEFAULT (datetime ('now')) + id TEXT PRIMARY KEY, + owner_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + is_public INTEGER NOT NULL DEFAULT 0, + is_smart INTEGER NOT NULL DEFAULT 0, + filter_query TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- Playlist items CREATE TABLE IF NOT EXISTS playlist_items ( - playlist_id TEXT NOT NULL, - media_id TEXT NOT NULL, - position INTEGER NOT NULL DEFAULT 0, - added_at TEXT NOT NULL DEFAULT (datetime ('now')), - PRIMARY KEY (playlist_id, media_id), - FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE, - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + playlist_id TEXT NOT NULL, + media_id TEXT NOT NULL, + position INTEGER NOT NULL DEFAULT 0, + added_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (playlist_id, media_id), + FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -- Usage events CREATE TABLE IF NOT EXISTS usage_events ( - id TEXT PRIMARY KEY, - media_id TEXT, - user_id TEXT, - event_type TEXT NOT NULL, - timestamp TEXT NOT NULL DEFAULT (datetime ('now')), - duration_secs REAL, - context_json TEXT, - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL + id TEXT PRIMARY KEY, + media_id TEXT, + user_id TEXT, + event_type TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT (datetime('now')), + duration_secs REAL, + context_json TEXT, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE SET NULL ); -CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events (media_id); - -CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events (user_id); - -CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events (timestamp); +CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events(media_id); +CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events(user_id); +CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events(timestamp); -- Watch history / progress CREATE TABLE IF NOT EXISTS watch_history ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - progress_secs REAL NOT NULL DEFAULT 0, - last_watched TEXT NOT NULL DEFAULT (datetime ('now')), - UNIQUE (user_id, media_id), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + progress_secs REAL NOT NULL DEFAULT 0, + last_watched TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -- Subtitles CREATE TABLE IF NOT EXISTS subtitles ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - language TEXT, - format TEXT NOT NULL, - file_path TEXT, - is_embedded INTEGER NOT NULL DEFAULT 0, - track_index INTEGER, - offset_ms INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + language TEXT, + format TEXT NOT NULL, + file_path TEXT, + is_embedded INTEGER NOT NULL DEFAULT 0, + track_index INTEGER, + offset_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles (media_id); +CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles(media_id); -- External metadata (enrichment) CREATE TABLE IF NOT EXISTS external_metadata ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - source TEXT NOT NULL, - external_id TEXT, - metadata_json TEXT NOT NULL DEFAULT '{}', - confidence REAL NOT NULL DEFAULT 0.0, - last_updated TEXT NOT NULL DEFAULT (datetime ('now')), - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + source TEXT NOT NULL, + external_id TEXT, + metadata_json TEXT NOT NULL DEFAULT '{}', + confidence REAL NOT NULL DEFAULT 0.0, + last_updated TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata (media_id); +CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata(media_id); -- Transcode sessions CREATE TABLE IF NOT EXISTS transcode_sessions ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - user_id TEXT, - profile TEXT NOT NULL, - cache_path TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - progress REAL NOT NULL DEFAULT 0.0, - error_message TEXT, - created_at TEXT NOT NULL DEFAULT (datetime ('now')), - expires_at TEXT, - FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + user_id TEXT, + profile TEXT NOT NULL, + cache_path TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + progress REAL NOT NULL DEFAULT 0.0, + error_message TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions (media_id); +CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions(media_id); diff --git a/migrations/sqlite/V9__fix_indexes_and_constraints.sql b/migrations/sqlite/V9__fix_indexes_and_constraints.sql index dd1bfa0..432f35a 100644 --- a/migrations/sqlite/V9__fix_indexes_and_constraints.sql +++ b/migrations/sqlite/V9__fix_indexes_and_constraints.sql @@ -1,25 +1,18 @@ -- Drop redundant indexes (already covered by UNIQUE constraints) DROP INDEX IF EXISTS idx_users_username; - DROP INDEX IF EXISTS idx_user_libraries_user_id; -- Add missing indexes for comments table -CREATE INDEX IF NOT EXISTS idx_comments_media ON comments (media_id); - -CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments (parent_comment_id); +CREATE INDEX IF NOT EXISTS idx_comments_media ON comments(media_id); +CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_comment_id); -- Remove duplicates before adding unique index (keep the first row) DELETE FROM external_metadata -WHERE - rowid NOT IN ( - SELECT - MIN(rowid) - FROM - external_metadata - GROUP BY - media_id, - source - ); +WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM external_metadata + GROUP BY media_id, source +); -- Add unique index for external_metadata to prevent duplicates -CREATE UNIQUE INDEX IF NOT EXISTS uq_external_metadata_source ON external_metadata (media_id, source); +CREATE UNIQUE INDEX IF NOT EXISTS uq_external_metadata_source ON external_metadata(media_id, source); diff --git a/nix/shell.nix b/nix/shell.nix index a2a53c9..dcff21d 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -26,22 +26,21 @@ in name = "pinakes-dev"; packages = [ + # Build tools # We use the rust-overlay to get the stable Rust toolchain for various targets. # This is not exactly necessary, but it allows for compiling for various targets - # with the least amount of friction. The extensions are to make sure all tooling - # uses the same Rust version and the general surrounding tooling. + # with the least amount of friction. (rust-bin.nightly.latest.default.override { extensions = ["rustfmt" "rust-src" "rust-analyzer" "clippy" "rust-analyzer"]; targets = ["wasm32-unknown-unknown" "wasm32-wasip1"]; # web + plugins }) - # Modern, LLVM based linking pipeline. Kind of sucks on Windows, though. + # Modern, LLVM based linking pipeline llvmPackages.lld llvmPackages.clang - # CLI helpers - pkgs.dioxus-cli # for packaging Dioxus apps and such - pkgs.just # general command runner for everything + # Handy CLI for packaging Dioxus apps and such + pkgs.dioxus-cli # Additional Cargo Tooling pkgs.cargo-nextest @@ -50,9 +49,29 @@ in # Other tools pkgs.taplo # TOML formatter pkgs.lldb # debugger + pkgs.sccache # distributed Rust cache ] ++ runtimeDeps; + # We could do this in the NixOS configuration via ~/.config/cargo.toml + # or something, but this is better because it lets everyone benefit + # from sccache while working with Pinakes. The reason those variables + # are not in 'env' are that we have to use Bash, which doesn't mix well + # with Nix strings. + shellHook = '' + if [ -n "$SCCACHE_BIN" ]; then + export RUSTC_WRAPPER="$SCCACHE_BIN" + export SCCACHE_DIR="''${HOME}/.cache/sccache" + export SCCACHE_CACHE_SIZE="50G" + mkdir -p "$SCCACHE_DIR" + + echo "sccache setup complete!" + fi + + # Start the daemon early for slightly faster startup. + "$SCCACHE_BIN" --start-server >/dev/null 2>&1 || true + ''; + env = { # Allow Cargo to use lld and clang properly LIBCLANG_PATH = "${llvmPackages.libclang.lib}/lib"; @@ -65,5 +84,8 @@ in # Runtime library path for GTK/WebKit/xdotool LD_LIBRARY_PATH = "${lib.makeLibraryPath runtimeDeps}"; + + # Enable sccache for local nix develop shell + SCCACHE_BIN = "${pkgs.sccache}/bin/sccache"; }; } diff --git a/packages/pinakes-ui/assets/css/main.css b/packages/pinakes-ui/assets/css/main.css deleted file mode 100644 index 149dd65..0000000 --- a/packages/pinakes-ui/assets/css/main.css +++ /dev/null @@ -1,4627 +0,0 @@ -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} -* { - margin: 0; - padding: 0; - box-sizing: border-box; - scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.06) rgba(0, 0, 0, 0); -} -*::-webkit-scrollbar { - width: 5px; - height: 5px; -} -*::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0); -} -*::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.06); - border-radius: 3px; -} -*::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.14); -} -:root { - --bg-0: #111118; - --bg-1: #18181f; - --bg-2: #1f1f28; - --bg-3: #26263a; - --border-subtle: rgba(255, 255, 255, 0.06); - --border: rgba(255, 255, 255, 0.09); - --border-strong: rgba(255, 255, 255, 0.14); - --text-0: #dcdce4; - --text-1: #a0a0b8; - --text-2: #6c6c84; - --accent: #7c7ef5; - --accent-dim: rgba(124, 126, 245, 0.15); - --accent-text: #9698f7; - --success: #3ec97a; - --error: #e45858; - --warning: #d4a037; - --radius-sm: 3px; - --radius: 5px; - --radius-md: 7px; - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow: 0 2px 8px rgba(0, 0, 0, 0.35); - --shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.45); -} -body { - font-family: - "Inter", - -apple-system, - "Segoe UI", - system-ui, - sans-serif; - background: var(--bg-0); - color: var(--text-0); - font-size: 13px; - line-height: 1.5; - -webkit-font-smoothing: antialiased; - overflow: hidden; -} -:focus-visible:focus-visible { - outline: 2px solid #7c7ef5; - outline-offset: 2px; -} -::selection { - background: rgba(124, 126, 245, 0.15); - color: #9698f7; -} -a { - color: #9698f7; - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -code { - padding: 1px 5px; - border-radius: 3px; - background: #111118; - color: #9698f7; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 11px; -} -ul { - list-style: none; - padding: 0; -} -ul li { - padding: 3px 0; - font-size: 12px; - color: #a0a0b8; -} -.text-muted { - color: #a0a0b8; -} -.text-sm { - font-size: 11px; -} -.mono { - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 12px; -} -.flex-row { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; -} -.flex-between { - display: flex; - justify-content: space-between; - align-items: center; -} -.mb-16 { - margin-bottom: 16px; -} -.mb-8 { - margin-bottom: 12px; -} -.mt-16 { - margin-top: 16px; -} -.mt-8 { - margin-top: 12px; -} -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} -@keyframes slide-up { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -@keyframes pulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.3; - } -} -@keyframes spin { - to { - transform: rotate(360deg); - } -} -@keyframes skeleton-pulse { - 0% { - opacity: 0.6; - } - 50% { - opacity: 0.3; - } - 100% { - opacity: 0.6; - } -} -@keyframes indeterminate { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(400%); - } -} -.app { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: stretch; - height: 100vh; - overflow: hidden; -} -.sidebar { - width: 220px; - min-width: 220px; - max-width: 220px; - background: #18181f; - border-right: 1px solid rgba(255, 255, 255, 0.09); - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - flex-shrink: 0; - user-select: none; - overflow-y: auto; - overflow-x: hidden; - z-index: 10; - transition: - width 0.15s, - min-width 0.15s, - max-width 0.15s; -} -.sidebar.collapsed { - width: 48px; - min-width: 48px; - max-width: 48px; -} -.sidebar.collapsed .nav-label, -.sidebar.collapsed .sidebar-header .logo, -.sidebar.collapsed .sidebar-header .version, -.sidebar.collapsed .nav-badge, -.sidebar.collapsed .nav-item-text, -.sidebar.collapsed .sidebar-footer .status-text, -.sidebar.collapsed .user-name, -.sidebar.collapsed .role-badge, -.sidebar.collapsed .user-info .btn, -.sidebar.collapsed .sidebar-import-header span, -.sidebar.collapsed .sidebar-import-file { - display: none; -} -.sidebar.collapsed .nav-item { - justify-content: center; - padding: 8px; - border-left: none; - border-radius: 3px; -} -.sidebar.collapsed .nav-item.active { - border-left: none; -} -.sidebar.collapsed .nav-icon { - width: auto; - margin: 0; -} -.sidebar.collapsed .sidebar-header { - padding: 12px 8px; - justify-content: center; -} -.sidebar.collapsed .nav-section { - padding: 0 4px; -} -.sidebar.collapsed .sidebar-footer { - padding: 8px; -} -.sidebar.collapsed .sidebar-footer .user-info { - justify-content: center; - padding: 4px; -} -.sidebar.collapsed .sidebar-import-progress { - padding: 6px; -} -.sidebar-header { - padding: 16px 16px 20px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: baseline; - gap: 8px; -} -.sidebar-header .logo { - font-size: 15px; - font-weight: 700; - letter-spacing: -0.4px; - color: #dcdce4; -} -.sidebar-header .version { - font-size: 10px; - color: #6c6c84; -} -.sidebar-toggle { - background: rgba(0, 0, 0, 0); - border: none; - color: #6c6c84; - padding: 8px; - font-size: 18px; - width: 100%; - text-align: center; -} -.sidebar-toggle:hover { - color: #dcdce4; -} -.sidebar-spacer { - flex: 1; -} -.sidebar-footer { - padding: 12px; - border-top: 1px solid rgba(255, 255, 255, 0.06); - overflow: visible; - min-width: 0; -} -.nav-section { - padding: 0 8px; - margin-bottom: 2px; -} -.nav-label { - padding: 8px 8px 4px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - color: #6c6c84; -} -.nav-item { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - padding: 6px 8px; - border-radius: 3px; - cursor: pointer; - color: #a0a0b8; - font-size: 13px; - font-weight: 450; - transition: - color 0.1s, - background 0.1s; - border: none; - background: none; - width: 100%; - text-align: left; - border-left: 2px solid rgba(0, 0, 0, 0); - margin-left: 0; -} -.nav-item:hover { - color: #dcdce4; - background: rgba(255, 255, 255, 0.03); -} -.nav-item.active { - color: #9698f7; - border-left-color: #7c7ef5; - background: rgba(124, 126, 245, 0.15); -} -.nav-item-text { - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; -} -.sidebar:not(.collapsed) .nav-item-text { - overflow: visible; -} -.nav-icon { - width: 18px; - text-align: center; - font-size: 14px; - opacity: 0.7; -} -.nav-badge { - margin-left: auto; - font-size: 10px; - font-weight: 600; - color: #6c6c84; - background: #26263a; - padding: 1px 6px; - border-radius: 12px; - min-width: 20px; - text-align: center; - font-variant-numeric: tabular-nums; -} -.status-indicator { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: 6px; - font-size: 11px; - font-weight: 500; - min-width: 0; - overflow: visible; -} -.sidebar:not(.collapsed) .status-indicator { - justify-content: flex-start; -} -.status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; -} -.status-dot.connected { - background: #3ec97a; -} -.status-dot.disconnected { - background: #e45858; -} -.status-dot.checking { - background: #d4a037; - animation: pulse 1.5s infinite; -} -.status-text { - color: #6c6c84; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; -} -.sidebar:not(.collapsed) .status-text { - overflow: visible; -} -.main { - flex: 1; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - overflow: hidden; - min-width: 0; -} -.header { - height: 48px; - min-height: 48px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 12px; - padding: 0 20px; - background: #18181f; -} -.page-title { - font-size: 14px; - font-weight: 600; - color: #dcdce4; -} -.header-spacer { - flex: 1; -} -.content { - flex: 1; - overflow-y: auto; - padding: 20px; - scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.06) rgba(0, 0, 0, 0); -} -.sidebar-import-progress { - padding: 10px 12px; - background: #1f1f28; - border-top: 1px solid rgba(255, 255, 255, 0.06); - font-size: 11px; -} -.sidebar-import-header { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - margin-bottom: 4px; - color: #a0a0b8; -} -.sidebar-import-file { - color: #6c6c84; - font-size: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-bottom: 4px; -} -.sidebar-import-progress .progress-bar { - height: 3px; -} -.user-info { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - font-size: 12px; - overflow: hidden; - min-width: 0; -} -.user-name { - font-weight: 500; - color: #dcdce4; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 90px; - flex-shrink: 1; -} -.role-badge { - display: inline-block; - padding: 1px 6px; - border-radius: 3px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; -} -.role-badge.role-admin { - background: rgba(139, 92, 246, 0.1); - color: #9d8be0; -} -.role-badge.role-editor { - background: rgba(34, 160, 80, 0.1); - color: #5cb97a; -} -.role-badge.role-viewer { - background: rgba(59, 120, 200, 0.1); - color: #6ca0d4; -} -.btn { - padding: 5px 12px; - border-radius: 3px; - border: none; - cursor: pointer; - font-size: 12px; - font-weight: 500; - transition: all 0.1s; - display: inline-flex; - align-items: center; - gap: 5px; - white-space: nowrap; - line-height: 1.5; -} -.btn-primary { - background: #7c7ef5; - color: #fff; -} -.btn-primary:hover { - background: #8b8df7; -} -.btn-secondary { - background: #26263a; - color: #dcdce4; - border: 1px solid rgba(255, 255, 255, 0.09); -} -.btn-secondary:hover { - border-color: rgba(255, 255, 255, 0.14); - background: rgba(255, 255, 255, 0.06); -} -.btn-danger { - background: rgba(0, 0, 0, 0); - color: #e45858; - border: 1px solid rgba(228, 88, 88, 0.25); -} -.btn-danger:hover { - background: rgba(228, 88, 88, 0.08); -} -.btn-ghost { - background: rgba(0, 0, 0, 0); - border: none; - color: #a0a0b8; - padding: 5px 8px; -} -.btn-ghost:hover { - color: #dcdce4; - background: rgba(255, 255, 255, 0.04); -} -.btn-sm { - padding: 3px 8px; - font-size: 11px; -} -.btn-icon { - padding: 4px; - border-radius: 3px; - background: rgba(0, 0, 0, 0); - border: none; - color: #6c6c84; - cursor: pointer; - transition: color 0.1s; - font-size: 13px; -} -.btn-icon:hover { - color: #dcdce4; -} -.btn:disabled, -.btn[disabled] { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; -} -.btn.btn-disabled-hint:disabled { - opacity: 0.6; - border-style: dashed; - pointer-events: auto; - cursor: help; -} -.card { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - padding: 16px; -} -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} -.card-title { - font-size: 14px; - font-weight: 600; -} -.card-body { - padding-top: 8px; -} -.data-table { - width: 100%; - border-collapse: collapse; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - overflow: hidden; -} -.data-table thead th { - padding: 8px 14px; - text-align: left; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - color: #6c6c84; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); - background: #26263a; -} -.data-table tbody td { - padding: 8px 14px; - font-size: 13px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - max-width: 300px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.data-table tbody tr { - cursor: pointer; - transition: background 0.08s; -} -.data-table tbody tr:hover { - background: rgba(255, 255, 255, 0.02); -} -.data-table tbody tr.row-selected { - background: rgba(99, 102, 241, 0.12); -} -.data-table tbody tr:last-child td { - border-bottom: none; -} -.sortable-header { - cursor: pointer; - user-select: none; - transition: color 0.1s; -} -.sortable-header:hover { - color: #9698f7; -} -input[type="text"], -textarea, -select { - padding: 6px 10px; - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.09); - background: #111118; - color: #dcdce4; - font-size: 13px; - outline: none; - transition: border-color 0.15s; - font-family: inherit; -} -input[type="text"]::placeholder, -textarea::placeholder, -select::placeholder { - color: #6c6c84; -} -input[type="text"]:focus, -textarea:focus, -select:focus { - border-color: #7c7ef5; -} -input[type="text"][type="number"], -textarea[type="number"], -select[type="number"] { - width: 80px; - padding: 6px 8px; - -moz-appearance: textfield; -} -input[type="text"][type="number"]::-webkit-outer-spin-button, -input[type="text"][type="number"]::-webkit-inner-spin-button, -textarea[type="number"]::-webkit-outer-spin-button, -textarea[type="number"]::-webkit-inner-spin-button, -select[type="number"]::-webkit-outer-spin-button, -select[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} -textarea { - min-height: 64px; - resize: vertical; -} -select { - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%236c6c84' d='M5 7L1 3h8z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; - padding-right: 26px; - min-width: 100px; -} -.form-group { - margin-bottom: 12px; -} -.form-label { - display: block; - font-size: 11px; - font-weight: 600; - color: #a0a0b8; - margin-bottom: 4px; - text-transform: uppercase; - letter-spacing: 0.03em; -} -.form-row { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: flex-end; - gap: 8px; -} -.form-row input[type="text"] { - flex: 1; -} -.form-label-row { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 2px; - margin-bottom: 4px; -} -.form-label-row .form-label { - margin-bottom: 0; -} -.badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 50%; - font-size: 9px; - font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; -} -.badge-neutral { - background: rgba(255, 255, 255, 0.04); - color: #a0a0b8; -} -.badge-success { - background: rgba(62, 201, 122, 0.08); - color: #3ec97a; -} -.badge-warning { - background: rgba(212, 160, 55, 0.06); - color: #d4a037; -} -.badge-danger { - background: rgba(228, 88, 88, 0.06); - color: #d47070; -} -.tab-bar { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 4px; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); - margin-bottom: 12px; -} -.tab-btn { - background: rgba(0, 0, 0, 0); - border: none; - padding: 6px 8px; - font-size: 13px; - color: #a0a0b8; - border-bottom: 2px solid rgba(0, 0, 0, 0); - margin-bottom: -1px; - cursor: pointer; - transition: - color 0.1s, - border-color 0.1s; -} -.tab-btn:hover { - color: #dcdce4; -} -.tab-btn.active { - color: #9698f7; - border-bottom-color: #7c7ef5; -} -.input-sm { - padding: 6px 10px; - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.09); - background: #111118; - color: #dcdce4; - font-size: 13px; - outline: none; - transition: border-color 0.15s; - font-family: inherit; - padding: 3px 8px; - font-size: 12px; -} -.input-sm::placeholder { - color: #6c6c84; -} -.input-sm:focus { - border-color: #7c7ef5; -} -.input-suffix { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: stretch; -} -.input-suffix input { - border-radius: 3px 0 0 3px; - border-right: none; - flex: 1; -} -.input-suffix .btn { - border-radius: 0 3px 3px 0; -} -.field-error { - color: #d47070; - font-size: 11px; - margin-top: 2px; -} -.comments-list { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 8px; -} -.comment-item { - padding: 8px; - background: #18181f; - border-radius: 5px; - border: 1px solid rgba(255, 255, 255, 0.06); -} -.comment-text { - font-size: 13px; - color: #dcdce4; - line-height: 1.5; - margin-top: 4px; -} -input[type="checkbox"] { - appearance: none; - -webkit-appearance: none; - width: 16px; - height: 16px; - border: 1px solid rgba(255, 255, 255, 0.14); - border-radius: 3px; - background: #1f1f28; - cursor: pointer; - position: relative; - flex-shrink: 0; - transition: all 0.15s ease; -} -input[type="checkbox"]:hover { - border-color: #7c7ef5; - background: #26263a; -} -input[type="checkbox"]:checked { - background: #7c7ef5; - border-color: #7c7ef5; -} -input[type="checkbox"]:checked::after { - content: ""; - position: absolute; - left: 5px; - top: 2px; - width: 4px; - height: 8px; - border: solid #111118; - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} -input[type="checkbox"]:focus-visible:focus-visible { - outline: 2px solid #7c7ef5; - outline-offset: 2px; -} -.checkbox-label { - display: inline-flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 13px; - color: #a0a0b8; - user-select: none; -} -.checkbox-label:hover { - color: #dcdce4; -} -.checkbox-label input[type="checkbox"] { - margin: 0; -} -.toggle { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 13px; - color: #dcdce4; -} -.toggle.disabled { - opacity: 0.4; - cursor: not-allowed; -} -.toggle-track { - width: 32px; - height: 18px; - border-radius: 9px; - background: #26263a; - border: 1px solid rgba(255, 255, 255, 0.09); - position: relative; - transition: background 0.15s; - flex-shrink: 0; -} -.toggle-track.active { - background: #7c7ef5; - border-color: #7c7ef5; -} -.toggle-track.active .toggle-thumb { - transform: translateX(14px); -} -.toggle-thumb { - width: 14px; - height: 14px; - border-radius: 50%; - background: #dcdce4; - position: absolute; - top: 1px; - left: 1px; - transition: transform 0.15s; -} -.filter-bar { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 12px; - padding: 12px; - background: #111118; - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 5px; - margin-bottom: 12px; -} -.filter-row { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} -.filter-label { - font-size: 11px; - font-weight: 500; - color: #6c6c84; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-right: 4px; -} -.filter-chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 5px 10px; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 14px; - cursor: pointer; - font-size: 11px; - color: #a0a0b8; - transition: all 0.15s ease; - user-select: none; -} -.filter-chip:hover { - background: #26263a; - border-color: rgba(255, 255, 255, 0.14); - color: #dcdce4; -} -.filter-chip.active { - background: rgba(124, 126, 245, 0.15); - border-color: #7c7ef5; - color: #9698f7; -} -.filter-chip input[type="checkbox"] { - width: 12px; - height: 12px; - margin: 0; -} -.filter-chip input[type="checkbox"]:checked::after { - left: 3px; - top: 1px; - width: 3px; - height: 6px; -} -.filter-group { - display: flex; - align-items: center; - gap: 6px; -} -.filter-group label { - display: flex; - align-items: center; - gap: 3px; - cursor: pointer; - color: #a0a0b8; - font-size: 11px; - white-space: nowrap; -} -.filter-group label:hover { - color: #dcdce4; -} -.filter-separator { - width: 1px; - height: 20px; - background: rgba(255, 255, 255, 0.09); - flex-shrink: 0; -} -.view-toggle { - display: flex; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - overflow: hidden; -} -.view-btn { - padding: 4px 10px; - background: #1f1f28; - border: none; - color: #6c6c84; - cursor: pointer; - font-size: 18px; - line-height: 1; - transition: - background 0.1s, - color 0.1s; -} -.view-btn:first-child { - border-right: 1px solid rgba(255, 255, 255, 0.09); -} -.view-btn:hover { - color: #dcdce4; - background: #26263a; -} -.view-btn.active { - background: rgba(124, 126, 245, 0.15); - color: #9698f7; -} -.breadcrumb { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 4px; - padding: 10px 16px; - font-size: 0.85rem; - color: #6c6c84; -} -.breadcrumb-sep { - color: #6c6c84; - opacity: 0.5; -} -.breadcrumb-link { - color: #9698f7; - text-decoration: none; - cursor: pointer; -} -.breadcrumb-link:hover { - text-decoration: underline; -} -.breadcrumb-current { - color: #dcdce4; - font-weight: 500; -} -.progress-bar { - width: 100%; - height: 8px; - background: #26263a; - border-radius: 4px; - overflow: hidden; - margin-bottom: 6px; -} -.progress-fill { - height: 100%; - background: #7c7ef5; - border-radius: 4px; - transition: width 0.3s ease; -} -.progress-fill.indeterminate { - width: 30%; - animation: indeterminate 1.5s ease-in-out infinite; -} -.loading-overlay { - display: flex; - align-items: center; - justify-content: center; - padding: 48px 16px; - color: #6c6c84; - font-size: 13px; - gap: 10px; -} -.spinner { - width: 18px; - height: 18px; - border: 2px solid rgba(255, 255, 255, 0.09); - border-top-color: #7c7ef5; - border-radius: 50%; - animation: spin 0.7s linear infinite; -} -.spinner-small { - width: 14px; - height: 14px; - border: 2px solid rgba(255, 255, 255, 0.09); - border-top-color: #7c7ef5; - border-radius: 50%; - animation: spin 0.7s linear infinite; -} -.spinner-tiny { - width: 10px; - height: 10px; - border: 1.5px solid rgba(255, 255, 255, 0.09); - border-top-color: #7c7ef5; - border-radius: 50%; - animation: spin 0.7s linear infinite; -} -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 100; - animation: fade-in 0.1s ease-out; -} -.modal { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 7px; - padding: 20px; - min-width: 360px; - max-width: 480px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); -} -.modal.wide { - max-width: 600px; - max-height: 70vh; - overflow-y: auto; -} -.modal-title { - font-size: 15px; - font-weight: 600; - margin-bottom: 6px; -} -.modal-body { - font-size: 12px; - color: #a0a0b8; - margin-bottom: 16px; - line-height: 1.5; -} -.modal-actions { - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - gap: 6px; -} -.tooltip-trigger { - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - border-radius: 50%; - background: #26263a; - color: #6c6c84; - font-size: 9px; - font-weight: 700; - cursor: help; - position: relative; - flex-shrink: 0; - margin-left: 4px; -} -.tooltip-trigger:hover { - background: rgba(124, 126, 245, 0.15); - color: #9698f7; -} -.tooltip-trigger:hover .tooltip-text { - display: block; -} -.tooltip-text { - display: none; - position: absolute; - bottom: calc(100% + 6px); - left: 50%; - transform: translateX(-50%); - padding: 6px 10px; - background: #26263a; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - color: #dcdce4; - font-size: 11px; - font-weight: 400; - line-height: 1.4; - white-space: normal; - width: 220px; - text-transform: none; - letter-spacing: normal; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); - z-index: 100; - pointer-events: none; -} -.media-player { - position: relative; - background: #111118; - border-radius: 5px; - overflow: hidden; -} -.media-player:focus { - outline: none; -} -.media-player-audio .player-artwork { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 8px; - padding: 24px 16px 8px; -} -.player-native-video { - width: 100%; - display: block; -} -.player-native-audio { - display: none; -} -.player-artwork img { - max-width: 200px; - max-height: 200px; - border-radius: 5px; - object-fit: cover; -} -.player-artwork-placeholder { - width: 120px; - height: 120px; - display: flex; - align-items: center; - justify-content: center; - background: #1f1f28; - border-radius: 5px; - font-size: 48px; - opacity: 0.3; -} -.player-title { - font-size: 13px; - font-weight: 500; - color: #dcdce4; - text-align: center; -} -.player-controls { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - padding: 10px 14px; - background: #1f1f28; -} -.media-player-video .player-controls { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: rgba(0, 0, 0, 0.7); - opacity: 0; - transition: opacity 0.2s; -} -.media-player-video:hover .player-controls { - opacity: 1; -} -.play-btn, -.mute-btn, -.fullscreen-btn { - background: none; - border: none; - color: #dcdce4; - cursor: pointer; - font-size: 18px; - padding: 4px; - line-height: 1; - transition: color 0.1s; -} -.play-btn:hover, -.mute-btn:hover, -.fullscreen-btn:hover { - color: #9698f7; -} -.player-time { - font-size: 11px; - color: #6c6c84; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - min-width: 36px; - text-align: center; - user-select: none; -} -.seek-bar { - flex: 1; - -webkit-appearance: none; - appearance: none; - height: 4px; - border-radius: 4px; - background: #26263a; - outline: none; - cursor: pointer; -} -.seek-bar::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 12px; - height: 12px; - border-radius: 50%; - background: #7c7ef5; - cursor: pointer; - border: none; -} -.seek-bar::-moz-range-thumb { - width: 12px; - height: 12px; - border-radius: 50%; - background: #7c7ef5; - cursor: pointer; - border: none; -} -.volume-slider { - width: 70px; - -webkit-appearance: none; - appearance: none; - height: 4px; - border-radius: 4px; - background: #26263a; - outline: none; - cursor: pointer; -} -.volume-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 10px; - height: 10px; - border-radius: 50%; - background: #a0a0b8; - cursor: pointer; - border: none; -} -.volume-slider::-moz-range-thumb { - width: 10px; - height: 10px; - border-radius: 50%; - background: #a0a0b8; - cursor: pointer; - border: none; -} -.image-viewer-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.92); - z-index: 150; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - animation: fade-in 0.15s ease-out; -} -.image-viewer-overlay:focus { - outline: none; -} -.image-viewer-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 16px; - background: rgba(0, 0, 0, 0.5); - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - z-index: 2; - user-select: none; -} -.image-viewer-toolbar-left, -.image-viewer-toolbar-center, -.image-viewer-toolbar-right { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; -} -.iv-btn { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.1); - color: #dcdce4; - border-radius: 3px; - padding: 4px 10px; - font-size: 12px; - cursor: pointer; - transition: background 0.1s; -} -.iv-btn:hover { - background: rgba(255, 255, 255, 0.12); -} -.iv-btn.iv-close { - color: #e45858; - font-weight: 600; -} -.iv-zoom-label { - font-size: 11px; - color: #a0a0b8; - min-width: 40px; - text-align: center; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; -} -.image-viewer-canvas { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - position: relative; -} -.image-viewer-canvas img { - max-width: 100%; - max-height: 100%; - object-fit: contain; - user-select: none; - -webkit-user-drag: none; -} -.pdf-viewer { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - height: 100%; - min-height: 500px; - background: #111118; - border-radius: 5px; - overflow: hidden; -} -.pdf-toolbar { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 12px; - padding: 10px 12px; - background: #18181f; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); -} -.pdf-toolbar-group { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 4px; -} -.pdf-toolbar-btn { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - color: #a0a0b8; - font-size: 14px; - cursor: pointer; - transition: all 0.15s; -} -.pdf-toolbar-btn:hover:not(:disabled) { - background: #26263a; - color: #dcdce4; -} -.pdf-toolbar-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} -.pdf-zoom-label { - min-width: 45px; - text-align: center; - font-size: 12px; - color: #a0a0b8; -} -.pdf-container { - flex: 1; - position: relative; - overflow: hidden; - background: #1f1f28; -} -.pdf-object { - width: 100%; - height: 100%; - border: none; -} -.pdf-loading, -.pdf-error { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 12px; - background: #18181f; - color: #a0a0b8; -} -.pdf-error { - padding: 12px; - text-align: center; -} -.pdf-fallback { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 16px; - padding: 48px 12px; - text-align: center; - color: #6c6c84; -} -.markdown-viewer { - padding: 16px; - text-align: left; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 12px; -} -.markdown-toolbar { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - padding: 8px; - background: #1f1f28; - border-radius: 5px; - border: 1px solid rgba(255, 255, 255, 0.09); -} -.toolbar-btn { - padding: 6px 12px; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - background: #18181f; - color: #a0a0b8; - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: all 0.15s; -} -.toolbar-btn:hover { - background: #26263a; - border-color: rgba(255, 255, 255, 0.14); -} -.toolbar-btn.active { - background: #7c7ef5; - color: #fff; - border-color: #7c7ef5; -} -.markdown-source { - max-width: 100%; - background: #26263a; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - padding: 16px; - overflow-x: auto; - font-family: "Menlo", "Monaco", "Courier New", monospace; - font-size: 13px; - line-height: 1.7; - color: #dcdce4; - white-space: pre-wrap; - word-wrap: break-word; -} -.markdown-source code { - font-family: inherit; - background: none; - padding: 0; - border: none; -} -.markdown-content { - max-width: 800px; - color: #dcdce4; - line-height: 1.7; - font-size: 14px; - text-align: left; -} -.markdown-content h1 { - font-size: 1.8em; - font-weight: 700; - margin: 1em 0 0.5em; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - padding-bottom: 0.3em; -} -.markdown-content h2 { - font-size: 1.5em; - font-weight: 600; - margin: 0.8em 0 0.4em; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - padding-bottom: 0.2em; -} -.markdown-content h3 { - font-size: 1.25em; - font-weight: 600; - margin: 0.6em 0 0.3em; -} -.markdown-content h4 { - font-size: 1.1em; - font-weight: 600; - margin: 0.5em 0 0.25em; -} -.markdown-content h5, -.markdown-content h6 { - font-size: 1em; - font-weight: 600; - margin: 0.4em 0 0.2em; - color: #a0a0b8; -} -.markdown-content p { - margin: 0 0 1em; -} -.markdown-content a { - color: #7c7ef5; - text-decoration: none; -} -.markdown-content a:hover { - text-decoration: underline; -} -.markdown-content pre { - background: #26263a; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - padding: 12px 16px; - overflow-x: auto; - margin: 0 0 1em; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 12px; - line-height: 1.5; -} -.markdown-content code { - background: #26263a; - padding: 1px 5px; - border-radius: 3px; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 0.9em; -} -.markdown-content pre code { - background: none; - padding: 0; -} -.markdown-content blockquote { - border-left: 3px solid #7c7ef5; - padding: 4px 16px; - margin: 0 0 1em; - color: #a0a0b8; - background: rgba(124, 126, 245, 0.04); -} -.markdown-content table { - width: 100%; - border-collapse: collapse; - margin: 0 0 1em; -} -.markdown-content th, -.markdown-content td { - padding: 6px 12px; - border: 1px solid rgba(255, 255, 255, 0.09); - font-size: 13px; -} -.markdown-content th { - background: #26263a; - font-weight: 600; - text-align: left; -} -.markdown-content tr:nth-child(even) { - background: #1f1f28; -} -.markdown-content ul, -.markdown-content ol { - margin: 0 0 1em; - padding-left: 16px; -} -.markdown-content ul { - list-style: disc; -} -.markdown-content ol { - list-style: decimal; -} -.markdown-content li { - padding: 2px 0; - font-size: 14px; - color: #dcdce4; -} -.markdown-content hr { - border: none; - border-top: 1px solid rgba(255, 255, 255, 0.09); - margin: 1.5em 0; -} -.markdown-content img { - max-width: 100%; - border-radius: 5px; -} -.markdown-content .footnote-definition { - font-size: 0.85em; - color: #a0a0b8; - margin-top: 0.5em; - padding-left: 1.5em; -} -.markdown-content .footnote-definition sup { - color: #7c7ef5; - margin-right: 4px; -} -.markdown-content sup a { - color: #7c7ef5; - text-decoration: none; - font-size: 0.8em; -} -.wikilink { - color: #9698f7; - text-decoration: none; - border-bottom: 1px dashed #7c7ef5; - cursor: pointer; - transition: - border-color 0.1s, - color 0.1s; -} -.wikilink:hover { - color: #7c7ef5; - border-bottom-style: solid; -} -.wikilink-embed { - display: inline-block; - padding: 2px 8px; - background: rgba(139, 92, 246, 0.08); - border: 1px dashed rgba(139, 92, 246, 0.3); - border-radius: 3px; - color: #9d8be0; - font-size: 12px; - cursor: default; -} -.media-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 12px; -} -.media-card { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - overflow: hidden; - cursor: pointer; - transition: - border-color 0.12s, - box-shadow 0.12s; - position: relative; -} -.media-card:hover { - border-color: rgba(255, 255, 255, 0.14); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); -} -.media-card.selected { - border-color: #7c7ef5; - box-shadow: 0 0 0 1px #7c7ef5; -} -.card-checkbox { - position: absolute; - top: 6px; - left: 6px; - z-index: 2; - opacity: 0; - transition: opacity 0.1s; -} -.card-checkbox input[type="checkbox"] { - width: 16px; - height: 16px; - cursor: pointer; - filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)); -} -.media-card:hover .card-checkbox, -.media-card.selected .card-checkbox { - opacity: 1; -} -.card-thumbnail { - width: 100%; - aspect-ratio: 1; - background: #111118; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - position: relative; -} -.card-thumbnail img, -.card-thumbnail .card-thumb-img { - width: 100%; - height: 100%; - object-fit: cover; - position: absolute; - top: 0; - left: 0; - z-index: 1; -} -.card-type-icon { - font-size: 32px; - opacity: 0.4; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - z-index: 0; -} -.card-info { - padding: 8px 10px; -} -.card-name { - font-size: 12px; - font-weight: 500; - color: #dcdce4; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-bottom: 4px; -} -.card-title, -.card-artist { - font-size: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.3; -} -.card-meta { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - font-size: 10px; -} -.card-size { - color: #6c6c84; - font-size: 10px; -} -.table-thumb-cell { - width: 36px; - padding: 4px 6px !important; - position: relative; -} -.table-thumb { - width: 28px; - height: 28px; - object-fit: cover; - border-radius: 3px; - display: block; -} -.table-thumb-overlay { - position: absolute; - top: 4px; - left: 6px; - z-index: 1; -} -.table-type-icon { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - font-size: 14px; - opacity: 0.5; - border-radius: 3px; - background: #111118; - z-index: 0; -} -.type-badge { - display: inline-block; - padding: 1px 6px; - border-radius: 3px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; -} -.type-badge.type-audio { - background: rgba(139, 92, 246, 0.1); - color: #9d8be0; -} -.type-badge.type-video { - background: rgba(200, 72, 130, 0.1); - color: #d07eaa; -} -.type-badge.type-image { - background: rgba(34, 160, 80, 0.1); - color: #5cb97a; -} -.type-badge.type-document { - background: rgba(59, 120, 200, 0.1); - color: #6ca0d4; -} -.type-badge.type-text { - background: rgba(200, 160, 36, 0.1); - color: #c4a840; -} -.type-badge.type-other { - background: rgba(128, 128, 160, 0.08); - color: #6c6c84; -} -.tag-list { - display: flex; - flex-wrap: wrap; - gap: 4px; -} -.tag-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 10px; - background: rgba(124, 126, 245, 0.15); - color: #9698f7; - border-radius: 12px; - font-size: 11px; - font-weight: 500; -} -.tag-badge.selected { - background: #7c7ef5; - color: #fff; - cursor: pointer; -} -.tag-badge:not(.selected) { - cursor: pointer; -} -.tag-badge .tag-remove { - cursor: pointer; - opacity: 0.4; - font-size: 13px; - line-height: 1; - transition: opacity 0.1s; -} -.tag-badge .tag-remove:hover { - opacity: 1; -} -.tag-group { - margin-bottom: 6px; -} -.tag-children { - margin-left: 16px; - margin-top: 4px; - display: flex; - flex-wrap: wrap; - gap: 4px; -} -.tag-confirm-delete { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 10px; - color: #a0a0b8; -} -.tag-confirm-yes { - cursor: pointer; - color: #e45858; - font-weight: 600; -} -.tag-confirm-yes:hover { - text-decoration: underline; -} -.tag-confirm-no { - cursor: pointer; - color: #6c6c84; - font-weight: 500; -} -.tag-confirm-no:hover { - text-decoration: underline; -} -.detail-actions { - display: flex; - gap: 6px; - margin-bottom: 16px; -} -.detail-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; -} -.detail-field { - padding: 10px 12px; - background: #111118; - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.06); -} -.detail-field.full-width { - grid-column: 1/-1; -} -.detail-field input[type="text"], -.detail-field textarea, -.detail-field select { - width: 100%; - margin-top: 4px; -} -.detail-field textarea { - min-height: 64px; - resize: vertical; -} -.detail-label { - font-size: 10px; - font-weight: 600; - color: #6c6c84; - text-transform: uppercase; - letter-spacing: 0.04em; - margin-bottom: 2px; -} -.detail-value { - font-size: 13px; - color: #dcdce4; - word-break: break-all; -} -.detail-value.mono { - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 11px; - color: #a0a0b8; -} -.detail-preview { - margin-bottom: 16px; - background: #111118; - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 5px; - overflow: hidden; - text-align: center; -} -.detail-preview:has(.markdown-viewer) { - max-height: none; - overflow-y: auto; - text-align: left; -} -.detail-preview:not(:has(.markdown-viewer)) { - max-height: 450px; -} -.detail-preview img { - max-width: 100%; - max-height: 400px; - object-fit: contain; - display: block; - margin: 0 auto; -} -.detail-preview audio { - width: 100%; - padding: 16px; -} -.detail-preview video { - max-width: 100%; - max-height: 400px; - display: block; - margin: 0 auto; -} -.detail-no-preview { - padding: 16px 16px; - text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 10px; -} -.frontmatter-card { - max-width: 800px; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - padding: 12px 16px; - margin-bottom: 16px; -} -.frontmatter-fields { - display: grid; - grid-template-columns: auto 1fr; - gap: 4px 12px; - margin: 0; -} -.frontmatter-fields dt { - font-weight: 600; - font-size: 12px; - color: #a0a0b8; - text-transform: capitalize; -} -.frontmatter-fields dd { - font-size: 13px; - color: #dcdce4; - margin: 0; -} -.empty-state { - text-align: center; - padding: 48px 12px; - color: #6c6c84; -} -.empty-state .empty-icon { - font-size: 32px; - margin-bottom: 12px; - opacity: 0.3; -} -.empty-title { - font-size: 15px; - font-weight: 600; - color: #a0a0b8; - margin-bottom: 4px; -} -.empty-subtitle { - font-size: 12px; - max-width: 320px; - margin: 0 auto; - line-height: 1.5; -} -.toast-container { - position: fixed; - bottom: 16px; - right: 16px; - z-index: 300; - display: flex; - flex-direction: column-reverse; - gap: 6px; - align-items: flex-end; -} -.toast-container .toast { - position: static; - transform: none; -} -.toast { - position: fixed; - bottom: 16px; - right: 16px; - padding: 10px 16px; - border-radius: 5px; - background: #26263a; - border: 1px solid rgba(255, 255, 255, 0.09); - color: #dcdce4; - font-size: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); - z-index: 300; - animation: slide-up 0.15s ease-out; - max-width: 420px; -} -.toast.success { - border-left: 3px solid #3ec97a; -} -.toast.error { - border-left: 3px solid #e45858; -} -.offline-banner, -.error-banner { - background: rgba(228, 88, 88, 0.06); - border: 1px solid rgba(228, 88, 88, 0.2); - border-radius: 3px; - padding: 10px 12px; - margin-bottom: 12px; - font-size: 12px; - color: #d47070; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; -} -.offline-banner .offline-icon, -.offline-banner .error-icon, -.error-banner .offline-icon, -.error-banner .error-icon { - font-size: 14px; - flex-shrink: 0; -} -.error-banner { - padding: 10px 14px; -} -.readonly-banner { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - padding: 10px 12px; - background: rgba(212, 160, 55, 0.06); - border: 1px solid rgba(212, 160, 55, 0.15); - border-radius: 3px; - margin-bottom: 16px; - font-size: 12px; - color: #d4a037; -} -.batch-actions { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - padding: 8px 10px; - background: rgba(124, 126, 245, 0.15); - border: 1px solid rgba(124, 126, 245, 0.2); - border-radius: 3px; - margin-bottom: 12px; - font-size: 12px; - font-weight: 500; - color: #9698f7; -} -.select-all-banner { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: 8px; - padding: 10px 16px; - background: rgba(99, 102, 241, 0.08); - border-radius: 6px; - margin-bottom: 8px; - font-size: 0.85rem; - color: #a0a0b8; -} -.select-all-banner button { - background: none; - border: none; - color: #7c7ef5; - cursor: pointer; - font-weight: 600; - text-decoration: underline; - font-size: 0.85rem; - padding: 0; -} -.select-all-banner button:hover { - color: #dcdce4; -} -.import-status-panel { - background: #1f1f28; - border: 1px solid #7c7ef5; - border-radius: 5px; - padding: 12px 16px; - margin-bottom: 16px; -} -.import-status-header { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - margin-bottom: 8px; - font-size: 13px; - color: #dcdce4; -} -.import-current-file { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 2px; - margin-bottom: 6px; - font-size: 12px; - overflow: hidden; -} -.import-file-label { - color: #6c6c84; - flex-shrink: 0; -} -.import-file-name { - color: #dcdce4; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-family: monospace; - font-size: 11px; -} -.import-queue-indicator { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 2px; - margin-bottom: 8px; - font-size: 11px; -} -.import-queue-badge { - display: flex; - align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - padding: 0 6px; - background: rgba(124, 126, 245, 0.15); - color: #9698f7; - border-radius: 9px; - font-weight: 600; - font-size: 10px; -} -.import-queue-text { - color: #6c6c84; -} -.import-tabs { - display: flex; - gap: 0; - margin-bottom: 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); -} -.import-tab { - padding: 10px 16px; - background: none; - border: none; - border-bottom: 2px solid rgba(0, 0, 0, 0); - color: #6c6c84; - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: - color 0.1s, - border-color 0.1s; -} -.import-tab:hover { - color: #dcdce4; -} -.import-tab.active { - color: #9698f7; - border-bottom-color: #7c7ef5; -} -.queue-panel { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - border-left: 1px solid rgba(255, 255, 255, 0.09); - background: #18181f; - min-width: 280px; - max-width: 320px; -} -.queue-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.queue-header h3 { - margin: 0; - font-size: 0.9rem; - color: #dcdce4; -} -.queue-controls { - display: flex; - gap: 2px; -} -.queue-list { - overflow-y: auto; - flex: 1; -} -.queue-item { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - padding: 8px 16px; - cursor: pointer; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - transition: background 0.15s; -} -.queue-item:hover { - background: #1f1f28; -} -.queue-item:hover .queue-item-remove { - opacity: 1; -} -.queue-item-active { - background: rgba(124, 126, 245, 0.15); - border-left: 3px solid #7c7ef5; -} -.queue-item-info { - flex: 1; - min-width: 0; -} -.queue-item-title { - display: block; - font-size: 0.85rem; - color: #dcdce4; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.queue-item-artist { - display: block; - font-size: 0.75rem; - color: #6c6c84; -} -.queue-item-remove { - opacity: 0; - transition: opacity 0.15s; -} -.queue-empty { - padding: 16px 16px; - text-align: center; - color: #6c6c84; - font-size: 0.85rem; -} -.statistics-page { - padding: 20px; -} -.stats-overview, -.stats-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; - margin-bottom: 24px; -} -@media (max-width: 768px) { - .stats-overview, - .stats-grid { - grid-template-columns: 1fr; - } -} -.stat-card { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - padding: 20px; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 16px; -} -.stat-card.stat-primary { - border-left: 3px solid #7c7ef5; -} -.stat-card.stat-success { - border-left: 3px solid #3ec97a; -} -.stat-card.stat-info { - border-left: 3px solid #6ca0d4; -} -.stat-card.stat-warning { - border-left: 3px solid #d4a037; -} -.stat-card.stat-purple { - border-left: 3px solid #9d8be0; -} -.stat-card.stat-danger { - border-left: 3px solid #e45858; -} -.stat-icon { - flex-shrink: 0; - color: #6c6c84; -} -.stat-content { - flex: 1; -} -.stat-value { - font-size: 28px; - font-weight: 700; - color: #dcdce4; - line-height: 1.2; - font-variant-numeric: tabular-nums; -} -.stat-label { - font-size: 12px; - color: #6c6c84; - margin-top: 4px; - font-weight: 500; -} -.stats-section { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - padding: 16px; - margin-bottom: 20px; -} -.section-title { - font-size: 16px; - font-weight: 600; - color: #dcdce4; - margin-bottom: 20px; -} -.section-title.small { - font-size: 14px; - margin-bottom: 12px; - padding-bottom: 6px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.chart-bars { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 16px; -} -.bar-item { - display: grid; - grid-template-columns: 120px 1fr 80px; - align-items: center; - gap: 16px; -} -.bar-label { - font-size: 13px; - font-weight: 500; - color: #a0a0b8; - text-align: right; -} -.bar-track { - height: 28px; - background: #26263a; - border-radius: 3px; - overflow: hidden; - position: relative; -} -.bar-fill { - height: 100%; - transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); - border-radius: 3px; -} -.bar-fill.bar-primary { - background: linear-gradient(90deg, #7c7ef5 0%, #7c7ef3 100%); -} -.bar-fill.bar-success { - background: linear-gradient(90deg, #3ec97a 0%, #66bb6a 100%); -} -.bar-value { - font-size: 13px; - font-weight: 600; - color: #a0a0b8; - text-align: right; - font-variant-numeric: tabular-nums; -} -.settings-section { - margin-bottom: 16px; -} -.settings-card { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 7px; - padding: 20px; - margin-bottom: 16px; -} -.settings-card.danger-card { - border: 1px solid rgba(228, 88, 88, 0.25); -} -.settings-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.settings-card-title { - font-size: 14px; - font-weight: 600; -} -.settings-card-body { - padding-top: 2px; -} -.settings-field { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.settings-field:last-child { - border-bottom: none; -} -.settings-field select { - min-width: 120px; -} -.config-path { - font-size: 11px; - color: #6c6c84; - margin-bottom: 12px; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - padding: 6px 10px; - background: #111118; - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.06); -} -.config-status { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - padding: 3px 10px; - border-radius: 12px; - font-size: 11px; - font-weight: 600; -} -.config-status.writable { - background: rgba(62, 201, 122, 0.1); - color: #3ec97a; -} -.config-status.readonly { - background: rgba(228, 88, 88, 0.1); - color: #e45858; -} -.root-list { - list-style: none; -} -.root-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - background: #111118; - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 3px; - margin-bottom: 4px; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 12px; - color: #a0a0b8; -} -.info-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - font-size: 13px; -} -.info-row:last-child { - border-bottom: none; -} -.info-label { - color: #a0a0b8; - font-weight: 500; -} -.info-value { - color: #dcdce4; -} -.tasks-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); - gap: 16px; - padding: 12px; -} -.task-card { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - overflow: hidden; - transition: all 0.2s; -} -.task-card:hover { - border-color: rgba(255, 255, 255, 0.14); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); -} -.task-card-enabled { - border-left: 3px solid #3ec97a; -} -.task-card-disabled { - border-left: 3px solid #4a4a5e; - opacity: 0.7; -} -.task-card-header { - display: flex; - justify-content: space-between; - align-items: center; - align-items: flex-start; - padding: 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.task-header-left { - flex: 1; - min-width: 0; -} -.task-name { - font-size: 16px; - font-weight: 600; - color: #dcdce4; - margin-bottom: 2px; -} -.task-schedule { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - font-size: 12px; - color: #6c6c84; - font-family: "Menlo", "Monaco", "Courier New", monospace; -} -.schedule-icon { - font-size: 14px; -} -.task-status-badge { - flex-shrink: 0; -} -.status-badge { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - padding: 2px 10px; - border-radius: 3px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.03em; -} -.status-badge.status-enabled { - background: rgba(76, 175, 80, 0.12); - color: #3ec97a; -} -.status-badge.status-enabled .status-dot { - animation: pulse 1.5s infinite; -} -.status-badge.status-disabled { - background: #26263a; - color: #6c6c84; -} -.status-badge .status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; - background: currentColor; -} -.task-info-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 12px; - padding: 16px; -} -.task-info-item { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: flex-start; - gap: 10px; -} -.task-info-icon { - font-size: 18px; - color: #6c6c84; - flex-shrink: 0; -} -.task-info-content { - flex: 1; - min-width: 0; -} -.task-info-label { - font-size: 10px; - color: #6c6c84; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.03em; - margin-bottom: 2px; -} -.task-info-value { - font-size: 12px; - color: #a0a0b8; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.task-card-actions { - display: flex; - gap: 8px; - padding: 10px 16px; - background: #18181f; - border-top: 1px solid rgba(255, 255, 255, 0.06); -} -.task-card-actions button { - flex: 1; -} -.db-actions { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 16px; - padding: 10px; -} -.db-action-row { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - gap: 16px; - padding: 10px; - border-radius: 6px; - background: rgba(0, 0, 0, 0.06); -} -.db-action-info { - flex: 1; -} -.db-action-info h4 { - font-size: 0.95rem; - font-weight: 600; - color: #dcdce4; - margin-bottom: 2px; -} -.db-action-confirm { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - flex-shrink: 0; -} -.library-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - margin-bottom: 12px; - gap: 12px; - flex-wrap: wrap; -} -.toolbar-left { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 10px; -} -.toolbar-right { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 10px; -} -.sort-control select, -.page-size-control select { - padding: 4px 24px 4px 8px; - font-size: 11px; - background: #1f1f28; -} -.page-size-control { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 4px; -} -.library-stats { - display: flex; - justify-content: space-between; - align-items: center; - padding: 2px 0 6px 0; - font-size: 11px; -} -.type-filter-row { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - padding: 4px 0; - margin-bottom: 6px; - flex-wrap: wrap; -} -.pagination { - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - margin-top: 16px; - padding: 8px 0; -} -.page-btn { - min-width: 28px; - text-align: center; - font-variant-numeric: tabular-nums; -} -.page-ellipsis { - color: #6c6c84; - padding: 0 4px; - font-size: 12px; - user-select: none; -} -.audit-controls { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - margin-bottom: 12px; -} -.filter-select { - padding: 4px 24px 4px 8px; - font-size: 11px; - background: #1f1f28; -} -.action-danger { - background: rgba(228, 88, 88, 0.1); - color: #d47070; -} -.action-updated { - background: rgba(59, 120, 200, 0.1); - color: #6ca0d4; -} -.action-collection { - background: rgba(34, 160, 80, 0.1); - color: #5cb97a; -} -.action-collection-remove { - background: rgba(212, 160, 55, 0.1); - color: #c4a840; -} -.action-opened { - background: rgba(139, 92, 246, 0.1); - color: #9d8be0; -} -.action-scanned { - background: rgba(128, 128, 160, 0.08); - color: #6c6c84; -} -.clickable { - cursor: pointer; - color: #9698f7; -} -.clickable:hover { - text-decoration: underline; -} -.clickable-row { - cursor: pointer; -} -.clickable-row:hover { - background: rgba(255, 255, 255, 0.03); -} -.duplicates-view { - padding: 0; -} -.duplicates-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; -} -.duplicates-header h3 { - margin: 0; -} -.duplicates-summary { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 12px; -} -.duplicate-group { - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - margin-bottom: 8px; - overflow: hidden; -} -.duplicate-group-header { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 12px; - width: 100%; - padding: 10px 14px; - background: #1f1f28; - border: none; - cursor: pointer; - text-align: left; - color: #dcdce4; - font-size: 13px; -} -.duplicate-group-header:hover { - background: #26263a; -} -.expand-icon { - font-size: 10px; - width: 14px; - flex-shrink: 0; -} -.group-name { - font-weight: 600; - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.group-badge { - background: #7c7ef5; - color: #fff; - padding: 2px 8px; - border-radius: 10px; - font-size: 11px; - font-weight: 600; - flex-shrink: 0; -} -.group-size { - flex-shrink: 0; - font-size: 12px; -} -.group-hash { - font-size: 11px; - flex-shrink: 0; -} -.duplicate-items { - border-top: 1px solid rgba(255, 255, 255, 0.09); -} -.duplicate-item { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 12px; - padding: 10px 14px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.duplicate-item:last-child { - border-bottom: none; -} -.duplicate-item-keep { - background: rgba(76, 175, 80, 0.06); -} -.dup-thumb { - width: 48px; - height: 48px; - flex-shrink: 0; - border-radius: 3px; - overflow: hidden; -} -.dup-thumb-img { - width: 100%; - height: 100%; - object-fit: cover; -} -.dup-thumb-placeholder { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background: #26263a; - font-size: 20px; - color: #6c6c84; -} -.dup-info { - flex: 1; - min-width: 0; -} -.dup-filename { - font-weight: 600; - font-size: 13px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.dup-path { - font-size: 11px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.dup-meta { - font-size: 12px; - margin-top: 2px; -} -.dup-actions { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - flex-shrink: 0; -} -.keep-badge { - background: rgba(76, 175, 80, 0.12); - color: #4caf50; - padding: 2px 10px; - border-radius: 10px; - font-size: 11px; - font-weight: 600; -} -.saved-searches-list { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 4px; - max-height: 300px; - overflow-y: auto; -} -.saved-search-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - background: #18181f; - border-radius: 3px; - cursor: pointer; - transition: background 0.15s ease; -} -.saved-search-item:hover { - background: #1f1f28; -} -.saved-search-info { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 2px; - flex: 1; - min-width: 0; -} -.saved-search-name { - font-weight: 500; - color: #dcdce4; -} -.saved-search-query { - font-size: 11px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.backlinks-panel, -.outgoing-links-panel { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - margin-top: 16px; - overflow: hidden; -} -.backlinks-header, -.outgoing-links-header { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - padding: 10px 14px; - background: #26263a; - cursor: pointer; - user-select: none; - transition: background 0.1s; -} -.backlinks-header:hover, -.outgoing-links-header:hover { - background: rgba(255, 255, 255, 0.04); -} -.backlinks-toggle, -.outgoing-links-toggle { - font-size: 10px; - color: #6c6c84; - width: 12px; - text-align: center; -} -.backlinks-title, -.outgoing-links-title { - font-size: 12px; - font-weight: 600; - color: #dcdce4; - flex: 1; -} -.backlinks-count, -.outgoing-links-count { - font-size: 11px; - color: #6c6c84; -} -.backlinks-reindex-btn { - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - margin-left: auto; - background: rgba(0, 0, 0, 0); - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - color: #6c6c84; - font-size: 12px; - cursor: pointer; - transition: - background 0.1s, - color 0.1s, - border-color 0.1s; -} -.backlinks-reindex-btn:hover:not(:disabled) { - background: #1f1f28; - color: #dcdce4; - border-color: rgba(255, 255, 255, 0.14); -} -.backlinks-reindex-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} -.backlinks-content, -.outgoing-links-content { - padding: 12px; - border-top: 1px solid rgba(255, 255, 255, 0.06); -} -.backlinks-loading, -.outgoing-links-loading { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - padding: 12px; - color: #6c6c84; - font-size: 12px; -} -.backlinks-error, -.outgoing-links-error { - padding: 8px 12px; - background: rgba(228, 88, 88, 0.06); - border: 1px solid rgba(228, 88, 88, 0.2); - border-radius: 3px; - font-size: 12px; - color: #e45858; -} -.backlinks-empty, -.outgoing-links-empty { - padding: 16px; - text-align: center; - color: #6c6c84; - font-size: 12px; - font-style: italic; -} -.backlinks-list, -.outgoing-links-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 6px; -} -.backlink-item, -.outgoing-link-item { - padding: 10px 12px; - background: #111118; - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 3px; - cursor: pointer; - transition: - background 0.1s, - border-color 0.1s; -} -.backlink-item:hover, -.outgoing-link-item:hover { - background: #18181f; - border-color: rgba(255, 255, 255, 0.09); -} -.backlink-item.unresolved, -.outgoing-link-item.unresolved { - opacity: 0.7; - border-style: dashed; -} -.backlink-source, -.outgoing-link-target { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - margin-bottom: 2px; -} -.backlink-title, -.outgoing-link-text { - font-size: 13px; - font-weight: 500; - color: #dcdce4; - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.backlink-type-badge, -.outgoing-link-type-badge { - display: inline-block; - padding: 1px 6px; - border-radius: 12px; - font-size: 9px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.03em; -} -.backlink-type-badge.backlink-type-wikilink, -.backlink-type-badge.link-type-wikilink, -.outgoing-link-type-badge.backlink-type-wikilink, -.outgoing-link-type-badge.link-type-wikilink { - background: rgba(124, 126, 245, 0.15); - color: #9698f7; -} -.backlink-type-badge.backlink-type-embed, -.backlink-type-badge.link-type-embed, -.outgoing-link-type-badge.backlink-type-embed, -.outgoing-link-type-badge.link-type-embed { - background: rgba(139, 92, 246, 0.1); - color: #9d8be0; -} -.backlink-type-badge.backlink-type-markdown_link, -.backlink-type-badge.link-type-markdown_link, -.outgoing-link-type-badge.backlink-type-markdown_link, -.outgoing-link-type-badge.link-type-markdown_link { - background: rgba(59, 120, 200, 0.1); - color: #6ca0d4; -} -.backlink-context { - font-size: 11px; - color: #6c6c84; - line-height: 1.4; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} -.backlink-line { - color: #a0a0b8; - font-weight: 500; -} -.unresolved-badge { - padding: 1px 6px; - border-radius: 12px; - font-size: 9px; - font-weight: 600; - background: rgba(212, 160, 55, 0.1); - color: #d4a037; -} -.outgoing-links-unresolved-badge { - margin-left: 8px; - padding: 2px 8px; - border-radius: 10px; - font-size: 10px; - font-weight: 500; - background: rgba(212, 160, 55, 0.12); - color: #d4a037; -} -.outgoing-links-global-unresolved { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - margin-top: 12px; - padding: 10px 12px; - background: rgba(212, 160, 55, 0.06); - border: 1px solid rgba(212, 160, 55, 0.15); - border-radius: 3px; - font-size: 11px; - color: #6c6c84; -} -.outgoing-links-global-unresolved .unresolved-icon { - color: #d4a037; -} -.backlinks-message { - padding: 8px 10px; - margin-bottom: 10px; - border-radius: 3px; - font-size: 11px; -} -.backlinks-message.success { - background: rgba(62, 201, 122, 0.08); - border: 1px solid rgba(62, 201, 122, 0.2); - color: #3ec97a; -} -.backlinks-message.error { - background: rgba(228, 88, 88, 0.06); - border: 1px solid rgba(228, 88, 88, 0.2); - color: #e45858; -} -.graph-view { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - height: 100%; - background: #18181f; - border-radius: 5px; - overflow: hidden; -} -.graph-toolbar { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 16px; - padding: 12px 16px; - background: #1f1f28; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); -} -.graph-title { - font-size: 14px; - font-weight: 600; - color: #dcdce4; -} -.graph-controls { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 8px; - font-size: 12px; - color: #a0a0b8; -} -.graph-controls select { - padding: 4px 20px 4px 8px; - font-size: 11px; - background: #26263a; -} -.graph-stats { - margin-left: auto; - font-size: 11px; - color: #6c6c84; -} -.graph-container { - flex: 1; - position: relative; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - background: #111118; -} -.graph-loading, -.graph-error, -.graph-empty { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 10px; - padding: 48px; - color: #6c6c84; - font-size: 13px; - text-align: center; -} -.graph-svg { - max-width: 100%; - max-height: 100%; - cursor: grab; -} -.graph-svg-container { - position: relative; - width: 100%; - height: 100%; -} -.graph-zoom-controls { - position: absolute; - top: 16px; - left: 16px; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 8px; - z-index: 5; -} -.zoom-btn { - width: 36px; - height: 36px; - border-radius: 6px; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - color: #dcdce4; - font-size: 18px; - font-weight: bold; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.15s; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); -} -.zoom-btn:hover { - background: #26263a; - border-color: rgba(255, 255, 255, 0.14); - transform: scale(1.05); -} -.zoom-btn:active { - transform: scale(0.95); -} -.graph-edges line { - stroke: rgba(255, 255, 255, 0.14); - stroke-width: 1; - opacity: 0.6; -} -.graph-edges line.edge-type-wikilink { - stroke: #7c7ef5; -} -.graph-edges line.edge-type-embed { - stroke: #9d8be0; - stroke-dasharray: 4 2; -} -.graph-nodes .graph-node { - cursor: pointer; -} -.graph-nodes .graph-node circle { - fill: #4caf50; - stroke: #388e3c; - stroke-width: 2; - transition: - fill 0.15s, - stroke 0.15s; -} -.graph-nodes .graph-node:hover circle { - fill: #66bb6a; -} -.graph-nodes .graph-node.selected circle { - fill: #7c7ef5; - stroke: #5456d6; -} -.graph-nodes .graph-node text { - fill: #a0a0b8; - font-size: 11px; - pointer-events: none; - text-anchor: middle; - dominant-baseline: central; - transform: translateY(16px); -} -.node-details-panel { - position: absolute; - top: 16px; - right: 16px; - width: 280px; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); - z-index: 10; -} -.node-details-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 14px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.node-details-header h3 { - font-size: 13px; - font-weight: 600; - color: #dcdce4; - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.node-details-header .close-btn { - background: none; - border: none; - color: #6c6c84; - cursor: pointer; - font-size: 14px; - padding: 2px 6px; - line-height: 1; -} -.node-details-header .close-btn:hover { - color: #dcdce4; -} -.node-details-content { - padding: 14px; -} -.node-details-content .node-title { - font-size: 12px; - color: #a0a0b8; - margin-bottom: 12px; -} -.node-stats { - display: flex; - gap: 16px; - margin-bottom: 12px; -} -.node-stats .stat { - font-size: 12px; - color: #6c6c84; -} -.node-stats .stat strong { - color: #dcdce4; -} -.physics-controls-panel { - position: absolute; - top: 16px; - right: 16px; - width: 300px; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); - padding: 16px; - z-index: 10; -} -.physics-controls-panel h4 { - font-size: 13px; - font-weight: 600; - color: #dcdce4; - margin: 0 0 16px 0; - padding-bottom: 8px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} -.physics-controls-panel .btn { - width: 100%; - margin-top: 8px; -} -.control-group { - margin-bottom: 14px; -} -.control-group label { - display: block; - font-size: 11px; - font-weight: 500; - color: #a0a0b8; - margin-bottom: 6px; - text-transform: uppercase; - letter-spacing: 0.5px; -} -.control-group input[type="range"] { - width: 100%; - height: 4px; - border-radius: 4px; - background: #26263a; - outline: none; - -webkit-appearance: none; -} -.control-group input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 14px; - height: 14px; - border-radius: 50%; - background: #7c7ef5; - cursor: pointer; - transition: transform 0.1s; -} -.control-group input[type="range"]::-webkit-slider-thumb:hover { - transform: scale(1.15); -} -.control-group input[type="range"]::-moz-range-thumb { - width: 14px; - height: 14px; - border-radius: 50%; - background: #7c7ef5; - cursor: pointer; - border: none; - transition: transform 0.1s; -} -.control-group input[type="range"]::-moz-range-thumb:hover { - transform: scale(1.15); -} -.control-value { - display: inline-block; - margin-top: 2px; - font-size: 11px; - color: #6c6c84; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; -} -.theme-light { - --bg-0: #f5f5f7; - --bg-1: #eeeef0; - --bg-2: #fff; - --bg-3: #e8e8ec; - --border-subtle: rgba(0, 0, 0, 0.06); - --border: rgba(0, 0, 0, 0.1); - --border-strong: rgba(0, 0, 0, 0.16); - --text-0: #1a1a2e; - --text-1: #555570; - --text-2: #8888a0; - --accent: #6366f1; - --accent-dim: rgba(99, 102, 241, 0.1); - --accent-text: #4f52e8; - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); - --shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.12); -} -.theme-light ::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.12); -} -.theme-light ::-webkit-scrollbar-thumb:hover { - background: rgba(0, 0, 0, 0.08); -} -.theme-light ::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.06); -} -.theme-light .graph-nodes .graph-node text { - fill: #1a1a2e; -} -.theme-light .graph-edges line { - stroke: rgba(0, 0, 0, 0.12); -} -.theme-light .pdf-container { - background: #e8e8ec; -} -.skeleton-pulse { - animation: skeleton-pulse 1.5s ease-in-out infinite; - background: #26263a; - border-radius: 4px; -} -.skeleton-card { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 8px; - padding: 8px; -} -.skeleton-thumb { - width: 100%; - aspect-ratio: 1; - border-radius: 6px; -} -.skeleton-text { - height: 14px; - width: 80%; -} -.skeleton-text-short { - width: 50%; -} -.skeleton-row { - display: flex; - gap: 12px; - padding: 10px 16px; - align-items: center; -} -.skeleton-cell { - height: 14px; - flex: 1; - border-radius: 4px; -} -.skeleton-cell-icon { - width: 32px; - height: 32px; - flex: none; - border-radius: 4px; -} -.skeleton-cell-wide { - flex: 3; -} -.loading-overlay { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 10px; - background: rgba(0, 0, 0, 0.3); - z-index: 100; - border-radius: 8px; -} -.loading-spinner { - width: 32px; - height: 32px; - border: 3px solid rgba(255, 255, 255, 0.09); - border-top-color: #7c7ef5; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} -.loading-message { - color: #a0a0b8; - font-size: 0.9rem; -} -.login-container { - display: flex; - align-items: center; - justify-content: center; - height: 100vh; - background: #111118; -} -.login-card { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 7px; - padding: 24px; - width: 360px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); -} -.login-title { - font-size: 20px; - font-weight: 700; - color: #dcdce4; - text-align: center; - margin-bottom: 2px; -} -.login-subtitle { - font-size: 13px; - color: #6c6c84; - text-align: center; - margin-bottom: 20px; -} -.login-error { - background: rgba(228, 88, 88, 0.08); - border: 1px solid rgba(228, 88, 88, 0.2); - border-radius: 3px; - padding: 8px 12px; - margin-bottom: 12px; - font-size: 12px; - color: #e45858; -} -.login-form input[type="text"], -.login-form input[type="password"] { - width: 100%; -} -.login-btn { - width: 100%; - padding: 8px 16px; - font-size: 13px; - margin-top: 2px; -} -.pagination { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: 2px; - margin-top: 16px; - padding: 8px 0; -} -.page-btn { - min-width: 28px; - text-align: center; - font-variant-numeric: tabular-nums; -} -.page-ellipsis { - color: #6c6c84; - padding: 0 4px; - font-size: 12px; - user-select: none; -} -.help-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 200; - animation: fade-in 0.1s ease-out; -} -.help-dialog { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 7px; - padding: 16px; - min-width: 300px; - max-width: 400px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); -} -.help-dialog h3 { - font-size: 16px; - font-weight: 600; - margin-bottom: 16px; -} -.help-shortcuts { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - gap: 8px; - margin-bottom: 16px; -} -.shortcut-row { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 12px; -} -.shortcut-row kbd { - display: inline-block; - padding: 2px 8px; - background: #111118; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 11px; - color: #dcdce4; - min-width: 32px; - text-align: center; -} -.shortcut-row span { - font-size: 13px; - color: #a0a0b8; -} -.help-close { - display: block; - width: 100%; - padding: 6px 12px; - background: #26263a; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 3px; - color: #dcdce4; - font-size: 12px; - cursor: pointer; - text-align: center; -} -.help-close:hover { - background: rgba(255, 255, 255, 0.06); -} -.plugin-page { - padding: 16px 24px; - max-width: 100%; - overflow-x: hidden; -} -.plugin-page-title { - font-size: 14px; - font-weight: 600; - color: #dcdce4; - margin: 0 0 16px; -} -.plugin-container { - display: flex; - flex-direction: column; - gap: var(--plugin-gap, 0px); - padding: var(--plugin-padding, 0); -} -.plugin-grid { - display: grid; - grid-template-columns: repeat(var(--plugin-columns, 1), 1fr); - gap: var(--plugin-gap, 0px); -} -.plugin-flex { - display: flex; - gap: var(--plugin-gap, 0px); -} -.plugin-flex[data-direction="row"] { - flex-direction: row; -} -.plugin-flex[data-direction="column"] { - flex-direction: column; -} -.plugin-flex[data-justify="flex-start"] { - justify-content: flex-start; -} -.plugin-flex[data-justify="flex-end"] { - justify-content: flex-end; -} -.plugin-flex[data-justify="center"] { - justify-content: center; -} -.plugin-flex[data-justify="space-between"] { - justify-content: space-between; -} -.plugin-flex[data-justify="space-around"] { - justify-content: space-around; -} -.plugin-flex[data-justify="space-evenly"] { - justify-content: space-evenly; -} -.plugin-flex[data-align="flex-start"] { - align-items: flex-start; -} -.plugin-flex[data-align="flex-end"] { - align-items: flex-end; -} -.plugin-flex[data-align="center"] { - align-items: center; -} -.plugin-flex[data-align="stretch"] { - align-items: stretch; -} -.plugin-flex[data-align="baseline"] { - align-items: baseline; -} -.plugin-flex[data-wrap="wrap"] { - flex-wrap: wrap; -} -.plugin-flex[data-wrap="nowrap"] { - flex-wrap: nowrap; -} -.plugin-split { - display: flex; -} -.plugin-split-sidebar { - width: var(--plugin-sidebar-width, 200px); - flex-shrink: 0; -} -.plugin-split-main { - flex: 1; - min-width: 0; -} -.plugin-card { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 7px; - overflow: hidden; -} -.plugin-card-header { - padding: 12px 16px; - font-size: 12px; - font-weight: 600; - color: #dcdce4; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); - background: #26263a; -} -.plugin-card-content { - padding: 16px; -} -.plugin-card-footer { - padding: 12px 16px; - border-top: 1px solid rgba(255, 255, 255, 0.09); - background: #18181f; -} -.plugin-heading { - color: #dcdce4; - margin: 0; - line-height: 1.2; -} -.plugin-heading.level-1 { - font-size: 28px; - font-weight: 700; -} -.plugin-heading.level-2 { - font-size: 18px; - font-weight: 600; -} -.plugin-heading.level-3 { - font-size: 16px; - font-weight: 600; -} -.plugin-heading.level-4 { - font-size: 14px; - font-weight: 500; -} -.plugin-heading.level-5 { - font-size: 13px; - font-weight: 500; -} -.plugin-heading.level-6 { - font-size: 12px; - font-weight: 500; -} -.plugin-text { - margin: 0; - font-size: 12px; - color: #dcdce4; - line-height: 1.4; -} -.plugin-text.text-secondary { - color: #a0a0b8; -} -.plugin-text.text-error { - color: #d47070; -} -.plugin-text.text-success { - color: #3ec97a; -} -.plugin-text.text-warning { - color: #d4a037; -} -.plugin-text.text-bold { - font-weight: 600; -} -.plugin-text.text-italic { - font-style: italic; -} -.plugin-text.text-small { - font-size: 10px; -} -.plugin-text.text-large { - font-size: 15px; -} -.plugin-code { - background: #18181f; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - padding: 16px 24px; - font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; - font-size: 12px; - color: #dcdce4; - overflow-x: auto; - white-space: pre; -} -.plugin-code code { - font-family: inherit; - font-size: inherit; - color: inherit; -} -.plugin-tabs { - display: flex; - flex-direction: column; -} -.plugin-tab-list { - display: flex; - gap: 2px; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); - margin-bottom: 16px; -} -.plugin-tab { - padding: 8px 20px; - font-size: 12px; - font-weight: 500; - color: #a0a0b8; - background: rgba(0, 0, 0, 0); - border: none; - border-bottom: 2px solid rgba(0, 0, 0, 0); - cursor: pointer; - transition: - color 0.1s, - border-color 0.1s; -} -.plugin-tab:hover { - color: #dcdce4; -} -.plugin-tab.active { - color: #9698f7; - border-bottom-color: #7c7ef5; -} -.plugin-tab .tab-icon { - margin-right: 4px; -} -.plugin-tab-panel:not(.active) { - display: none; -} -.plugin-description-list-wrapper { - width: 100%; -} -.plugin-description-list { - display: grid; - grid-template-columns: max-content 1fr; - gap: 4px 16px; - margin: 0; - padding: 0; -} -.plugin-description-list dt { - font-size: 10px; - font-weight: 500; - color: #a0a0b8; - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 6px 0; - white-space: nowrap; -} -.plugin-description-list dd { - font-size: 12px; - color: #dcdce4; - padding: 6px 0; - margin: 0; - word-break: break-word; -} -.plugin-description-list.horizontal { - display: flex; - flex-wrap: wrap; - gap: 16px 24px; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); -} -.plugin-description-list.horizontal dt { - width: auto; - padding: 0; -} -.plugin-description-list.horizontal dd { - width: auto; - padding: 0; -} -.plugin-description-list.horizontal dt, -.plugin-description-list.horizontal dd { - display: inline; -} -.plugin-description-list.horizontal dt { - font-size: 9px; - text-transform: uppercase; - letter-spacing: 0.5px; - color: #6c6c84; - margin-bottom: 2px; -} -.plugin-description-list.horizontal dd { - font-size: 13px; - font-weight: 600; - color: #dcdce4; -} -.plugin-data-table-wrapper { - overflow-x: auto; -} -.plugin-data-table { - width: 100%; - border-collapse: collapse; - font-size: 12px; -} -.plugin-data-table thead tr { - border-bottom: 1px solid rgba(255, 255, 255, 0.14); -} -.plugin-data-table thead th { - padding: 8px 12px; - text-align: left; - font-size: 10px; - font-weight: 600; - color: #a0a0b8; - text-transform: uppercase; - letter-spacing: 0.5px; - white-space: nowrap; -} -.plugin-data-table tbody tr { - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - transition: background 0.08s; -} -.plugin-data-table tbody tr:hover { - background: rgba(255, 255, 255, 0.03); -} -.plugin-data-table tbody tr:last-child { - border-bottom: none; -} -.plugin-data-table tbody td { - padding: 8px 12px; - color: #dcdce4; - vertical-align: middle; -} -.plugin-col-constrained { - width: var(--plugin-col-width); -} -.table-filter { - margin-bottom: 12px; -} -.table-filter input { - width: 240px; - padding: 6px 12px; - background: #18181f; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - color: #dcdce4; - font-size: 12px; -} -.table-filter input::placeholder { - color: #6c6c84; -} -.table-filter input:focus { - outline: none; - border-color: #7c7ef5; -} -.table-pagination { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 0; - font-size: 12px; - color: #a0a0b8; -} -.row-actions { - white-space: nowrap; - width: 1%; -} -.row-actions .plugin-button { - padding: 4px 8px; - font-size: 10px; - margin-right: 4px; -} -.plugin-media-grid { - display: grid; - grid-template-columns: repeat(var(--plugin-columns, 2), 1fr); - gap: var(--plugin-gap, 8px); -} -.media-grid-item { - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 7px; - overflow: hidden; - display: flex; - flex-direction: column; -} -.media-grid-img { - width: 100%; - aspect-ratio: 16/9; - object-fit: cover; - display: block; -} -.media-grid-no-img { - width: 100%; - aspect-ratio: 16/9; - background: #26263a; - display: flex; - align-items: center; - justify-content: center; - font-size: 10px; - color: #6c6c84; -} -.media-grid-caption { - padding: 8px 12px; - font-size: 10px; - color: #dcdce4; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.plugin-list { - list-style: none; - margin: 0; - padding: 0; -} -.plugin-list-item { - padding: 8px 0; -} -.plugin-list-divider { - border: none; - border-top: 1px solid rgba(255, 255, 255, 0.06); - margin: 0; -} -.plugin-list-empty { - padding: 16px; - text-align: center; - color: #6c6c84; - font-size: 12px; -} -.plugin-button { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: - background 0.08s, - border-color 0.08s, - color 0.08s; - background: #1f1f28; - color: #dcdce4; -} -.plugin-button:disabled { - opacity: 0.45; - cursor: not-allowed; -} -.plugin-button.btn-primary { - background: #7c7ef5; - border-color: #7c7ef5; - color: #fff; -} -.plugin-button.btn-primary:hover:not(:disabled) { - background: #8b8df7; -} -.plugin-button.btn-secondary { - background: #26263a; - border-color: rgba(255, 255, 255, 0.14); - color: #dcdce4; -} -.plugin-button.btn-secondary:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.04); -} -.plugin-button.btn-tertiary { - background: rgba(0, 0, 0, 0); - border-color: rgba(0, 0, 0, 0); - color: #9698f7; -} -.plugin-button.btn-tertiary:hover:not(:disabled) { - background: rgba(124, 126, 245, 0.15); -} -.plugin-button.btn-danger { - background: rgba(0, 0, 0, 0); - border-color: rgba(228, 88, 88, 0.2); - color: #d47070; -} -.plugin-button.btn-danger:hover:not(:disabled) { - background: rgba(228, 88, 88, 0.06); -} -.plugin-button.btn-success { - background: rgba(0, 0, 0, 0); - border-color: rgba(62, 201, 122, 0.2); - color: #3ec97a; -} -.plugin-button.btn-success:hover:not(:disabled) { - background: rgba(62, 201, 122, 0.08); -} -.plugin-button.btn-ghost { - background: rgba(0, 0, 0, 0); - border-color: rgba(0, 0, 0, 0); - color: #a0a0b8; -} -.plugin-button.btn-ghost:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.04); -} -.plugin-badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 50%; - font-size: 9px; - font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; -} -.plugin-badge.badge-default, -.plugin-badge.badge-neutral { - background: rgba(255, 255, 255, 0.04); - color: #a0a0b8; -} -.plugin-badge.badge-primary { - background: rgba(124, 126, 245, 0.15); - color: #9698f7; -} -.plugin-badge.badge-secondary { - background: rgba(255, 255, 255, 0.03); - color: #dcdce4; -} -.plugin-badge.badge-success { - background: rgba(62, 201, 122, 0.08); - color: #3ec97a; -} -.plugin-badge.badge-warning { - background: rgba(212, 160, 55, 0.06); - color: #d4a037; -} -.plugin-badge.badge-error { - background: rgba(228, 88, 88, 0.06); - color: #d47070; -} -.plugin-badge.badge-info { - background: rgba(99, 102, 241, 0.08); - color: #9698f7; -} -.plugin-form { - display: flex; - flex-direction: column; - gap: 16px; -} -.form-field { - display: flex; - flex-direction: column; - gap: 6px; -} -.form-field label { - font-size: 12px; - font-weight: 500; - color: #dcdce4; -} -.form-field input, -.form-field textarea, -.form-field select { - padding: 8px 12px; - background: #18181f; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - color: #dcdce4; - font-size: 12px; - font-family: inherit; -} -.form-field input::placeholder, -.form-field textarea::placeholder, -.form-field select::placeholder { - color: #6c6c84; -} -.form-field input:focus, -.form-field textarea:focus, -.form-field select:focus { - outline: none; - border-color: #7c7ef5; - box-shadow: 0 0 0 2px rgba(124, 126, 245, 0.15); -} -.form-field textarea { - min-height: 80px; - resize: vertical; -} -.form-field select { - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a0a0b8' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 12px center; - padding-right: 32px; -} -.form-help { - margin: 0; - font-size: 10px; - color: #6c6c84; -} -.form-actions { - display: flex; - gap: 12px; - padding-top: 8px; -} -.required { - color: #e45858; -} -.plugin-link { - color: #9698f7; - text-decoration: none; -} -.plugin-link:hover { - text-decoration: underline; -} -.plugin-link-blocked { - color: #6c6c84; - text-decoration: line-through; - cursor: not-allowed; -} -.plugin-progress { - background: #18181f; - border: 1px solid rgba(255, 255, 255, 0.09); - border-radius: 5px; - height: 8px; - overflow: hidden; - display: flex; - align-items: center; - gap: 8px; -} -.plugin-progress-bar { - height: 100%; - background: #7c7ef5; - border-radius: 4px; - transition: width 0.3s ease; - width: var(--plugin-progress, 0%); -} -.plugin-progress-label { - font-size: 10px; - color: #a0a0b8; - white-space: nowrap; - flex-shrink: 0; -} -.plugin-chart { - overflow: auto; - height: var(--plugin-chart-height, 200px); -} -.plugin-chart .chart-title { - font-size: 13px; - font-weight: 600; - color: #dcdce4; - margin-bottom: 8px; -} -.plugin-chart .chart-x-label, -.plugin-chart .chart-y-label { - font-size: 10px; - color: #6c6c84; - margin-bottom: 4px; -} -.plugin-chart .chart-data-table { - overflow-x: auto; -} -.plugin-chart .chart-no-data { - padding: 24px; - text-align: center; - color: #6c6c84; - font-size: 12px; -} -.plugin-loading { - padding: 16px; - color: #a0a0b8; - font-size: 12px; - font-style: italic; -} -.plugin-error { - padding: 12px 16px; - background: rgba(228, 88, 88, 0.06); - border: 1px solid rgba(228, 88, 88, 0.2); - border-radius: 5px; - color: #d47070; - font-size: 12px; -} -.plugin-feedback { - position: sticky; - bottom: 16px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 12px 16px; - border-radius: 7px; - font-size: 12px; - z-index: 300; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); -} -.plugin-feedback.success { - background: rgba(62, 201, 122, 0.08); - border: 1px solid rgba(62, 201, 122, 0.2); - color: #3ec97a; -} -.plugin-feedback.error { - background: rgba(228, 88, 88, 0.06); - border: 1px solid rgba(228, 88, 88, 0.2); - color: #d47070; -} -.plugin-feedback-dismiss { - background: rgba(0, 0, 0, 0); - border: none; - color: inherit; - font-size: 14px; - cursor: pointer; - line-height: 1; - padding: 0; - opacity: 0.7; -} -.plugin-feedback-dismiss:hover { - opacity: 1; -} -.plugin-modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.65); - display: flex; - align-items: center; - justify-content: center; - z-index: 100; -} -.plugin-modal { - position: relative; - background: #1f1f28; - border: 1px solid rgba(255, 255, 255, 0.14); - border-radius: 12px; - padding: 32px; - min-width: 380px; - max-width: 640px; - max-height: 80vh; - overflow-y: auto; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); - z-index: 200; -} -.plugin-modal-close { - position: absolute; - top: 16px; - right: 16px; - background: rgba(0, 0, 0, 0); - border: none; - color: #a0a0b8; - font-size: 14px; - cursor: pointer; - line-height: 1; - padding: 4px; - border-radius: 5px; -} -.plugin-modal-close:hover { - background: rgba(255, 255, 255, 0.04); - color: #dcdce4; -} diff --git a/xtask/src/docs.rs b/xtask/src/docs.rs index bcf373a..5f9872c 100644 --- a/xtask/src/docs.rs +++ b/xtask/src/docs.rs @@ -95,45 +95,8 @@ pub fn run() { files_written += 1; } - // Generate docs/api.md index - let index_path = std::path::Path::new("docs/api.md"); - let mut index = String::new(); - index.push_str("# API Documentation\n\n"); - index.push_str( - "This is the index of all generated REST API documentation for \ - Pinakes.\n\n", - ); - index.push_str( - "Documentation is generated from OpenAPI annotations via `cargo xtask \ - docs`\n", - ); - index.push_str("(or `just docs`). Do not edit generated files by hand.\n\n"); - index.push_str("## Reference\n\n"); - index.push_str( - "- [openapi.json](api/openapi.json) - Full OpenAPI 3.0 specification\n\n", - ); - index.push_str("## Endpoints by Tag\n\n"); - for tag_name in tag_ops.keys() { - let file_name = format!("{}.md", tag_name.replace('/', "_")); - let description = tag_descriptions.get(tag_name).map_or("", String::as_str); - if description.is_empty() { - writeln!(index, "- [{}](api/{file_name})", title_case(tag_name)) - .expect("write to String"); - } else { - writeln!( - index, - "- [{}](api/{file_name}) - {description}", - title_case(tag_name) - ) - .expect("write to String"); - } - } - std::fs::write(index_path, &index).expect("write docs/api.md"); - println!("Written docs/api.md"); - println!( - "Done: wrote docs/api/openapi.json, docs/api.md, and {files_written} \ - markdown files." + "Done: wrote docs/api/openapi.json and {files_written} markdown files." ); }