mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 06:26:44 -06:00
feat: add Rust reverse proxy with JSON logging
- Axum-based API server with Unix socket and TCP support - Custom tracing formatters for Railway-compatible JSON logs - SvelteKit hooks and Vite plugin for unified logging - Justfile updated for concurrent dev workflow with hl log viewer
This commit is contained in:
Vendored
+5
@@ -3,3 +3,8 @@ target/
|
|||||||
.vscode/
|
.vscode/
|
||||||
web/build/
|
web/build/
|
||||||
web/.svelte-kit/
|
web/.svelte-kit/
|
||||||
|
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
|
||||||
|
/target
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#:schema https://raw.githubusercontent.com/pamburus/hl/v0.34.0/schema/json/config.schema.json
|
||||||
|
|
||||||
|
# Configuration for Railway-compatible JSON logs
|
||||||
|
# Usage: hl --config .hl.config.toml
|
||||||
|
|
||||||
|
# Time format for display (Railway ignores timestamp but we include it)
|
||||||
|
time-format = "%b %d %T.%3N"
|
||||||
|
time-zone = "UTC"
|
||||||
|
|
||||||
|
# Input file display settings
|
||||||
|
input-info = "auto"
|
||||||
|
|
||||||
|
# ASCII mode detection
|
||||||
|
ascii = "auto"
|
||||||
|
|
||||||
|
# Theme
|
||||||
|
theme = "uni"
|
||||||
|
theme-overlays = ["@accent-italic"]
|
||||||
|
|
||||||
|
[fields]
|
||||||
|
# Don't ignore any fields by default
|
||||||
|
ignore = []
|
||||||
|
hide = []
|
||||||
|
|
||||||
|
# Field configuration for Railway format
|
||||||
|
[fields.predefined]
|
||||||
|
|
||||||
|
# Timestamp field (Railway ignores this but we want consistency)
|
||||||
|
[fields.predefined.time]
|
||||||
|
show = "auto"
|
||||||
|
names = [
|
||||||
|
"timestamp",
|
||||||
|
"ts",
|
||||||
|
"time",
|
||||||
|
"@timestamp"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Logger field (optional, matches Rust 'target' field)
|
||||||
|
[fields.predefined.logger]
|
||||||
|
names = ["logger", "target", "span.name"]
|
||||||
|
|
||||||
|
# Level field (Railway uses this)
|
||||||
|
[fields.predefined.level]
|
||||||
|
show = "auto"
|
||||||
|
|
||||||
|
[[fields.predefined.level.variants]]
|
||||||
|
names = ["level"]
|
||||||
|
|
||||||
|
[fields.predefined.level.variants.values]
|
||||||
|
error = ["error", "err", "fatal", "critical", "panic"]
|
||||||
|
warning = ["warning", "warn"]
|
||||||
|
info = ["info", "information"]
|
||||||
|
debug = ["debug"]
|
||||||
|
trace = ["trace"]
|
||||||
|
|
||||||
|
# Message field (Railway uses this)
|
||||||
|
[fields.predefined.message]
|
||||||
|
names = ["message", "msg"]
|
||||||
|
|
||||||
|
# Caller field
|
||||||
|
[fields.predefined.caller]
|
||||||
|
names = ["caller"]
|
||||||
|
|
||||||
|
[fields.predefined.caller-file]
|
||||||
|
names = []
|
||||||
|
|
||||||
|
[fields.predefined.caller-line]
|
||||||
|
names = []
|
||||||
|
|
||||||
|
# Formatting settings
|
||||||
|
[formatting]
|
||||||
|
# Flatten nested fields
|
||||||
|
flatten = "always"
|
||||||
|
|
||||||
|
# Message format - delimited for better readability
|
||||||
|
[formatting.message]
|
||||||
|
format = "delimited"
|
||||||
|
|
||||||
|
# Punctuation
|
||||||
|
[formatting.punctuation]
|
||||||
|
logger-name-separator = ":"
|
||||||
|
field-key-value-separator = "="
|
||||||
|
string-opening-quote = "'"
|
||||||
|
string-closing-quote = "'"
|
||||||
|
caller-name-file-separator = " @ "
|
||||||
|
hidden-fields-indicator = " ..."
|
||||||
|
level-left-separator = "["
|
||||||
|
level-right-separator = "]"
|
||||||
|
input-number-prefix = "#"
|
||||||
|
input-number-left-separator = ""
|
||||||
|
input-name-left-separator = ""
|
||||||
|
array-separator = " "
|
||||||
|
source-location-separator = { ascii = "-> ", unicode = "→ " }
|
||||||
|
input-number-right-separator = { ascii = " | ", unicode = " │ " }
|
||||||
|
input-name-right-separator = { ascii = " | ", unicode = " │ " }
|
||||||
|
input-name-clipping = { ascii = "..", unicode = "··" }
|
||||||
|
input-name-common-part = { ascii = "..", unicode = "··" }
|
||||||
|
message-delimiter = { ascii = "::", unicode = "›" }
|
||||||
Generated
+1561
File diff suppressed because it is too large
Load Diff
+20
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.8.8"
|
||||||
|
clap = { version = "4.5.54", features = ["derive", "env"] }
|
||||||
|
nu-ansi-term = "0.50.3"
|
||||||
|
reqwest = { version = "0.13", features = ["charset", "json"], default-features = false }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.148"
|
||||||
|
time = { version = "0.3.44", features = ["formatting", "macros"] }
|
||||||
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7.18", features = ["io"] }
|
||||||
|
tower = "0.5"
|
||||||
|
tower-http = { version = "0.6.8", features = ["trace", "cors"] }
|
||||||
|
tracing = "0.1.44"
|
||||||
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] }
|
||||||
|
ulid = { version = "1", features = ["serde"] }
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
|
default:
|
||||||
|
just --list
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
bun run --cwd web dev
|
just dev-json | hl --config .hl.config.toml -P
|
||||||
|
|
||||||
|
dev-json:
|
||||||
|
LOG_JSON=true UPSTREAM_URL=/tmp/xevion-api.sock bunx concurrently --raw --prefix none "bun run --silent --cwd web dev --port 5173" "cargo watch --quiet --exec 'run --quiet -- --listen localhost:8080 --listen /tmp/xevion-api.sock --downstream http://localhost:5173'"
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
bun install --cwd web
|
bun install --cwd web
|
||||||
|
cargo build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
bun run --cwd web build
|
bun run --cwd web build
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
check:
|
check:
|
||||||
bun run --cwd web format
|
bun run --cwd web format
|
||||||
bun run --cwd web lint
|
bun run --cwd web lint
|
||||||
bun run --cwd web check
|
bun run --cwd web check
|
||||||
|
cargo clippy --all-targets
|
||||||
|
cargo fmt --check
|
||||||
|
|||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Server configuration parsed from CLI arguments and environment variables
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "api")]
|
||||||
|
#[command(about = "xevion.dev API server with ISR caching", long_about = None)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Address(es) to listen on. Can be host:port, :port, or Unix socket path.
|
||||||
|
/// Can be specified multiple times.
|
||||||
|
/// Examples: :8080, 0.0.0.0:8080, [::]:8080, /tmp/api.sock
|
||||||
|
#[arg(long, env = "LISTEN_ADDR", value_delimiter = ',', required = true)]
|
||||||
|
pub listen: Vec<ListenAddr>,
|
||||||
|
|
||||||
|
/// Downstream Bun SSR server URL or Unix socket path
|
||||||
|
/// Examples: http://localhost:5173, /tmp/bun.sock
|
||||||
|
#[arg(long, env = "DOWNSTREAM_URL", required = true)]
|
||||||
|
pub downstream: String,
|
||||||
|
|
||||||
|
/// Optional header name to trust for request IDs (e.g., X-Railway-Request-Id)
|
||||||
|
#[arg(long, env = "TRUST_REQUEST_ID")]
|
||||||
|
pub trust_request_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Address to listen on - either TCP or Unix socket
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ListenAddr {
|
||||||
|
Tcp(SocketAddr),
|
||||||
|
Unix(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ListenAddr {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
// Unix socket: starts with / or ./
|
||||||
|
if s.starts_with('/') || s.starts_with("./") {
|
||||||
|
return Ok(ListenAddr::Unix(PathBuf::from(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shorthand :port -> 127.0.0.1:port
|
||||||
|
if let Some(port_str) = s.strip_prefix(':') {
|
||||||
|
let port: u16 = port_str
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("Invalid port number: {}", port_str))?;
|
||||||
|
return Ok(ListenAddr::Tcp(SocketAddr::from(([127, 0, 0, 1], port))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as a socket address (handles both IPv4 and IPv6)
|
||||||
|
// This supports formats like: 0.0.0.0:8080, [::]:8080, 192.168.1.1:3000
|
||||||
|
match s.parse::<SocketAddr>() {
|
||||||
|
Ok(addr) => Ok(ListenAddr::Tcp(addr)),
|
||||||
|
Err(_) => {
|
||||||
|
// Try resolving as hostname:port
|
||||||
|
match s.to_socket_addrs() {
|
||||||
|
Ok(mut addrs) => addrs
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format!("Could not resolve address: {}", s))
|
||||||
|
.map(ListenAddr::Tcp),
|
||||||
|
Err(_) => Err(format!(
|
||||||
|
"Invalid address '{}'. Expected host:port, :port, or Unix socket path",
|
||||||
|
s
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ListenAddr {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ListenAddr::Tcp(addr) => write!(f, "{}", addr),
|
||||||
|
ListenAddr::Unix(path) => write!(f, "{}", path.display()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_shorthand_port() {
|
||||||
|
let addr: ListenAddr = ":8080".parse().unwrap();
|
||||||
|
match addr {
|
||||||
|
ListenAddr::Tcp(socket) => {
|
||||||
|
assert_eq!(socket.port(), 8080);
|
||||||
|
assert_eq!(socket.ip().to_string(), "127.0.0.1");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected TCP address"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_ipv4() {
|
||||||
|
let addr: ListenAddr = "0.0.0.0:8080".parse().unwrap();
|
||||||
|
match addr {
|
||||||
|
ListenAddr::Tcp(socket) => {
|
||||||
|
assert_eq!(socket.port(), 8080);
|
||||||
|
assert_eq!(socket.ip().to_string(), "0.0.0.0");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected TCP address"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_ipv6() {
|
||||||
|
let addr: ListenAddr = "[::]:8080".parse().unwrap();
|
||||||
|
match addr {
|
||||||
|
ListenAddr::Tcp(socket) => {
|
||||||
|
assert_eq!(socket.port(), 8080);
|
||||||
|
assert_eq!(socket.ip().to_string(), "::");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected TCP address"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_unix_socket() {
|
||||||
|
let addr: ListenAddr = "/tmp/api.sock".parse().unwrap();
|
||||||
|
match addr {
|
||||||
|
ListenAddr::Unix(path) => {
|
||||||
|
assert_eq!(path, PathBuf::from("/tmp/api.sock"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Unix socket"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_relative_unix_socket() {
|
||||||
|
let addr: ListenAddr = "./api.sock".parse().unwrap();
|
||||||
|
match addr {
|
||||||
|
ListenAddr::Unix(path) => {
|
||||||
|
assert_eq!(path, PathBuf::from("./api.sock"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Unix socket"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
//! Custom tracing formatter for Railway-compatible structured logging
|
||||||
|
|
||||||
|
use nu_ansi_term::Color;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::fmt;
|
||||||
|
use time::macros::format_description;
|
||||||
|
use time::{format_description::FormatItem, OffsetDateTime};
|
||||||
|
use tracing::field::{Field, Visit};
|
||||||
|
use tracing::{Event, Level, Subscriber};
|
||||||
|
use tracing_subscriber::fmt::format::Writer;
|
||||||
|
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields};
|
||||||
|
use tracing_subscriber::registry::LookupSpan;
|
||||||
|
|
||||||
|
/// Cached format description for timestamps with 3 subsecond digits (milliseconds)
|
||||||
|
const TIMESTAMP_FORMAT: &[FormatItem<'static>] =
|
||||||
|
format_description!("[hour]:[minute]:[second].[subsecond digits:3]");
|
||||||
|
|
||||||
|
/// A custom formatter with enhanced timestamp formatting and colored output
|
||||||
|
///
|
||||||
|
/// Provides human-readable output for local development with:
|
||||||
|
/// - Colored log levels
|
||||||
|
/// - Timestamp with millisecond precision
|
||||||
|
/// - Span context with hierarchy
|
||||||
|
/// - Clean field formatting
|
||||||
|
pub struct CustomPrettyFormatter;
|
||||||
|
|
||||||
|
impl<S, N> FormatEvent<S, N> for CustomPrettyFormatter
|
||||||
|
where
|
||||||
|
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||||
|
N: for<'a> FormatFields<'a> + 'static,
|
||||||
|
{
|
||||||
|
fn format_event(
|
||||||
|
&self,
|
||||||
|
ctx: &FmtContext<'_, S, N>,
|
||||||
|
mut writer: Writer<'_>,
|
||||||
|
event: &Event<'_>,
|
||||||
|
) -> fmt::Result {
|
||||||
|
let meta = event.metadata();
|
||||||
|
|
||||||
|
// 1) Timestamp (dimmed when ANSI)
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let formatted_time = now.format(&TIMESTAMP_FORMAT).map_err(|e| {
|
||||||
|
eprintln!("Failed to format timestamp: {}", e);
|
||||||
|
fmt::Error
|
||||||
|
})?;
|
||||||
|
write_dimmed(&mut writer, formatted_time)?;
|
||||||
|
writer.write_char(' ')?;
|
||||||
|
|
||||||
|
// 2) Colored 5-char level
|
||||||
|
write_colored_level(&mut writer, meta.level())?;
|
||||||
|
writer.write_char(' ')?;
|
||||||
|
|
||||||
|
// 3) Span scope chain (bold names, fields in braces, dimmed ':')
|
||||||
|
if let Some(scope) = ctx.event_scope() {
|
||||||
|
let mut saw_any = false;
|
||||||
|
for span in scope.from_root() {
|
||||||
|
write_bold(&mut writer, span.metadata().name())?;
|
||||||
|
saw_any = true;
|
||||||
|
|
||||||
|
write_dimmed(&mut writer, ":")?;
|
||||||
|
|
||||||
|
let ext = span.extensions();
|
||||||
|
if let Some(fields) = &ext.get::<FormattedFields<N>>() {
|
||||||
|
if !fields.fields.is_empty() {
|
||||||
|
write_bold(&mut writer, "{")?;
|
||||||
|
writer.write_str(fields.fields.as_str())?;
|
||||||
|
write_bold(&mut writer, "}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_dimmed(&mut writer, ":")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if saw_any {
|
||||||
|
writer.write_char(' ')?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Target (dimmed), then a space
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "{}: ", Color::DarkGray.paint(meta.target()))?;
|
||||||
|
} else {
|
||||||
|
write!(writer, "{}: ", meta.target())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Event fields
|
||||||
|
ctx.format_fields(writer.by_ref(), event)?;
|
||||||
|
|
||||||
|
// 6) Newline
|
||||||
|
writeln!(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A custom JSON formatter that flattens fields to root level for Railway
|
||||||
|
///
|
||||||
|
/// Outputs logs in Railway-compatible format:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "message": "...",
|
||||||
|
/// "level": "...",
|
||||||
|
/// "target": "...",
|
||||||
|
/// "customAttribute": "..."
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This format allows Railway to:
|
||||||
|
/// - Parse the `message` field correctly
|
||||||
|
/// - Filter by `level` and custom attributes using `@attribute:value`
|
||||||
|
/// - Preserve multi-line logs like stack traces
|
||||||
|
pub struct CustomJsonFormatter;
|
||||||
|
|
||||||
|
impl<S, N> FormatEvent<S, N> for CustomJsonFormatter
|
||||||
|
where
|
||||||
|
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||||
|
N: for<'a> FormatFields<'a> + 'static,
|
||||||
|
{
|
||||||
|
fn format_event(
|
||||||
|
&self,
|
||||||
|
ctx: &FmtContext<'_, S, N>,
|
||||||
|
mut writer: Writer<'_>,
|
||||||
|
event: &Event<'_>,
|
||||||
|
) -> fmt::Result {
|
||||||
|
let meta = event.metadata();
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct EventFields {
|
||||||
|
timestamp: String,
|
||||||
|
message: String,
|
||||||
|
level: String,
|
||||||
|
target: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
fields: Map<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let (message, fields) = {
|
||||||
|
let mut message: Option<String> = None;
|
||||||
|
let mut fields: Map<String, Value> = Map::new();
|
||||||
|
|
||||||
|
struct FieldVisitor<'a> {
|
||||||
|
message: &'a mut Option<String>,
|
||||||
|
fields: &'a mut Map<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Visit for FieldVisitor<'a> {
|
||||||
|
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||||
|
let key = field.name();
|
||||||
|
if key == "message" {
|
||||||
|
*self.message = Some(format!("{:?}", value));
|
||||||
|
} else {
|
||||||
|
self.fields
|
||||||
|
.insert(key.to_string(), Value::String(format!("{:?}", value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_str(&mut self, field: &Field, value: &str) {
|
||||||
|
let key = field.name();
|
||||||
|
if key == "message" {
|
||||||
|
*self.message = Some(value.to_string());
|
||||||
|
} else {
|
||||||
|
self.fields
|
||||||
|
.insert(key.to_string(), Value::String(value.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_i64(&mut self, field: &Field, value: i64) {
|
||||||
|
let key = field.name();
|
||||||
|
if key != "message" {
|
||||||
|
self.fields.insert(
|
||||||
|
key.to_string(),
|
||||||
|
Value::Number(serde_json::Number::from(value)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_u64(&mut self, field: &Field, value: u64) {
|
||||||
|
let key = field.name();
|
||||||
|
if key != "message" {
|
||||||
|
self.fields.insert(
|
||||||
|
key.to_string(),
|
||||||
|
Value::Number(serde_json::Number::from(value)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_bool(&mut self, field: &Field, value: bool) {
|
||||||
|
let key = field.name();
|
||||||
|
if key != "message" {
|
||||||
|
self.fields.insert(key.to_string(), Value::Bool(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visitor = FieldVisitor {
|
||||||
|
message: &mut message,
|
||||||
|
fields: &mut fields,
|
||||||
|
};
|
||||||
|
event.record(&mut visitor);
|
||||||
|
|
||||||
|
// Collect span information from the span hierarchy
|
||||||
|
// Flatten all span fields directly into root level
|
||||||
|
if let Some(scope) = ctx.event_scope() {
|
||||||
|
for span in scope.from_root() {
|
||||||
|
// Extract span fields by parsing the stored extension data
|
||||||
|
// The fields are stored as a formatted string, so we need to parse them
|
||||||
|
let ext = span.extensions();
|
||||||
|
if let Some(formatted_fields) = ext.get::<FormattedFields<N>>() {
|
||||||
|
let field_str = formatted_fields.fields.as_str();
|
||||||
|
|
||||||
|
// Parse key=value pairs from the formatted string
|
||||||
|
// Format is typically: key=value key2=value2
|
||||||
|
for pair in field_str.split_whitespace() {
|
||||||
|
if let Some((key, value)) = pair.split_once('=') {
|
||||||
|
// Remove quotes if present
|
||||||
|
let value = value.trim_matches('"').trim_matches('\'');
|
||||||
|
fields.insert(key.to_string(), Value::String(value.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(message, fields)
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = EventFields {
|
||||||
|
timestamp: OffsetDateTime::now_utc()
|
||||||
|
.format(&time::format_description::well_known::Rfc3339)
|
||||||
|
.unwrap_or_else(|_| String::from("1970-01-01T00:00:00Z")),
|
||||||
|
message: message.unwrap_or_default(),
|
||||||
|
level: meta.level().to_string().to_lowercase(),
|
||||||
|
target: meta.target().to_string(),
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
writer,
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(&json).unwrap_or_else(|_| "{}".to_string())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the verbosity level with colored output
|
||||||
|
fn write_colored_level(writer: &mut Writer<'_>, level: &Level) -> fmt::Result {
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
let colored = match *level {
|
||||||
|
Level::TRACE => Color::Purple.paint("TRACE"),
|
||||||
|
Level::DEBUG => Color::Blue.paint("DEBUG"),
|
||||||
|
Level::INFO => Color::Green.paint(" INFO"),
|
||||||
|
Level::WARN => Color::Yellow.paint(" WARN"),
|
||||||
|
Level::ERROR => Color::Red.paint("ERROR"),
|
||||||
|
};
|
||||||
|
write!(writer, "{}", colored)
|
||||||
|
} else {
|
||||||
|
// Right-pad to width 5 for alignment
|
||||||
|
match *level {
|
||||||
|
Level::TRACE => write!(writer, "{:>5}", "TRACE"),
|
||||||
|
Level::DEBUG => write!(writer, "{:>5}", "DEBUG"),
|
||||||
|
Level::INFO => write!(writer, "{:>5}", " INFO"),
|
||||||
|
Level::WARN => write!(writer, "{:>5}", " WARN"),
|
||||||
|
Level::ERROR => write!(writer, "{:>5}", "ERROR"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_dimmed(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result {
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "{}", Color::DarkGray.paint(s.to_string()))
|
||||||
|
} else {
|
||||||
|
write!(writer, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_bold(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result {
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "{}", Color::White.bold().paint(s.to_string()))
|
||||||
|
} else {
|
||||||
|
write!(writer, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
+418
@@ -0,0 +1,418 @@
|
|||||||
|
use axum::{
|
||||||
|
Json, Router,
|
||||||
|
extract::{Request, State},
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use clap::Parser;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
||||||
|
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod formatter;
|
||||||
|
mod middleware;
|
||||||
|
use config::{Args, ListenAddr};
|
||||||
|
use formatter::{CustomJsonFormatter, CustomPrettyFormatter};
|
||||||
|
use middleware::RequestIdLayer;
|
||||||
|
|
||||||
|
fn init_tracing() {
|
||||||
|
let use_json = std::env::var("LOG_JSON")
|
||||||
|
.map(|v| v == "true" || v == "1")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// Build the EnvFilter
|
||||||
|
// Priority: RUST_LOG > LOG_LEVEL > default
|
||||||
|
let filter = if let Ok(rust_log) = std::env::var("RUST_LOG") {
|
||||||
|
// RUST_LOG overwrites everything
|
||||||
|
EnvFilter::new(rust_log)
|
||||||
|
} else {
|
||||||
|
// Get LOG_LEVEL for our crate, default based on build profile
|
||||||
|
let our_level = std::env::var("LOG_LEVEL").unwrap_or_else(|_| {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
"debug".to_string()
|
||||||
|
} else {
|
||||||
|
"info".to_string()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default other crates to WARN, our crate to LOG_LEVEL
|
||||||
|
EnvFilter::new(format!("warn,api={}", our_level))
|
||||||
|
};
|
||||||
|
|
||||||
|
if use_json {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.event_format(CustomJsonFormatter)
|
||||||
|
.fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new())
|
||||||
|
.with_ansi(false), // Disable ANSI codes in JSON mode
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
} else {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(tracing_subscriber::fmt::layer().event_format(CustomPrettyFormatter))
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Initialize tracing with configurable format and levels
|
||||||
|
init_tracing();
|
||||||
|
|
||||||
|
// Parse CLI arguments and environment variables
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// Validate we have at least one listen address
|
||||||
|
if args.listen.is_empty() {
|
||||||
|
eprintln!("Error: At least one --listen address is required");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create shared application state
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
downstream_url: args.downstream.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build router with shared state
|
||||||
|
let app = Router::new()
|
||||||
|
.nest("/api", api_routes().fallback(api_404_handler))
|
||||||
|
.fallback(isr_handler)
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(RequestIdLayer::new(args.trust_request_id.clone()))
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
// Spawn a listener for each address
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
for listen_addr in &args.listen {
|
||||||
|
let app = app.clone();
|
||||||
|
let listen_addr = listen_addr.clone();
|
||||||
|
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
match listen_addr {
|
||||||
|
ListenAddr::Tcp(addr) => {
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr)
|
||||||
|
.await
|
||||||
|
.expect("Failed to bind TCP listener");
|
||||||
|
|
||||||
|
// Format as clickable URL
|
||||||
|
let url = if addr.is_ipv6() {
|
||||||
|
format!("http://[{}]:{}", addr.ip(), addr.port())
|
||||||
|
} else {
|
||||||
|
format!("http://{}:{}", addr.ip(), addr.port())
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(url, "Listening on TCP");
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("Server error on TCP listener");
|
||||||
|
}
|
||||||
|
ListenAddr::Unix(path) => {
|
||||||
|
// Remove existing socket file if it exists
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
|
let listener = tokio::net::UnixListener::bind(&path)
|
||||||
|
.expect("Failed to bind Unix socket listener");
|
||||||
|
|
||||||
|
tracing::info!(socket = %path.display(), "Listening on Unix socket");
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("Server error on Unix socket listener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all listeners (this will run forever unless interrupted)
|
||||||
|
for task in tasks {
|
||||||
|
task.await.expect("Listener task panicked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared application state
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
downstream_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom error type for proxy operations
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ProxyError {
|
||||||
|
/// Network error (connection failed, timeout, etc.)
|
||||||
|
Network(reqwest::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ProxyError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ProxyError::Network(e) => write!(f, "Network error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ProxyError {}
|
||||||
|
|
||||||
|
/// Check if a path represents a static asset that should be logged at TRACE level
|
||||||
|
fn is_static_asset(path: &str) -> bool {
|
||||||
|
path.starts_with("/node_modules/")
|
||||||
|
|| path.starts_with("/@") // Vite internals like /@vite/client, /@fs/, /@id/
|
||||||
|
|| path.starts_with("/.svelte-kit/")
|
||||||
|
|| path.starts_with("/.well-known/")
|
||||||
|
|| path.ends_with(".woff2")
|
||||||
|
|| path.ends_with(".woff")
|
||||||
|
|| path.ends_with(".ttf")
|
||||||
|
|| path.ends_with(".ico")
|
||||||
|
|| path.ends_with(".png")
|
||||||
|
|| path.ends_with(".jpg")
|
||||||
|
|| path.ends_with(".svg")
|
||||||
|
|| path.ends_with(".webp")
|
||||||
|
|| path.ends_with(".css")
|
||||||
|
|| path.ends_with(".js")
|
||||||
|
|| path.ends_with(".map")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a path represents a page route (heuristic: no file extension)
|
||||||
|
fn is_page_route(path: &str) -> bool {
|
||||||
|
!path.starts_with("/node_modules/")
|
||||||
|
&& !path.starts_with("/@")
|
||||||
|
&& !path.starts_with("/.svelte-kit/")
|
||||||
|
&& !path.contains('.') // Simple heuristic: no extension = likely a page
|
||||||
|
}
|
||||||
|
|
||||||
|
// API routes for data endpoints
|
||||||
|
fn api_routes() -> Router<Arc<AppState>> {
|
||||||
|
Router::new()
|
||||||
|
.route("/health", get(health_handler))
|
||||||
|
.route("/projects", get(projects_handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
async fn health_handler() -> impl IntoResponse {
|
||||||
|
(StatusCode::OK, "OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 404 fallback handler - catches unmatched /api/* routes
|
||||||
|
async fn api_404_handler(uri: axum::http::Uri) -> impl IntoResponse {
|
||||||
|
tracing::warn!(path = %uri.path(), "API route not found");
|
||||||
|
(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": "Not found",
|
||||||
|
"path": uri.path()
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project data structure
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct ProjectLink {
|
||||||
|
url: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
title: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct Project {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
#[serde(rename = "shortDescription")]
|
||||||
|
short_description: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
icon: Option<String>,
|
||||||
|
links: Vec<ProjectLink>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projects endpoint - returns hardcoded project data for now
|
||||||
|
async fn projects_handler() -> impl IntoResponse {
|
||||||
|
let projects = vec![
|
||||||
|
Project {
|
||||||
|
id: "1".to_string(),
|
||||||
|
name: "xevion.dev".to_string(),
|
||||||
|
short_description: "Personal portfolio with fuzzy tag discovery".to_string(),
|
||||||
|
icon: None,
|
||||||
|
links: vec![ProjectLink {
|
||||||
|
url: "https://github.com/Xevion/xevion.dev".to_string(),
|
||||||
|
title: Some("GitHub".to_string()),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
Project {
|
||||||
|
id: "2".to_string(),
|
||||||
|
name: "Contest".to_string(),
|
||||||
|
short_description: "Competitive programming problem archive".to_string(),
|
||||||
|
icon: None,
|
||||||
|
links: vec![
|
||||||
|
ProjectLink {
|
||||||
|
url: "https://github.com/Xevion/contest".to_string(),
|
||||||
|
title: Some("GitHub".to_string()),
|
||||||
|
},
|
||||||
|
ProjectLink {
|
||||||
|
url: "https://contest.xevion.dev".to_string(),
|
||||||
|
title: Some("Demo".to_string()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Json(projects)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISR handler - proxies to Bun SSR server
|
||||||
|
// This is the fallback for all routes not matched by /api/*
|
||||||
|
#[tracing::instrument(skip(state, req), fields(path = %req.uri().path()))]
|
||||||
|
async fn isr_handler(State(state): State<Arc<AppState>>, req: Request) -> Response {
|
||||||
|
let uri = req.uri();
|
||||||
|
let path = uri.path();
|
||||||
|
let query = uri.query().unwrap_or("");
|
||||||
|
|
||||||
|
// Check if API route somehow reached ISR handler (shouldn't happen)
|
||||||
|
if path.starts_with("/api/") {
|
||||||
|
tracing::error!("API request reached ISR handler - routing bug!");
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Internal routing error",
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build URL for Bun server
|
||||||
|
let bun_url = if query.is_empty() {
|
||||||
|
format!("{}{}", state.downstream_url, path)
|
||||||
|
} else {
|
||||||
|
format!("{}{}?{}", state.downstream_url, path, query)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track request timing
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
// TODO: Add ISR caching layer here (moka, singleflight, stale-while-revalidate)
|
||||||
|
// For now, just proxy directly to Bun
|
||||||
|
|
||||||
|
match proxy_to_bun(&bun_url, &state.downstream_url).await {
|
||||||
|
Ok((status, headers, body)) => {
|
||||||
|
let duration_ms = start.elapsed().as_millis() as u64;
|
||||||
|
let cache = "miss"; // Hardcoded for now, will change when caching is implemented
|
||||||
|
|
||||||
|
// Intelligent logging based on path type and status
|
||||||
|
let is_static = is_static_asset(path);
|
||||||
|
let is_page = is_page_route(path);
|
||||||
|
|
||||||
|
match (status.as_u16(), is_static, is_page) {
|
||||||
|
// Static assets - success at TRACE
|
||||||
|
(200..=299, true, _) => {
|
||||||
|
tracing::trace!(status = status.as_u16(), duration_ms, cache, "ISR request");
|
||||||
|
}
|
||||||
|
// Static assets - 404 at WARN
|
||||||
|
(404, true, _) => {
|
||||||
|
tracing::warn!(
|
||||||
|
status = status.as_u16(),
|
||||||
|
duration_ms,
|
||||||
|
cache,
|
||||||
|
"ISR request - missing asset"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Static assets - server error at ERROR
|
||||||
|
(500..=599, true, _) => {
|
||||||
|
tracing::error!(
|
||||||
|
status = status.as_u16(),
|
||||||
|
duration_ms,
|
||||||
|
cache,
|
||||||
|
"ISR request - server error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Page routes - success at DEBUG
|
||||||
|
(200..=299, _, true) => {
|
||||||
|
tracing::debug!(status = status.as_u16(), duration_ms, cache, "ISR request");
|
||||||
|
}
|
||||||
|
// Page routes - 404 silent (normal case for non-existent pages)
|
||||||
|
(404, _, true) => {}
|
||||||
|
// Page routes - server error at ERROR
|
||||||
|
(500..=599, _, _) => {
|
||||||
|
tracing::error!(
|
||||||
|
status = status.as_u16(),
|
||||||
|
duration_ms,
|
||||||
|
cache,
|
||||||
|
"ISR request - server error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Default fallback - DEBUG
|
||||||
|
_ => {
|
||||||
|
tracing::debug!(status = status.as_u16(), duration_ms, cache, "ISR request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward response
|
||||||
|
(status, headers, body).into_response()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let duration_ms = start.elapsed().as_millis() as u64;
|
||||||
|
tracing::error!(
|
||||||
|
error = %err,
|
||||||
|
url = %bun_url,
|
||||||
|
duration_ms,
|
||||||
|
"Failed to proxy to Bun"
|
||||||
|
);
|
||||||
|
(
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
format!("Failed to render page: {}", err),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy a request to the Bun SSR server, returning status, headers and body
|
||||||
|
async fn proxy_to_bun(
|
||||||
|
url: &str,
|
||||||
|
downstream_url: &str,
|
||||||
|
) -> Result<(StatusCode, HeaderMap, String), ProxyError> {
|
||||||
|
// Check if downstream is a Unix socket path
|
||||||
|
let client = if downstream_url.starts_with('/') || downstream_url.starts_with("./") {
|
||||||
|
// Unix socket
|
||||||
|
let path = PathBuf::from(downstream_url);
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.unix_socket(path)
|
||||||
|
.build()
|
||||||
|
.map_err(ProxyError::Network)?
|
||||||
|
} else {
|
||||||
|
// Regular HTTP
|
||||||
|
reqwest::Client::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client.get(url).send().await.map_err(ProxyError::Network)?;
|
||||||
|
|
||||||
|
// Extract status code
|
||||||
|
let status = StatusCode::from_u16(response.status().as_u16())
|
||||||
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
|
||||||
|
// Convert reqwest headers to axum HeaderMap
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
for (name, value) in response.headers() {
|
||||||
|
// Skip hop-by-hop headers and content-length (axum will recalculate it)
|
||||||
|
let name_str = name.as_str();
|
||||||
|
if name_str == "transfer-encoding"
|
||||||
|
|| name_str == "connection"
|
||||||
|
|| name_str == "content-length"
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(header_name) = axum::http::HeaderName::try_from(name.as_str()) {
|
||||||
|
if let Ok(header_value) = axum::http::HeaderValue::try_from(value.as_bytes()) {
|
||||||
|
headers.insert(header_name, header_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = response.text().await.map_err(ProxyError::Network)?;
|
||||||
|
Ok((status, headers, body))
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//! Request ID middleware for distributed tracing and correlation
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::Request,
|
||||||
|
http::HeaderName,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
use tower::{Layer, Service};
|
||||||
|
|
||||||
|
/// Layer that creates request ID spans for all requests
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RequestIdLayer {
|
||||||
|
/// Optional header name to trust for request IDs
|
||||||
|
trust_header: Option<HeaderName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestIdLayer {
|
||||||
|
/// Create a new request ID layer
|
||||||
|
pub fn new(trust_header: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
trust_header: trust_header.and_then(|h| h.parse().ok()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Layer<S> for RequestIdLayer {
|
||||||
|
type Service = RequestIdService<S>;
|
||||||
|
|
||||||
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
|
RequestIdService {
|
||||||
|
inner,
|
||||||
|
trust_header: self.trust_header.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service that extracts or generates request IDs and creates tracing spans
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RequestIdService<S> {
|
||||||
|
inner: S,
|
||||||
|
trust_header: Option<HeaderName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Service<Request> for RequestIdService<S>
|
||||||
|
where
|
||||||
|
S: Service<Request, Response = Response<Body>> + Send + 'static,
|
||||||
|
S::Future: Send + 'static,
|
||||||
|
{
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, req: Request) -> Self::Future {
|
||||||
|
// Extract or generate request ID
|
||||||
|
let req_id = self
|
||||||
|
.trust_header
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|header| req.headers().get(header))
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_else(|| ulid::Ulid::new().to_string());
|
||||||
|
|
||||||
|
// Create a tracing span for this request
|
||||||
|
let span = tracing::info_span!("request", req_id = %req_id);
|
||||||
|
let _enter = span.enter();
|
||||||
|
|
||||||
|
// Clone span for the future
|
||||||
|
let span_clone = span.clone();
|
||||||
|
|
||||||
|
// Call the inner service
|
||||||
|
let future = self.inner.call(req);
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
// Execute the future within the span
|
||||||
|
let _enter = span_clone.enter();
|
||||||
|
future.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
-1
@@ -7,6 +7,7 @@
|
|||||||
"@fontsource-variable/inter": "^5.1.0",
|
"@fontsource-variable/inter": "^5.1.0",
|
||||||
"@fontsource/hanken-grotesk": "^5.1.0",
|
"@fontsource/hanken-grotesk": "^5.1.0",
|
||||||
"@fontsource/schibsted-grotesk": "^5.2.8",
|
"@fontsource/schibsted-grotesk": "^5.2.8",
|
||||||
|
"@logtape/logtape": "^1.3.5",
|
||||||
"bits-ui": "^2.8.2",
|
"bits-ui": "^2.8.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
"@sveltejs/kit": "^2.21.0",
|
"@sveltejs/kit": "^2.21.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.13.1",
|
"eslint-plugin-svelte": "^3.13.1",
|
||||||
@@ -150,6 +153,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@logtape/logtape": ["@logtape/logtape@1.3.5", "", {}, "sha512-G+MxWB7Tbv/2764519+Cp6rKXUdRbe/GiRwTvlm/Wv/sNsiquRnx9Hzr9eXaIpAYLT4PrBlkthjJ4gmqdSPrFg=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.71.0", "", {}, "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw=="],
|
"@oxc-project/runtime": ["@oxc-project/runtime@0.71.0", "", {}, "sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw=="],
|
||||||
@@ -278,6 +283,8 @@
|
|||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="],
|
||||||
@@ -304,6 +311,8 @@
|
|||||||
|
|
||||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
||||||
@@ -326,6 +335,8 @@
|
|||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
@@ -334,6 +345,8 @@
|
|||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
|
|
||||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||||
@@ -354,10 +367,14 @@
|
|||||||
|
|
||||||
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
||||||
@@ -404,6 +421,8 @@
|
|||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="],
|
"globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="],
|
||||||
@@ -422,6 +441,8 @@
|
|||||||
|
|
||||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
@@ -542,6 +563,8 @@
|
|||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@oxc-project/runtime": "0.71.0", "@oxc-project/types": "0.71.0", "@rolldown/pluginutils": "1.0.0-beta.9-commit.d91dfb5", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-darwin-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9-commit.d91dfb5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g=="],
|
"rolldown": ["rolldown@1.0.0-beta.9-commit.d91dfb5", "", { "dependencies": { "@oxc-project/runtime": "0.71.0", "@oxc-project/types": "0.71.0", "@rolldown/pluginutils": "1.0.0-beta.9-commit.d91dfb5", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-darwin-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9-commit.d91dfb5", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9-commit.d91dfb5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g=="],
|
||||||
@@ -550,6 +573,8 @@
|
|||||||
|
|
||||||
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
|
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
|
||||||
|
|
||||||
|
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||||
|
|
||||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
@@ -560,15 +585,21 @@
|
|||||||
|
|
||||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||||
|
|
||||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||||
|
|
||||||
"svelte": ["svelte@5.46.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA=="],
|
"svelte": ["svelte@5.46.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA=="],
|
||||||
|
|
||||||
@@ -594,6 +625,8 @@
|
|||||||
|
|
||||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
|
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
@@ -606,6 +639,8 @@
|
|||||||
|
|
||||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||||
|
|
||||||
"unplugin-icons": ["unplugin-icons@22.5.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.0.2", "debug": "^4.4.3", "local-pkg": "^1.1.2", "unplugin": "^2.3.10" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0", "vue-template-compiler": "^2.6.12", "vue-template-es2015-compiler": "^1.9.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte", "vue-template-compiler", "vue-template-es2015-compiler"] }, "sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ=="],
|
"unplugin-icons": ["unplugin-icons@22.5.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.0.2", "debug": "^4.4.3", "local-pkg": "^1.1.2", "unplugin": "^2.3.10" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0", "vue-template-compiler": "^2.6.12", "vue-template-es2015-compiler": "^1.9.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte", "vue-template-compiler", "vue-template-es2015-compiler"] }, "sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ=="],
|
||||||
@@ -624,8 +659,16 @@
|
|||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||||
@@ -650,6 +693,8 @@
|
|||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
|
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
"eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
||||||
|
|
||||||
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@fontsource-variable/inter": "^5.1.0",
|
"@fontsource-variable/inter": "^5.1.0",
|
||||||
"@fontsource/hanken-grotesk": "^5.1.0",
|
"@fontsource/hanken-grotesk": "^5.1.0",
|
||||||
"@fontsource/schibsted-grotesk": "^5.2.8",
|
"@fontsource/schibsted-grotesk": "^5.2.8",
|
||||||
|
"@logtape/logtape": "^1.3.5",
|
||||||
"bits-ui": "^2.8.2",
|
"bits-ui": "^2.8.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
@@ -26,6 +27,8 @@
|
|||||||
"@sveltejs/kit": "^2.21.0",
|
"@sveltejs/kit": "^2.21.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.13.1",
|
"eslint-plugin-svelte": "^3.13.1",
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Handle, HandleServerError } from "@sveltejs/kit";
|
||||||
|
import { dev } from "$app/environment";
|
||||||
|
import { initLogger } from "$lib/logger";
|
||||||
|
import { getLogger } from "@logtape/logtape";
|
||||||
|
|
||||||
|
// Initialize logger on server startup
|
||||||
|
await initLogger();
|
||||||
|
|
||||||
|
const logger = getLogger(["ssr", "error"]);
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
// Handle DevTools request silently to prevent console.log spam
|
||||||
|
if (
|
||||||
|
dev &&
|
||||||
|
event.url.pathname === "/.well-known/appspecific/com.chrome.devtools.json"
|
||||||
|
) {
|
||||||
|
return new Response(undefined, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return await resolve(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleError: HandleServerError = async ({
|
||||||
|
error,
|
||||||
|
event,
|
||||||
|
status,
|
||||||
|
message,
|
||||||
|
}) => {
|
||||||
|
// Use structured logging via LogTape instead of console.error
|
||||||
|
logger.error(message, {
|
||||||
|
status,
|
||||||
|
method: event.request.method,
|
||||||
|
path: event.url.pathname,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: status === 404 ? "Not Found" : "Internal Error",
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { getLogger } from "@logtape/logtape";
|
||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
|
||||||
|
const logger = getLogger(["ssr", "lib", "api"]);
|
||||||
|
|
||||||
|
// Compute upstream configuration once at module load
|
||||||
|
const upstreamUrl = env.UPSTREAM_URL;
|
||||||
|
const isUnixSocket =
|
||||||
|
upstreamUrl?.startsWith("/") || upstreamUrl?.startsWith("./");
|
||||||
|
const baseUrl = isUnixSocket ? "http://localhost" : upstreamUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch utility for calling the Rust backend API.
|
||||||
|
* Automatically prefixes requests with the upstream URL from environment.
|
||||||
|
* Supports both HTTP URLs and Unix socket paths.
|
||||||
|
*
|
||||||
|
* Connection pooling and keep-alive are handled automatically by Bun.
|
||||||
|
* Default timeout is 30 seconds unless overridden via init.signal.
|
||||||
|
*/
|
||||||
|
export async function apiFetch<T>(
|
||||||
|
path: string,
|
||||||
|
init?: RequestInit,
|
||||||
|
): Promise<T> {
|
||||||
|
if (!upstreamUrl) {
|
||||||
|
logger.error("UPSTREAM_URL environment variable not set");
|
||||||
|
throw new Error("UPSTREAM_URL environment variable not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${baseUrl}${path}`;
|
||||||
|
const method = init?.method ?? "GET";
|
||||||
|
|
||||||
|
// Build fetch options with 30s default timeout and unix socket support
|
||||||
|
const fetchOptions: RequestInit & { unix?: string } = {
|
||||||
|
...init,
|
||||||
|
// Respect caller-provided signal, otherwise default to 30s timeout
|
||||||
|
signal: init?.signal ?? AbortSignal.timeout(30_000),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUnixSocket) {
|
||||||
|
fetchOptions.unix = upstreamUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("API request", {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
isUnixSocket,
|
||||||
|
upstreamUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.error("API request failed", {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
});
|
||||||
|
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
logger.debug("API response", { method, url, status: response.status });
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("API request exception", {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { configure, getConsoleSink, type LogRecord } from "@logtape/logtape";
|
||||||
|
|
||||||
|
interface RailwayLogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: string;
|
||||||
|
message: string;
|
||||||
|
target: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom formatter that outputs Railway-compatible JSON logs.
|
||||||
|
* Format: { timestamp, level, message, target, ...attributes }
|
||||||
|
*
|
||||||
|
* The target field is constructed from the logger category:
|
||||||
|
* - ["ssr"] -> "ssr"
|
||||||
|
* - ["ssr", "routes"] -> "ssr:routes"
|
||||||
|
* - ["ssr", "api", "auth"] -> "ssr:api:auth"
|
||||||
|
*/
|
||||||
|
function railwayFormatter(record: LogRecord): string {
|
||||||
|
const entry: RailwayLogEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: record.level.toLowerCase(),
|
||||||
|
message: record.message.join(" "),
|
||||||
|
target: record.category.join(":"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flatten properties to root level (custom attributes)
|
||||||
|
if (record.properties && Object.keys(record.properties).length > 0) {
|
||||||
|
Object.assign(entry, record.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(entry) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize LogTape with Railway-compatible JSON logging.
|
||||||
|
* Only outputs logs when LOG_JSON=true or LOG_JSON=1 is set.
|
||||||
|
* Safe to call multiple times (idempotent - will silently skip if already configured).
|
||||||
|
*/
|
||||||
|
export async function initLogger() {
|
||||||
|
const useJsonLogs =
|
||||||
|
process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!useJsonLogs) {
|
||||||
|
// In development, use default console logging with nice formatting
|
||||||
|
await configure({
|
||||||
|
sinks: {
|
||||||
|
console: getConsoleSink(),
|
||||||
|
},
|
||||||
|
filters: {},
|
||||||
|
loggers: [
|
||||||
|
{
|
||||||
|
category: ["logtape", "meta"],
|
||||||
|
lowestLevel: "warning",
|
||||||
|
sinks: ["console"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: [],
|
||||||
|
lowestLevel: "debug",
|
||||||
|
sinks: ["console"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In production/JSON mode, use Railway-compatible JSON formatter
|
||||||
|
await configure({
|
||||||
|
sinks: {
|
||||||
|
json: (record: LogRecord) => {
|
||||||
|
process.stdout.write(railwayFormatter(record));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters: {},
|
||||||
|
loggers: [
|
||||||
|
// Meta logger for LogTape's internal messages
|
||||||
|
{
|
||||||
|
category: ["logtape", "meta"],
|
||||||
|
lowestLevel: "warning",
|
||||||
|
sinks: ["json"],
|
||||||
|
},
|
||||||
|
// SSR application logs
|
||||||
|
{
|
||||||
|
category: ["ssr"],
|
||||||
|
lowestLevel: "info",
|
||||||
|
sinks: ["json"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Already configured (HMR in dev mode), silently ignore
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("Already configured")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,12 @@
|
|||||||
import IconSimpleIconsDiscord from "~icons/simple-icons/discord";
|
import IconSimpleIconsDiscord from "~icons/simple-icons/discord";
|
||||||
import MaterialSymbolsMailRounded from "~icons/material-symbols/mail-rounded";
|
import MaterialSymbolsMailRounded from "~icons/material-symbols/mail-rounded";
|
||||||
import MaterialSymbolsVpnKey from "~icons/material-symbols/vpn-key";
|
import MaterialSymbolsVpnKey from "~icons/material-symbols/vpn-key";
|
||||||
// import IconLucideRss from "~icons/lucide/rss";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppWrapper class="overflow-x-hidden font-schibsted">
|
<AppWrapper class="overflow-x-hidden font-schibsted">
|
||||||
<!-- Top Navigation Bar -->
|
<!-- Top Navigation Bar -->
|
||||||
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9">
|
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9">
|
||||||
<div class="flex gap-4 items-center">
|
<!-- <div class="flex gap-4 items-center"></div> -->
|
||||||
<!-- <a href="/rss" class="text-zinc-400 hover:text-zinc-200">
|
|
||||||
<IconLucideRss class="size-5" />
|
|
||||||
</a> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { apiFetch } from "$lib/api";
|
||||||
|
|
||||||
interface ProjectLink {
|
interface ProjectLink {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -14,8 +15,8 @@ export interface Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
// TODO: Fetch from Rust backend API
|
const projects = await apiFetch<Project[]>("/api/projects");
|
||||||
return {
|
return {
|
||||||
projects: [] as Project[],
|
projects,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const config = {
|
|||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
out: "build",
|
out: "build",
|
||||||
precompress: false,
|
precompress: false,
|
||||||
|
serveAssets: false,
|
||||||
}),
|
}),
|
||||||
alias: {
|
alias: {
|
||||||
$components: "src/lib/components",
|
$components: "src/lib/components",
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import type { Plugin, ViteDevServer } from "vite";
|
||||||
|
import { configure, getLogger, type LogRecord } from "@logtape/logtape";
|
||||||
|
|
||||||
|
interface RailwayLogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: string;
|
||||||
|
message: string;
|
||||||
|
target: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Railway-compatible JSON formatter for Vite logs
|
||||||
|
*/
|
||||||
|
function railwayFormatter(record: LogRecord): string {
|
||||||
|
const entry: RailwayLogEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: record.level.toLowerCase(),
|
||||||
|
message: record.message.join(" "),
|
||||||
|
target: "vite",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flatten properties to root level
|
||||||
|
if (record.properties && Object.keys(record.properties).length > 0) {
|
||||||
|
Object.assign(entry, record.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(entry) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip ANSI escape codes from strings
|
||||||
|
function stripAnsi(str: string): string {
|
||||||
|
return str.replace(/\u001b\[[0-9;]*m/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonLogger(): Plugin {
|
||||||
|
const useJsonLogs =
|
||||||
|
process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
|
||||||
|
|
||||||
|
// If JSON logging is disabled, return a minimal plugin that does nothing
|
||||||
|
if (!useJsonLogs) {
|
||||||
|
return {
|
||||||
|
name: "vite-plugin-json-logger",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure LogTape for Vite plugin logging
|
||||||
|
let loggerConfigured = false;
|
||||||
|
const configureLogger = async () => {
|
||||||
|
if (loggerConfigured) return;
|
||||||
|
await configure({
|
||||||
|
sinks: {
|
||||||
|
json: (record: LogRecord) => {
|
||||||
|
process.stdout.write(railwayFormatter(record));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filters: {},
|
||||||
|
loggers: [
|
||||||
|
// Suppress LogTape meta logger info messages
|
||||||
|
{
|
||||||
|
category: ["logtape", "meta"],
|
||||||
|
lowestLevel: "warning",
|
||||||
|
sinks: ["json"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: ["vite"],
|
||||||
|
lowestLevel: "debug",
|
||||||
|
sinks: ["json"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
loggerConfigured = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let server: ViteDevServer;
|
||||||
|
const ignoredMessages = new Set(["press h + enter to show help", "ready in"]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "vite-plugin-json-logger",
|
||||||
|
|
||||||
|
async config() {
|
||||||
|
await configureLogger();
|
||||||
|
const logger = getLogger(["vite"]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customLogger: {
|
||||||
|
info(msg: string) {
|
||||||
|
const cleaned = stripAnsi(msg);
|
||||||
|
// Filter out noise
|
||||||
|
if (
|
||||||
|
!cleaned ||
|
||||||
|
ignoredMessages.has(cleaned) ||
|
||||||
|
cleaned.includes("VITE v")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info(cleaned);
|
||||||
|
},
|
||||||
|
warn(msg: string) {
|
||||||
|
const cleaned = stripAnsi(msg);
|
||||||
|
if (cleaned) {
|
||||||
|
logger.warn(cleaned);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error(msg: string) {
|
||||||
|
const cleaned = stripAnsi(msg);
|
||||||
|
if (cleaned) {
|
||||||
|
logger.error(cleaned);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearScreen() {
|
||||||
|
// No-op since clearScreen is already false
|
||||||
|
},
|
||||||
|
hasErrorLogged() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
hasWarned: false,
|
||||||
|
warnOnce(msg: string) {
|
||||||
|
this.warn(msg);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
configureServer(s) {
|
||||||
|
server = s;
|
||||||
|
const logger = getLogger(["vite"]);
|
||||||
|
|
||||||
|
// Override the default URL printing
|
||||||
|
const originalPrintUrls = server.printUrls;
|
||||||
|
server.printUrls = () => {
|
||||||
|
const urls = server.resolvedUrls;
|
||||||
|
if (urls) {
|
||||||
|
logger.info("dev server running", {
|
||||||
|
local: urls.local,
|
||||||
|
network: urls.network,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen to server events
|
||||||
|
server.httpServer?.once("listening", () => {
|
||||||
|
logger.info("server listening");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.ws.on("connection", () => {
|
||||||
|
logger.info("client connected");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleHotUpdate({ file, modules }) {
|
||||||
|
const logger = getLogger(["vite"]);
|
||||||
|
logger.info("hmr update", {
|
||||||
|
file: file.replace(process.cwd(), ""),
|
||||||
|
modules: modules.length,
|
||||||
|
});
|
||||||
|
return modules;
|
||||||
|
},
|
||||||
|
|
||||||
|
buildStart() {
|
||||||
|
const logger = getLogger(["vite"]);
|
||||||
|
logger.info("build started");
|
||||||
|
},
|
||||||
|
|
||||||
|
buildEnd() {
|
||||||
|
const logger = getLogger(["vite"]);
|
||||||
|
logger.info("build ended");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+8
-1
@@ -2,7 +2,14 @@ import { sveltekit } from "@sveltejs/kit/vite";
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import Icons from "unplugin-icons/vite";
|
import Icons from "unplugin-icons/vite";
|
||||||
|
import { jsonLogger } from "./vite-plugin-json-logger";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit(), Icons({ compiler: "svelte" })],
|
plugins: [
|
||||||
|
jsonLogger(),
|
||||||
|
tailwindcss(),
|
||||||
|
sveltekit(),
|
||||||
|
Icons({ compiler: "svelte" }),
|
||||||
|
],
|
||||||
|
clearScreen: false,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user