This commit is contained in:
2026-03-23 21:54:11 -07:00
parent 7cb1dfa2d1
commit 8bcd6ada5d
12 changed files with 1131 additions and 1 deletions

452
Cargo.lock generated
View File

@@ -658,6 +658,26 @@ dependencies = [
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -958,6 +978,12 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fax"
version = "0.2.6"
@@ -1010,6 +1036,27 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -1122,11 +1169,24 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"wasip2",
"wasip3",
]
[[package]]
name = "gif"
version = "0.14.1"
@@ -1209,6 +1269,15 @@ dependencies = [
"allocator-api2",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1335,6 +1404,22 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -1353,9 +1438,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -1463,6 +1550,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "0.3.0"
@@ -1651,6 +1744,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lebe"
version = "0.5.3"
@@ -1710,6 +1809,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.1"
@@ -1955,6 +2060,23 @@ dependencies = [
"version_check",
]
[[package]]
name = "native-tls"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -2096,6 +2218,50 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openssl"
version = "0.10.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "owo-colors"
version = "4.2.3"
@@ -2203,6 +2369,19 @@ dependencies = [
"siphasher 1.0.1",
]
[[package]]
name = "pile-client"
version = "0.0.2"
source = "git+https://git.betalupi.com/Mark/pile.git?rev=90c5584513acde6f30f76d70c426cf6987643c1a#90c5584513acde6f30f76d70c426cf6987643c1a"
dependencies = [
"axum",
"bytes",
"futures-core",
"reqwest",
"serde",
"thiserror 2.0.18",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -2291,6 +2470,16 @@ dependencies = [
"prettytable-rs",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.117",
]
[[package]]
name = "prettytable-rs"
version = "0.10.0"
@@ -2502,6 +2691,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.5"
@@ -2717,10 +2912,12 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"quinn",
@@ -2731,6 +2928,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower",
@@ -2813,6 +3011,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.37"
@@ -2869,6 +3080,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -2881,6 +3101,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.27"
@@ -3002,6 +3245,26 @@ dependencies = [
"tower-http",
]
[[package]]
name = "service-pile"
version = "0.0.1"
dependencies = [
"axum",
"grass",
"libservice",
"maud",
"mime",
"pile-client",
"reqwest",
"serde",
"servable",
"service-assets",
"tokio",
"tower-http",
"tracing",
"url",
]
[[package]]
name = "service-webpage"
version = "0.0.1"
@@ -3323,6 +3586,40 @@ dependencies = [
"yaml-rust",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "term"
version = "0.7.0"
@@ -3537,6 +3834,16 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@@ -3958,6 +4265,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -3998,6 +4311,15 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
@@ -4056,6 +4378,28 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasm-streams"
version = "0.4.2"
@@ -4069,6 +4413,18 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "web-sys"
version = "0.3.83"
@@ -4099,6 +4455,7 @@ dependencies = [
"libservice",
"serde",
"service-assets",
"service-pile",
"service-webpage",
"tokio",
"toolbox",
@@ -4192,6 +4549,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-result"
version = "0.4.1"
@@ -4318,6 +4686,88 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "writeable"

View File

@@ -70,8 +70,11 @@ md-footnote = { path = "crates/lib/md-footnote" }
md-dev = { path = "crates/lib/md-dev" }
service-webpage = { path = "crates/service/service-webpage" }
service-pile = { path = "crates/service/service-pile" }
service-assets = { path = "crates/service/service-assets" }
pile-client = { git = "https://git.betalupi.com/Mark/pile.git", rev = "90c5584513acde6f30f76d70c426cf6987643c1a" }
#
# MARK: Server

View File

@@ -13,6 +13,7 @@ libservice = { workspace = true }
service-webpage = { workspace = true }
service-assets = { workspace = true }
service-pile = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }

View File

@@ -1,6 +1,7 @@
use anyhow::{Context, Result};
use libservice::{Service, ServiceConnectInfo, ToService};
use service_assets::AssetService;
use service_pile::PileService;
use service_webpage::WebpageService;
use std::sync::Arc;
use tracing::{error, info};
@@ -72,10 +73,15 @@ pub struct RouterState {}
pub async fn make_service(_state: Option<Arc<RouterState>>) -> Result<impl ToService> {
let service_webpage = WebpageService::new();
let service_assets = AssetService::new();
let service_pile = PileService::new()
.await
.map_err(|e| anyhow::anyhow!(e))
.context("while initializing pile datasets")?;
Ok(Service::new()
.merge(service_webpage)
.nest("/assets", service_assets)
.nest("/pile", service_pile)
.to_service()
.trace())
}

View File

@@ -42,6 +42,7 @@ pub struct LoggingConfig {
// Libs
pub libservice: LogLevel,
pub servable: LogLevel,
pub toolbox: LogLevel,
// Bins
@@ -67,15 +68,22 @@ impl From<LoggingConfig> for EnvFilter {
format!("axum={}", conf.silence),
format!("selectors={}", conf.silence),
format!("html5ever={}", conf.silence),
format!("tantivy={}", conf.silence),
format!("aws_smithy_runtime={}", conf.silence),
format!("aws_smithy_http_client={}", conf.silence),
format!("aws_sdk_s3={}", conf.silence),
format!("aws_sigv4={}", conf.silence),
//
// Libs
//
format!("toolbox={}", conf.toolbox),
format!("libservice={}", conf.libservice),
format!("servable={}", conf.servable),
//
// Bins
//
format!("service_webpage={}", conf.service),
format!("service_pile={}", conf.service),
format!("webpage={}", conf.webpage),
conf.other.to_string(),
]
@@ -183,6 +191,7 @@ impl LogFilterPreset {
// Libs
libservice: LogLevel::Error,
servable: LogLevel::Error,
toolbox: LogLevel::Error,
// Bins
@@ -196,6 +205,7 @@ impl LogFilterPreset {
// Libs
libservice: LogLevel::Warn,
servable: LogLevel::Warn,
toolbox: LogLevel::Warn,
// Bins
@@ -209,6 +219,7 @@ impl LogFilterPreset {
// Libs
libservice: LogLevel::Info,
servable: LogLevel::Info,
toolbox: LogLevel::Info,
// Bins
@@ -222,6 +233,7 @@ impl LogFilterPreset {
// Libs
libservice: LogLevel::Debug,
servable: LogLevel::Debug,
toolbox: LogLevel::Debug,
// Bins
@@ -235,6 +247,7 @@ impl LogFilterPreset {
// Libs
libservice: LogLevel::Trace,
servable: LogLevel::Trace,
toolbox: LogLevel::Trace,
// Bins
@@ -248,6 +261,7 @@ impl LogFilterPreset {
// Libs
libservice: LogLevel::Trace,
servable: LogLevel::Trace,
toolbox: LogLevel::Trace,
// Bins
@@ -261,6 +275,7 @@ impl LogFilterPreset {
// Libs
libservice: LogLevel::Trace,
servable: LogLevel::Trace,
toolbox: LogLevel::Trace,
// Bins

View File

@@ -0,0 +1,28 @@
[package]
name = "service-pile"
version = { workspace = true }
rust-version = { workspace = true }
edition = { workspace = true }
[lints]
workspace = true
[dependencies]
libservice = { workspace = true }
service-assets = { workspace = true }
pile-client = { workspace = true }
tracing = { workspace = true }
grass = { workspace = true }
axum = { workspace = true }
maud = { workspace = true }
serde = { workspace = true }
reqwest = { workspace = true }
tower-http = { workspace = true }
servable = { workspace = true }
url = { workspace = true }
mime = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }

View File

@@ -0,0 +1,183 @@
@import "text";
:root {
// Misc colors
--bgColor: #121212;
--lightBgColor: #3a3f46;
--fgColor: #ebebeb;
--metaColor: #6199bb;
--lightMetaColor: #638c86;
--linkColor: #e4dab3;
--codeBgColor: #292929;
--codeFgColor: var(--fgColor);
// Main colors
--grey: #696969;
// Accent colors, used only manally
--green: #a2c579;
--magenta: #ad79c5;
--orange: #e86a33;
--yellow: #e8bc00;
--pink: #fa9f83;
}
::selection,
::-moz-selection {
color: var(--bgColor);
background: var(--metaColor);
}
html {
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-size: 62.5%;
scrollbar-color: var(--metaColor) var(--bgColor);
scrollbar-width: auto;
background: var(--bgColor);
}
body {
font-family: "Fira";
font-size: 1.6rem;
line-height: 1.35;
max-width: 64rem;
margin: auto;
overflow-wrap: break-word;
background: var(--bgColor);
color: var(--fgColor);
}
div.wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
}
@media (max-width: 650px) {
.wrapper {
margin: 1rem;
}
}
// Loading spinner (three dots)
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 8px 0;
}
@keyframes dot-bounce {
0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
.search-meta {
font-size: 1.2rem;
color: var(--grey);
margin: 0 0 1.5em 0;
text-align: left;
}
#search-results {
margin-top: 0;
}
.result-grid {
display: flex;
flex-direction: column;
gap: 0.5em;
margin-top: 1.5em;
}
.result-item {
display: flex;
align-items: stretch;
border: 1px solid var(--lightBgColor);
border-radius: 3px;
overflow: hidden;
height: 64px;
}
.result-item-thumb {
width: 64px;
flex-shrink: 0;
background: var(--lightBgColor);
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.result-item-info {
padding: 0.5em 0.8em;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.15em;
min-width: 0;
flex: 1;
}
.result-item-key {
font-family: monospace;
font-size: 1.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
color: var(--fgColor);
cursor: pointer;
&:hover {
color: var(--metaColor);
}
}
.result-item-link {
font-family: monospace;
font-size: 1.1rem;
color: var(--linkColor);
text-decoration: none;
align-self: flex-start;
&:hover {
text-decoration: underline;
}
}
.result-sentinel {
height: 1px;
}
#preview-overlay {
display: none;
position: fixed;
z-index: 1000;
pointer-events: none;
border: 1px solid var(--lightBgColor);
background: var(--bgColor);
padding: 4px;
border-radius: 3px;
box-shadow: 0 4px 24px rgba(0,0,0,0.6);
img {
display: block;
max-width: 480px;
max-height: 480px;
object-fit: contain;
}
}

View File

@@ -0,0 +1,29 @@
h1 {
font-size: 3.5rem;
margin-top: 1ex;
margin-bottom: 1ex;
}
h2 {
font-size: 2.5rem;
margin-top: 1ex;
margin-bottom: 0.5ex;
}
h3 {
font-size: 2rem;
}
a {
text-decoration: none;
border-radius: .3rem;
padding: 0 .2ex 0 .2ex;
color: var(--linkColor);
transition: 150ms;
}
a:hover {
background-color: var(--linkColor);
color: var(--bgColor);
transition: 150ms;
}

View File

@@ -0,0 +1,37 @@
use axum::Router;
use libservice::ToService;
use pile_client::PileClient;
use std::sync::Arc;
mod pages;
mod routes;
pub const PILE_PREFIX: &str = "/pile";
pub const ASSET_PREFIX: &str = "/assets";
pub struct PileService {
client: Arc<PileClient>,
}
impl PileService {
pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let endpoint = std::env::var("PILE_ENDPOINT")?;
let api_key = std::env::var("PILE_API_KEY").ok();
let client = PileClient::new(&endpoint, api_key.as_deref())?;
Ok(Self {
client: Arc::new(client),
})
}
}
impl ToService for PileService {
#[inline]
fn make_router(&self) -> Option<Router<()>> {
Some(routes::router(self.client.clone()))
}
#[inline]
fn service_name(&self) -> Option<String> {
Some("pile".to_owned())
}
}

View File

@@ -0,0 +1,110 @@
use maud::{Markup, html};
use servable::{HtmlPage, PageMetadata, RenderContext};
use service_assets::assets::{CSS_FIRA, CSS_FONTAWESOME, HTMX};
use std::{pin::Pin, sync::LazyLock};
use crate::{ASSET_PREFIX, PILE_PREFIX, routes::CSS_PILE};
pub static INDEX: LazyLock<HtmlPage> = LazyLock::new(|| {
HtmlPage::default()
.with_style_linked(CSS_PILE.route_at(PILE_PREFIX))
.with_style_linked(CSS_FIRA.route_at(ASSET_PREFIX))
.with_style_linked(CSS_FONTAWESOME.route_at(ASSET_PREFIX))
.with_script_linked(HTMX.route_at(ASSET_PREFIX))
.with_meta(PageMetadata {
title: "Pile".into(),
author: None,
description: None,
image: None,
})
.with_render(render)
});
fn render<'a>(
_page: &'a HtmlPage,
_ctx: &'a RenderContext,
) -> Pin<Box<dyn Future<Output = Markup> + Send + Sync + 'a>> {
Box::pin(async {
html! {
div class="wrapper" style="margin-top:3ex;" {
div {
div style="
text-align:center;
padding-top:30px;
padding-bottom:60px;
" {
h1 class="brand" {
span class="fa fa-solid fa-book" aria-hidden="true" {}
" Library search"
}
div style="max-width:500px;margin:0 auto;padding:.4em 1em;" {
form {
input
class="search-input"
id="search"
name="q"
type="text"
placeholder="Type to search..."
style="
-moz-box-sizing: border-box !important;
box-sizing: border-box !important;
outline: none;
border: none;
border-radius: 1px;
margin-top: 5px;
margin-bottom: 5px;
padding: 10px 16px;
font-size: 17px;
width: 100%;
box-shadow: 0 0 0 1px var(--color-border),0 0 0 1px var(--color-border);
transition: box-shadow 150ms ease-in-out;
"
autofocus=""
autocomplete="off"
hx-get=(format!("{PILE_PREFIX}/search"))
hx-trigger="load, keyup changed delay:100ms"
hx-target="#search-results"
hx-swap="outerHTML"
hx-indicator="#search-spinner"
{}
}
div id="search-spinner" class="htmx-indicator dot-spinner" {
span {}
span {}
span {}
}
div id="search-results" {}
}
}
}
}
div id="preview-overlay" {
img id="preview-overlay-img" src="" alt="" {}
}
script { (maud::PreEscaped("
function showPreview(el) {
var ov = document.getElementById('preview-overlay');
var img = document.getElementById('preview-overlay-img');
img.src = el.dataset.preview;
var rect = el.getBoundingClientRect();
var size = 480;
var left = rect.right + 12;
if (left + size > window.innerWidth) { left = rect.left - size - 12; }
ov.style.left = left + 'px';
ov.style.top = Math.max(8, rect.top - size / 2 + rect.height / 2) + 'px';
ov.style.display = 'block';
}
function hidePreview() {
var ov = document.getElementById('preview-overlay');
ov.style.display = 'none';
document.getElementById('preview-overlay-img').src = '';
}
")) }
}
})
}

View File

@@ -0,0 +1,37 @@
use maud::html;
use reqwest::StatusCode;
use servable::{HtmlPage, PageMetadata};
use service_assets::assets::{CSS_FIRA, CSS_FONTAWESOME};
use std::sync::LazyLock;
mod index;
pub use index::INDEX;
use crate::{ASSET_PREFIX, PILE_PREFIX, routes::CSS_PILE};
pub static NOT_FOUND: LazyLock<HtmlPage> = LazyLock::new(|| {
HtmlPage::default()
.with_style_linked(CSS_PILE.route_at(PILE_PREFIX))
.with_style_linked(CSS_FIRA.route_at(ASSET_PREFIX))
.with_style_linked(CSS_FONTAWESOME.route_at(ASSET_PREFIX))
.with_meta(PageMetadata {
title: "Page not found".into(),
author: None,
description: None,
image: None,
})
.with_render(move |_page, _ctx| {
Box::pin(async {
html! {
div class="wrapper" {
div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh" {
p style="font-weight:bold;font-size:50pt;margin:0;" { "404" }
p style="font-size:13pt;margin:0;color:var(--grey);" { "(page not found)" }
a style="font-size:12pt;margin:10pt;padding:5px;" href="/" {"<- Back to site"}
}
}
}
})
})
.with_code(StatusCode::NOT_FOUND)
});

View File

@@ -0,0 +1,231 @@
use axum::Router;
use axum::extract::{Query, State};
use axum::routing::get;
use maud::{Markup, html};
use pile_client::PileClient;
use servable::{CACHE_BUST_STR, ServableRouter, ServableWithRoute, StaticAsset};
use std::sync::Arc;
use std::time::Instant;
use tower_http::compression::{CompressionLayer, DefaultPredicate};
use tracing::warn;
use crate::PILE_PREFIX;
use crate::pages;
const PAGE_SIZE: usize = 50;
pub(super) fn router(client: Arc<PileClient>) -> Router<()> {
let compression: CompressionLayer = CompressionLayer::new()
.br(true)
.deflate(true)
.gzip(true)
.zstd(true)
.compress_when(DefaultPredicate::new());
let search_router: Router<()> = Router::new()
.route("/search", get(search_handler))
.with_state(client.clone());
let api_router: Router<()> = client.dataset("books").proxy_router();
build_server()
.into_router()
.merge(search_router)
.nest("/api", api_router)
.layer(compression)
}
#[derive(serde::Deserialize)]
struct SearchQuery {
#[serde(default)]
q: String,
#[serde(default)]
page: usize,
}
async fn search_handler(
State(client): State<Arc<PileClient>>,
Query(params): Query<SearchQuery>,
) -> Markup {
let start = Instant::now();
let query = params.q.trim().to_lowercase();
let page = params.page;
let mut query_invalid = false;
let mut list_error = false;
let mut all_keys: Vec<(String, String)> = Vec::new();
let mut filtered_total = 0;
let mut grand_total = 0;
match query.is_empty() {
true => {
match client
.dataset("books")
.list_items(page * PAGE_SIZE, PAGE_SIZE)
.await
{
Err(error) => {
list_error = true;
warn!(message = "error while listing items", ?error);
}
Ok(resp) => {
all_keys = resp
.items
.into_iter()
.map(|item| (item.source, item.key))
.collect();
filtered_total = resp.total;
grand_total = resp.total;
}
}
}
false => match client.dataset("books").lookup(&query, Some(512)).await {
Err(_error) => {
query_invalid = true;
}
Ok(resp) => {
let mut results = resp.results;
results.sort_unstable_by(|a, b| f32::total_cmp(&b.score, &a.score));
filtered_total = results.len();
grand_total = results.len();
all_keys = results.into_iter().map(|r| (r.source, r.key)).collect();
}
},
}
// For empty query the server already paginated; for non-empty we slice locally.
let page_items: Vec<&(String, String)> = if query.is_empty() {
all_keys.iter().collect()
} else {
all_keys
.iter()
.skip(page * PAGE_SIZE)
.take(PAGE_SIZE)
.collect()
};
let has_more = (page + 1) * PAGE_SIZE < filtered_total;
let next_page = page + 1;
let encoded_q: String = url::form_urlencoded::byte_serialize(query.as_bytes()).collect();
let next_url = format!("{PILE_PREFIX}/search?q={}&page={}", encoded_q, next_page);
let elapsed_ms = start.elapsed().as_millis();
let mut msg = Vec::new();
if query_invalid {
msg.push("invalid query");
}
if list_error {
msg.push("list error");
}
if filtered_total == 0 {
msg.push("no results");
}
if page == 0 {
html! {
div id="search-results" {
p class="search-meta" {
"Filtered " (filtered_total) "/" (grand_total) " items in " (elapsed_ms) "ms"
@if !msg.is_empty() {
span style="color:var(--orange)" {
(format!("\u{00A0}\u{00A0}({})", msg.join(", ")))
}
}
}
div class="result-grid" {
@for (source, key) in &page_items {
(result_item(source, key))
}
@if has_more {
div
class="result-sentinel"
hx-get=(next_url)
hx-trigger="revealed"
hx-swap="outerHTML"
hx-target="this"
{}
}
}
}
}
} else {
html! {
@for (source, key) in &page_items {
(result_item(source, key))
}
@if has_more {
div
class="result-sentinel"
hx-get=(next_url)
hx-trigger="revealed"
hx-swap="outerHTML"
hx-target="this"
{}
}
}
}
}
fn result_item(source: &str, key: &str) -> Markup {
let enc_source: String = url::form_urlencoded::byte_serialize(source.as_bytes()).collect();
let enc_key: String = url::form_urlencoded::byte_serialize(key.as_bytes()).collect();
let enc_path: String =
url::form_urlencoded::byte_serialize("$.pdf.pages[0]".as_bytes()).collect();
let thumb_url =
format!("{PILE_PREFIX}/api/field?source={enc_source}&key={enc_key}&path={enc_path}");
let item_url =
format!("{PILE_PREFIX}/api/item?source={enc_source}&key={enc_key}&download=false");
html! {
div class="result-item" {
div class="result-item-thumb"
data-preview=(thumb_url)
onmouseenter="showPreview(this)"
onmouseleave="hidePreview()"
{
img src=(thumb_url) alt="" onerror="this.style.visibility='hidden'" {}
}
div class="result-item-info" {
span class="result-item-key"
data-key=(key)
onclick="navigator.clipboard.writeText(this.dataset.key)"
title="Click to copy"
{ (key) }
a class="result-item-link" href=(item_url) target="_blank" { "item" }
}
}
}
}
pub static CSS_PILE: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|| format!("/css/{}/main.css", *CACHE_BUST_STR),
StaticAsset {
bytes: grass::include!("crates/service/service-pile/css/main.scss").as_bytes(),
mime: mime::TEXT_CSS,
ttl: StaticAsset::DEFAULT_TTL,
},
);
fn build_server() -> ServableRouter {
ServableRouter::new()
.with_404(&pages::NOT_FOUND)
.add_page("/", &pages::INDEX)
//
.add_page_with_route(&CSS_PILE)
}
#[test]
#[expect(clippy::unwrap_used)]
fn server_builds_without_panic() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let _server = build_server();
});
}