From 003d8840fd150b39c7a59d624bbc8abeea24959f Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Wed, 25 Jun 2025 06:36:15 +0800 Subject: [PATCH] fix: fix dotenv loader inconsistent and many ui issues --- apps/recorder/.env.development | 36 ++--- apps/recorder/.env.production.example | 27 ++-- apps/recorder/recorder.config.toml | 24 +-- apps/recorder/src/app/config/mod.rs | 148 +++++++++++++++++- apps/recorder/src/extract/origin/mod.rs | 30 +++- ...add_subscription_id_to_subscriber_tasks.rs | 62 ++++++++ apps/recorder/src/migrations/mod.rs | 2 + apps/recorder/src/models/episodes.rs | 2 +- apps/recorder/src/models/feeds/registry.rs | 5 +- apps/recorder/src/models/subscriber_tasks.rs | 17 ++ apps/recorder/src/models/subscriptions/mod.rs | 10 ++ apps/recorder/src/storage/client.rs | 2 +- apps/webui/.env.development | 16 +- apps/webui/.env.production.example | 12 +- apps/webui/rsbuild.config.ts | 30 ++-- .../components/ui/dropdown-menu-actions.tsx | 67 +++++--- .../domains/recorder/schema/subscriptions.ts | 7 + apps/webui/src/infra/auth/defs.ts | 2 +- apps/webui/src/infra/auth/oidc/config.ts | 12 +- apps/webui/src/infra/graphql/gql/gql.ts | 6 +- apps/webui/src/infra/graphql/gql/graphql.ts | 16 +- .../routes/_app/subscriptions/detail.$id.tsx | 72 +++++++-- .../routes/_app/tasks/-pretty-task-type.ts | 3 + .../routes/_app/tasks/detail.$id.tsx | 5 +- .../presentation/routes/_app/tasks/manage.tsx | 5 +- packages/fetch/src/client/core.rs | 4 +- packages/fetch/src/client/proxy.rs | 6 +- packages/util/src/lib.rs | 2 - packages/util/src/loose.rs | 19 --- 29 files changed, 481 insertions(+), 168 deletions(-) create mode 100644 apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs create mode 100644 apps/webui/src/presentation/routes/_app/tasks/-pretty-task-type.ts delete mode 100644 packages/util/src/loose.rs diff --git a/apps/recorder/.env.development b/apps/recorder/.env.development index 84b53c6..5455a2f 100644 --- a/apps/recorder/.env.development +++ b/apps/recorder/.env.development @@ -1,18 +1,18 @@ -HOST="konobangu.com" -DATABASE_URL = "postgres://konobangu:konobangu@localhost:5432/konobangu" -STORAGE_DATA_DIR = "./data" -AUTH_TYPE = "basic" # or oidc -BASIC_USER = "konobangu" -BASIC_PASSWORD = "konobangu" -LOG_LEVEL = "debug" -# OIDC_ISSUER="https://auth.logto.io/oidc" -# OIDC_AUDIENCE = "https://konobangu.com/api" -# OIDC_CLIENT_ID = "client_id" -# OIDC_CLIENT_SECRET = "client_secret" # optional -# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" -# OIDC_EXTRA_CLAIM_KEY = "" -# OIDC_EXTRA_CLAIM_VALUE = "" -MIKAN_PROXY = "http://127.0.0.1:8899" -# MIKAN_PROXY_AUTH_HEADER = "" -# MIKAN_NO_PROXY = "" -MIKAN_PROXY_ACCEPT_INVALID_CERTS = true +LOGGER__LEVEL = "debug" + +DATABASE__URI = "postgres://konobangu:konobangu@localhost:5432/konobangu" + +AUTH__AUTH_TYPE = "basic" +AUTH__BASIC_USER = "konobangu" +AUTH__BASIC_PASSWORD = "konobangu" + +# AUTH__OIDC_ISSUER = "https://auth.logto.io/oidc" +# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api" +# AUTH__OIDC_CLIENT_ID = "client_id" +# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional +# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" +# AUTH__OIDC_EXTRA_CLAIM_KEY = "" +# AUTH__OIDC_EXTRA_CLAIM_VALUE = "" + +MIKAN__HTTP_CLIENT__PROXY__ACCEPT_INVALID_CERTS = true +MIKAN__HTTP_CLIENT__PROXY__SERVER = "http://127.0.0.1:8899" diff --git a/apps/recorder/.env.production.example b/apps/recorder/.env.production.example index d70d99d..aefb75e 100644 --- a/apps/recorder/.env.production.example +++ b/apps/recorder/.env.production.example @@ -1,14 +1,15 @@ HOST="konobangu.com" -DATABASE_URL = "postgres://konobangu:konobangu@localhost:5432/konobangu" -STORAGE_DATA_DIR = "./data" -AUTH_TYPE = "basic" # or oidc -BASIC_USER = "konobangu" -BASIC_PASSWORD = "konobangu" -LOG_LEVEL = "info" -# OIDC_ISSUER="https://auth.logto.io/oidc" -# OIDC_AUDIENCE = "https://konobangu.com/api" -# OIDC_CLIENT_ID = "client_id" -# OIDC_CLIENT_SECRET = "client_secret" # optional -# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" -# OIDC_EXTRA_CLAIM_KEY = "" -# OIDC_EXTRA_CLAIM_VALUE = "" + +DATABASE__URI = "postgres://konobangu:konobangu@localhost:5432/konobangu" + +AUTH__AUTH_TYPE = "basic" # or oidc +AUTH__BASIC_USER = "konobangu" +AUTH__BASIC_PASSWORD = "konobangu" + +# AUTH__OIDC_ISSUER="https://auth.logto.io/oidc" +# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api" +# AUTH__OIDC_CLIENT_ID = "client_id" +# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional +# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" +# AUTH__OIDC_EXTRA_CLAIM_KEY = "" +# AUTH__OIDC_EXTRA_CLAIM_VALUE = "" diff --git a/apps/recorder/recorder.config.toml b/apps/recorder/recorder.config.toml index 54c695d..ede246c 100644 --- a/apps/recorder/recorder.config.toml +++ b/apps/recorder/recorder.config.toml @@ -4,7 +4,7 @@ enable = true # Enable pretty backtrace (sets RUST_BACKTRACE=1) pretty_backtrace = true -level = '{{ get_env(name="LOG_LEVEL", default="info") }}' +level = "info" # Log level, options: trace, debug, info, warn or error. # Define the logging format. options: compact, pretty or Json format = "compact" @@ -77,7 +77,7 @@ max_connections = 10 auto_migrate = true [storage] -data_dir = '{{ get_env(name="STORAGE_DATA_DIR", default="./data") }}' +data_dir = './data' [mikan] base_url = "https://mikanani.me/" @@ -89,26 +89,6 @@ leaky_bucket_initial_tokens = 1 leaky_bucket_refill_tokens = 1 leaky_bucket_refill_interval = 500 - -[mikan.http_client.proxy] -server = '{{ get_env(name="MIKAN_PROXY", default = "") }}' -auth_header = '{{ get_env(name="MIKAN_PROXY_AUTH_HEADER", default = "") }}' -no_proxy = '{{ get_env(name="MIKAN_NO_PROXY", default = "") }}' -accept_invalid_certs = '{{ get_env(name="MIKAN_PROXY_ACCEPT_INVALID_CERTS", default = "false") }}' - - -[auth] -auth_type = '{{ get_env(name="AUTH_TYPE", default = "basic") }}' -basic_user = '{{ get_env(name="BASIC_USER", default = "konobangu") }}' -basic_password = '{{ get_env(name="BASIC_PASSWORD", default = "konobangu") }}' -oidc_issuer = '{{ get_env(name="OIDC_ISSUER", default = "") }}' -oidc_audience = '{{ get_env(name="OIDC_AUDIENCE", default = "") }}' -oidc_client_id = '{{ get_env(name="OIDC_CLIENT_ID", default = "") }}' -oidc_client_secret = '{{ get_env(name="OIDC_CLIENT_SECRET", default = "") }}' -oidc_extra_scopes = '{{ get_env(name="OIDC_EXTRA_SCOPES", default = "") }}' -oidc_extra_claim_key = '{{ get_env(name="OIDC_EXTRA_CLAIM_KEY", default = "") }}' -oidc_extra_claim_value = '{{ get_env(name="OIDC_EXTRA_CLAIM_VALUE", default = "") }}' - [graphql] # depth_limit = inf # complexity_limit = inf diff --git a/apps/recorder/src/app/config/mod.rs b/apps/recorder/src/app/config/mod.rs index 88a4b39..69fb004 100644 --- a/apps/recorder/src/app/config/mod.rs +++ b/apps/recorder/src/app/config/mod.rs @@ -1,8 +1,13 @@ -use std::{fs, path::Path, str}; +use std::{ + collections::HashMap, + fs, + path::Path, + str::{self, FromStr}, +}; use figment::{ Figment, Provider, - providers::{Format, Json, Toml, Yaml}, + providers::{Env, Format, Json, Toml, Yaml}, }; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -65,6 +70,102 @@ impl AppConfig { Toml::string(DEFAULT_CONFIG_MIXIN) } + fn build_enhanced_tera_engine() -> tera::Tera { + let mut tera = tera::Tera::default(); + tera.register_filter( + "cast_to", + |value: &tera::Value, + args: &HashMap| + -> tera::Result { + let target_type = args + .get("type") + .and_then(|v| v.as_str()) + .ok_or_else(|| tera::Error::msg("invalid target type: should be string"))?; + + let target_type = TeraCastToFilterType::from_str(target_type) + .map_err(|e| tera::Error::msg(format!("invalid target type: {e}")))?; + + let input_str = value.as_str().unwrap_or(""); + + match target_type { + TeraCastToFilterType::Boolean => { + let is_true = matches!(input_str.to_lowercase().as_str(), "true" | "1"); + let is_false = matches!(input_str.to_lowercase().as_str(), "false" | "0"); + if is_true { + Ok(tera::Value::Bool(true)) + } else if is_false { + Ok(tera::Value::Bool(false)) + } else { + Err(tera::Error::msg( + "target type is bool but value is not a boolean like true, false, \ + 1, 0", + )) + } + } + TeraCastToFilterType::Integer => { + let parsed = input_str.parse::().map_err(|e| { + tera::Error::call_filter("invalid integer".to_string(), e) + })?; + Ok(tera::Value::Number(serde_json::Number::from(parsed))) + } + TeraCastToFilterType::Unsigned => { + let parsed = input_str.parse::().map_err(|e| { + tera::Error::call_filter("invalid unsigned integer".to_string(), e) + })?; + Ok(tera::Value::Number(serde_json::Number::from(parsed))) + } + TeraCastToFilterType::Float => { + let parsed = input_str.parse::().map_err(|e| { + tera::Error::call_filter("invalid float".to_string(), e) + })?; + Ok(tera::Value::Number( + serde_json::Number::from_f64(parsed).ok_or_else(|| { + tera::Error::msg("failed to convert f64 to serde_json::Number") + })?, + )) + } + TeraCastToFilterType::String => Ok(tera::Value::String(input_str.to_string())), + TeraCastToFilterType::Null => Ok(tera::Value::Null), + } + }, + ); + tera.register_filter( + "try_auto_cast", + |value: &tera::Value, + _args: &HashMap| + -> tera::Result { + let input_str = value.as_str().unwrap_or(""); + + if input_str == "null" { + return Ok(tera::Value::Null); + } + + if matches!(input_str, "true" | "false") { + return Ok(tera::Value::Bool(input_str == "true")); + } + + if let Ok(parsed) = input_str.parse::() { + return Ok(tera::Value::Number(serde_json::Number::from(parsed))); + } + + if let Ok(parsed) = input_str.parse::() { + return Ok(tera::Value::Number(serde_json::Number::from(parsed))); + } + + if let Ok(parsed) = input_str.parse::() { + return Ok(tera::Value::Number( + serde_json::Number::from_f64(parsed).ok_or_else(|| { + tera::Error::msg("failed to convert f64 to serde_json::Number") + })?, + )); + } + + Ok(tera::Value::String(input_str.to_string())) + }, + ); + tera + } + pub fn merge_provider_from_file( fig: Figment, filepath: impl AsRef, @@ -72,11 +173,9 @@ impl AppConfig { ) -> RecorderResult { let content = fs::read_to_string(filepath)?; - let rendered = tera::Tera::one_off( - &content, - &tera::Context::from_value(serde_json::json!({}))?, - false, - )?; + let mut tera_engine = AppConfig::build_enhanced_tera_engine(); + let rendered = + tera_engine.render_str(&content, &tera::Context::from_value(serde_json::json!({}))?)?; Ok(match ext { ".toml" => fig.merge(Toml::string(&rendered)), @@ -182,8 +281,43 @@ impl AppConfig { } } + fig = fig.merge(Env::prefixed("").split("__").lowercase(true)); + let app_config: AppConfig = fig.extract()?; Ok(app_config) } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum TeraCastToFilterType { + #[serde(alias = "str")] + String, + #[serde(alias = "bool")] + Boolean, + #[serde(alias = "int")] + Integer, + #[serde(alias = "uint")] + Unsigned, + #[serde(alias = "float")] + Float, + #[serde(alias = "null")] + Null, +} + +impl FromStr for TeraCastToFilterType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "string" | "str" => Ok(TeraCastToFilterType::String), + "boolean" | "bool" => Ok(TeraCastToFilterType::Boolean), + "integer" | "int" => Ok(TeraCastToFilterType::Integer), + "unsigned" | "uint" => Ok(TeraCastToFilterType::Unsigned), + "float" => Ok(TeraCastToFilterType::Float), + "null" => Ok(TeraCastToFilterType::Null), + _ => Err(format!("invalid target type: {s}")), + } + } +} diff --git a/apps/recorder/src/extract/origin/mod.rs b/apps/recorder/src/extract/origin/mod.rs index b0739dd..fbb239c 100644 --- a/apps/recorder/src/extract/origin/mod.rs +++ b/apps/recorder/src/extract/origin/mod.rs @@ -47,8 +47,27 @@ impl<'a> EpisodeComp<'a> { Ok((input, f32::round(num) as i32)) } + fn parse_ep_special_num(input: &'a str) -> IResult<&'a str, i32> { + terminated( + alt(( + value(0, tag_no_case("ova")), + value(0, tag_no_case("oad")), + value(0, tag_no_case("sp")), + value(0, tag_no_case("ex")), + )), + (space0, opt(parse_int::)), + ) + .parse(input) + } + fn parse_ep_num(input: &'a str) -> IResult<&'a str, i32> { - alt((parse_int::, Self::parse_ep_round_num, ZhNum::parse_int)).parse(input) + alt(( + parse_int::, + Self::parse_ep_round_num, + ZhNum::parse_int, + Self::parse_ep_special_num, + )) + .parse(input) } fn parse_ep_nums_core(input: &'a str) -> IResult<&'a str, (i32, Option)> { @@ -175,8 +194,13 @@ impl<'a> std::fmt::Debug for MoiveComp<'a> { impl<'a> OriginCompTrait<'a> for MoiveComp<'a> { #[cfg_attr(debug_assertions, instrument(level = Level::TRACE, ret, err(level=Level::TRACE), "MoiveComp::parse_comp"))] fn parse_comp(input: &'a str) -> IResult<&'a str, Self> { - let (input, source) = - alt((tag("剧场版"), tag("电影"), tag_no_case("movie"))).parse(input)?; + let (input, source) = alt(( + tag("剧场版"), + tag("电影"), + tag_no_case("movie"), + tag_no_case("film"), + )) + .parse(input)?; Ok(( input, Self { diff --git a/apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs b/apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs new file mode 100644 index 0000000..44347c4 --- /dev/null +++ b/apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs @@ -0,0 +1,62 @@ +use async_trait::async_trait; +use sea_orm_migration::prelude::*; + +use crate::task::SUBSCRIBER_TASK_APALIS_NAME; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE VIEW subscriber_tasks AS +SELECT + job, + job_type, + status, + (job ->> 'subscriber_id'::text)::integer AS subscriber_id, + job ->> 'task_type'::text AS task_type, + id, + attempts, + max_attempts, + run_at, + last_error, + lock_at, + lock_by, + done_at, + priority, + (job ->> 'subscription_id'::text)::integer AS subscription_id +FROM apalis.jobs +WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}' +AND jsonb_path_exists(job, '$.subscriber_id ? (@.type() == "number")') +AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"#, + )) + .await?; + + db.execute_unprepared(&format!( + r#"CREATE INDEX IF NOT EXISTS idx_apalis_jobs_subscription_id + ON apalis.jobs (((job -> 'subscription_id')::integer)) + WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}' + AND jsonb_path_exists(job, '$.subscription_id ? (@.type() == "number")') + AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"# + )) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + r#"DROP INDEX IF EXISTS idx_apalis_jobs_subscription_id + ON apalis.jobs"#, + ) + .await?; + + Ok(()) + } +} diff --git a/apps/recorder/src/migrations/mod.rs b/apps/recorder/src/migrations/mod.rs index bcda402..fe19430 100644 --- a/apps/recorder/src/migrations/mod.rs +++ b/apps/recorder/src/migrations/mod.rs @@ -10,6 +10,7 @@ pub mod m20250501_021523_credential_3rd; pub mod m20250520_021135_subscriber_tasks; pub mod m20250622_015618_feeds; pub mod m20250622_020819_bangumi_and_episode_type; +pub mod m20250625_060701_add_subscription_id_to_subscriber_tasks; pub struct Migrator; @@ -24,6 +25,7 @@ impl MigratorTrait for Migrator { Box::new(m20250520_021135_subscriber_tasks::Migration), Box::new(m20250622_015618_feeds::Migration), Box::new(m20250622_020819_bangumi_and_episode_type::Migration), + Box::new(m20250625_060701_add_subscription_id_to_subscriber_tasks::Migration), ] } } diff --git a/apps/recorder/src/models/episodes.rs b/apps/recorder/src/models/episodes.rs index 553ea24..7a20096 100644 --- a/apps/recorder/src/models/episodes.rs +++ b/apps/recorder/src/models/episodes.rs @@ -129,7 +129,7 @@ pub enum RelatedEntity { } impl ActiveModel { - #[tracing::instrument(err, skip(ctx), fields(bangumi_id = ?bangumi.id, mikan_episode_id = ?episode.mikan_episode_id))] + #[tracing::instrument(err, skip_all, fields(bangumi_id = ?bangumi.id, mikan_episode_id = ?episode.mikan_episode_id))] pub fn from_mikan_bangumi_and_episode_meta( ctx: &dyn AppContextTrait, bangumi: &bangumi::Model, diff --git a/apps/recorder/src/models/feeds/registry.rs b/apps/recorder/src/models/feeds/registry.rs index 68ed7e1..75d1254 100644 --- a/apps/recorder/src/models/feeds/registry.rs +++ b/apps/recorder/src/models/feeds/registry.rs @@ -1,5 +1,7 @@ use rss::Channel; -use sea_orm::{ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait}; +use sea_orm::{ + ColumnTrait, EntityTrait, JoinType, Order, QueryFilter, QueryOrder, QuerySelect, RelationTrait, +}; use url::Url; use crate::{ @@ -37,6 +39,7 @@ impl Feed { subscription_episode::Relation::Subscription.def(), ) .filter(subscriptions::Column::Id.eq(subscription_id)) + .order_by(episodes::Column::EnclosurePubDate, Order::Desc) .all(db) .await?; (subscription, episodes) diff --git a/apps/recorder/src/models/subscriber_tasks.rs b/apps/recorder/src/models/subscriber_tasks.rs index d1dd7b8..fa32e82 100644 --- a/apps/recorder/src/models/subscriber_tasks.rs +++ b/apps/recorder/src/models/subscriber_tasks.rs @@ -29,6 +29,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: String, pub subscriber_id: i32, + pub subscription_id: Option, pub job: SubscriberTask, pub task_type: SubscriberTaskType, pub status: SubscriberTaskStatus, @@ -52,6 +53,14 @@ pub enum Relation { on_delete = "Cascade" )] Subscriber, + #[sea_orm( + belongs_to = "super::subscriptions::Entity", + from = "Column::SubscriptionId", + to = "super::subscriptions::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Subscription, } impl Related for Entity { @@ -60,10 +69,18 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscription.def() + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { #[sea_orm(entity = "super::subscribers::Entity")] Subscriber, + #[sea_orm(entity = "super::subscriptions::Entity")] + Subscription, } #[async_trait] diff --git a/apps/recorder/src/models/subscriptions/mod.rs b/apps/recorder/src/models/subscriptions/mod.rs index 565f1d4..2e4e5ee 100644 --- a/apps/recorder/src/models/subscriptions/mod.rs +++ b/apps/recorder/src/models/subscriptions/mod.rs @@ -61,6 +61,8 @@ pub enum Relation { Credential3rd, #[sea_orm(has_many = "super::feeds::Entity")] Feed, + #[sea_orm(has_many = "super::subscriber_tasks::Entity")] + SubscriberTask, } impl Related for Entity { @@ -121,6 +123,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::SubscriberTask.def() + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { #[sea_orm(entity = "super::subscribers::Entity")] @@ -137,6 +145,8 @@ pub enum RelatedEntity { Credential3rd, #[sea_orm(entity = "super::feeds::Entity")] Feed, + #[sea_orm(entity = "super::subscriber_tasks::Entity")] + SubscriberTask, } #[async_trait] diff --git a/apps/recorder/src/storage/client.rs b/apps/recorder/src/storage/client.rs index 1a0f129..1da2118 100644 --- a/apps/recorder/src/storage/client.rs +++ b/apps/recorder/src/storage/client.rs @@ -209,7 +209,7 @@ impl StorageService { lister.try_collect().await } - #[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range, accept = ?accept))] + #[instrument(skip_all, err, fields(storage_path = %storage_path.as_ref(), range = ?range, accept = accept.to_string()))] pub async fn serve_optimized_image( &self, storage_path: impl AsRef, diff --git a/apps/webui/.env.development b/apps/webui/.env.development index 25d335c..d732181 100644 --- a/apps/webui/.env.development +++ b/apps/webui/.env.development @@ -1,8 +1,8 @@ -AUTH_TYPE = "basic" # or oidc -BASIC_USER = "konobangu" -BASIC_PASSWORD = "konobangu" -# OIDC_ISSUER="https://auth.logto.io/oidc" -# OIDC_AUDIENCE = "https://konobangu.com/api" -# OIDC_CLIENT_ID = "client_id" -# OIDC_CLIENT_SECRET = "client_secret" # optional -# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" +AUTH__AUTH_TYPE = "basic" # or oidc +AUTH__BASIC_USER = "konobangu" +AUTH__BASIC_PASSWORD = "konobangu" +# AUTH__OIDC_ISSUER="https://auth.logto.io/oidc" +# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api" +# AUTH__OIDC_CLIENT_ID = "client_id" +# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional +# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" diff --git a/apps/webui/.env.production.example b/apps/webui/.env.production.example index 85fb74b..414ee4b 100644 --- a/apps/webui/.env.production.example +++ b/apps/webui/.env.production.example @@ -1,6 +1,6 @@ -AUTH_TYPE = "basic" # or oidc -# OIDC_ISSUER="https://auth.logto.io/oidc" -# OIDC_AUDIENCE = "https://konobangu.com/api" -# OIDC_CLIENT_ID = "client_id" -# OIDC_CLIENT_SECRET = "client_secret" # optional -# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" +AUTH__AUTH_TYPE = "basic" # or oidc +# AUTH__OIDC_ISSUER="https://auth.logto.io/oidc" +# AUTH__OIDC_AUDIENCE = "https://konobangu.com/api" +# AUTH__OIDC_CLIENT_ID = "client_id" +# AUTH__OIDC_CLIENT_SECRET = "client_secret" # optional +# AUTH__OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" diff --git a/apps/webui/rsbuild.config.ts b/apps/webui/rsbuild.config.ts index 8252ac2..de65334 100644 --- a/apps/webui/rsbuild.config.ts +++ b/apps/webui/rsbuild.config.ts @@ -20,15 +20,23 @@ export default defineConfig({ index: './src/main.tsx', }, define: { - 'process.env.AUTH_TYPE': JSON.stringify(process.env.AUTH_TYPE), - 'process.env.OIDC_CLIENT_ID': JSON.stringify(process.env.OIDC_CLIENT_ID), - 'process.env.OIDC_CLIENT_SECRET': JSON.stringify( - process.env.OIDC_CLIENT_SECRET + 'process.env.AUTH__AUTH_TYPE': JSON.stringify( + process.env.AUTH__AUTH_TYPE ), - 'process.env.OIDC_ISSUER': JSON.stringify(process.env.OIDC_ISSUER), - 'process.env.OIDC_AUDIENCE': JSON.stringify(process.env.OIDC_AUDIENCE), - 'process.env.OIDC_EXTRA_SCOPES': JSON.stringify( - process.env.OIDC_EXTRA_SCOPES + 'process.env.AUTH__OIDC_CLIENT_ID': JSON.stringify( + process.env.AUTH__OIDC_CLIENT_ID + ), + 'process.env.AUTH__OIDC_CLIENT_SECRET': JSON.stringify( + process.env.AUTH__OIDC_CLIENT_SECRET + ), + 'process.env.AUTH__OIDC_ISSUER': JSON.stringify( + process.env.AUTH__OIDC_ISSUER + ), + 'process.env.AUTH__OIDC_AUDIENCE': JSON.stringify( + process.env.AUTH__OIDC_AUDIENCE + ), + 'process.env.AUTH__OIDC_EXTRA_SCOPES': JSON.stringify( + process.env.AUTH__OIDC_EXTRA_SCOPES ), }, }, @@ -39,7 +47,7 @@ export default defineConfig({ setupMiddlewares: [ (middlewares) => { middlewares.unshift((req, res, next) => { - if (process.env.AUTH_TYPE === 'basic') { + if (process.env.AUTH__AUTH_TYPE === 'basic') { res.setHeader('WWW-Authenticate', 'Basic realm="konobangu"'); const authorization = @@ -49,8 +57,8 @@ export default defineConfig({ .split(':'); if ( - user !== process.env.BASIC_USER || - password !== process.env.BASIC_PASSWORD + user !== process.env.AUTH__BASIC_USER || + password !== process.env.AUTH__BASIC_PASSWORD ) { res.statusCode = 401; res.write('Unauthorized'); diff --git a/apps/webui/src/components/ui/dropdown-menu-actions.tsx b/apps/webui/src/components/ui/dropdown-menu-actions.tsx index 02c012f..f5f7313 100644 --- a/apps/webui/src/components/ui/dropdown-menu-actions.tsx +++ b/apps/webui/src/components/ui/dropdown-menu-actions.tsx @@ -1,6 +1,6 @@ "use client"; -import { MoreHorizontal } from "lucide-react"; +import { Eye, MoreHorizontal } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -18,7 +18,7 @@ import { ComponentProps, PropsWithChildren } from "react"; interface DropdownMenuActionsProps extends ComponentProps { id: Id; - showDetail?: boolean; + showDetail?: boolean | "dropdown-menu"; showEdit?: boolean; showDelete?: boolean; onDetail?: (id: Id) => void; @@ -38,34 +38,49 @@ export function DropdownMenuActions({ ...rest }: PropsWithChildren>) { return ( - - +
+ {showDetail === true && ( - - - {children} - {showDetail && ( - onDetail?.(id)}> - Detail - - )} - {showEdit && ( - onEdit?.(id)}>Edit - )} - {(showDetail || showEdit) && showDelete && } - {showDelete && ( - onDelete?.(id)}> - Delete - ⌘⌫ - - )} - - + )} + + + + + + {children} + {showDetail === "dropdown-menu" && ( + onDetail?.(id)}> + Detail + + )} + {showEdit && ( + onEdit?.(id)}> + Edit + + )} + {(showDetail === "dropdown-menu" || showEdit || children) && + showDelete && } + {showDelete && ( + onDelete?.(id)}> + Delete + ⌘⌫ + + )} + + +
); } diff --git a/apps/webui/src/domains/recorder/schema/subscriptions.ts b/apps/webui/src/domains/recorder/schema/subscriptions.ts index 28dfaec..158acec 100644 --- a/apps/webui/src/domains/recorder/schema/subscriptions.ts +++ b/apps/webui/src/domains/recorder/schema/subscriptions.ts @@ -105,6 +105,13 @@ query GetSubscriptionDetail ($id: Int!) { feedSource } } + subscriberTask { + nodes { + id + taskType + status + } + } credential3rd { id username diff --git a/apps/webui/src/infra/auth/defs.ts b/apps/webui/src/infra/auth/defs.ts index eb85dfd..96064bb 100644 --- a/apps/webui/src/infra/auth/defs.ts +++ b/apps/webui/src/infra/auth/defs.ts @@ -8,5 +8,5 @@ export const AUTH_METHOD = { export type AuthMethodType = ValueOf; export function getAppAuthMethod(): AuthMethodType { - return process.env.AUTH_TYPE as AuthMethodType; + return process.env.AUTH__AUTH_TYPE as AuthMethodType; } diff --git a/apps/webui/src/infra/auth/oidc/config.ts b/apps/webui/src/infra/auth/oidc/config.ts index 2b034c1..61c268b 100644 --- a/apps/webui/src/infra/auth/oidc/config.ts +++ b/apps/webui/src/infra/auth/oidc/config.ts @@ -3,16 +3,16 @@ import { LogLevel, type OpenIdConfiguration } from 'oidc-client-rx'; export function buildOidcConfig(): OpenIdConfiguration { const origin = window.location.origin; - const resource = process.env.OIDC_AUDIENCE!; + const resource = process.env.AUTH__OIDC_AUDIENCE!; return { - authority: process.env.OIDC_ISSUER!, + authority: process.env.AUTH__OIDC_ISSUER!, redirectUrl: `${origin}/auth/oidc/callback`, postLogoutRedirectUri: `${origin}/`, - clientId: process.env.OIDC_CLIENT_ID!, - clientSecret: process.env.OIDC_CLIENT_SECRET, - scope: process.env.OIDC_EXTRA_SCOPES - ? `openid profile email offline_access ${process.env.OIDC_EXTRA_SCOPES}` + clientId: process.env.AUTH__OIDC_CLIENT_ID!, + clientSecret: process.env.AUTH__OIDC_CLIENT_SECRET, + scope: process.env.AUTH__OIDC_EXTRA_SCOPES + ? `openid profile email offline_access ${process.env.AUTH__OIDC_EXTRA_SCOPES}` : 'openid profile email offline_access', triggerAuthorizationResultEvent: true, responseType: 'code', diff --git a/apps/webui/src/infra/graphql/gql/gql.ts b/apps/webui/src/infra/graphql/gql/gql.ts index 6a2266c..585f881 100644 --- a/apps/webui/src/infra/graphql/gql/gql.ts +++ b/apps/webui/src/infra/graphql/gql/gql.ts @@ -26,7 +26,7 @@ type Documents = { "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument, @@ -47,7 +47,7 @@ const documents: Documents = { "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, "\n mutation SyncSubscriptionFeedsIncremental($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsIncremental(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneFeedsFull(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($filter: SubscriptionsFilterInput!) {\n subscriptionsSyncOneSources(filter: $filter) {\n id\n }\n }\n": types.SyncSubscriptionSourcesDocument, @@ -121,7 +121,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filters: Subscr /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"]; +export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/apps/webui/src/infra/graphql/gql/graphql.ts b/apps/webui/src/infra/graphql/gql/graphql.ts index 173b293..5156d26 100644 --- a/apps/webui/src/infra/graphql/gql/graphql.ts +++ b/apps/webui/src/infra/graphql/gql/graphql.ts @@ -1441,6 +1441,8 @@ export type SubscriberTasks = { status: SubscriberTaskStatusEnum; subscriber?: Maybe; subscriberId: Scalars['Int']['output']; + subscription?: Maybe; + subscriptionId?: Maybe; taskType: SubscriberTaskTypeEnum; }; @@ -1473,6 +1475,7 @@ export type SubscriberTasksFilterInput = { runAt?: InputMaybe; status?: InputMaybe; subscriberId?: InputMaybe; + subscriptionId?: InputMaybe; taskType?: InputMaybe; }; @@ -1489,6 +1492,7 @@ export type SubscriberTasksOrderInput = { runAt?: InputMaybe; status?: InputMaybe; subscriberId?: InputMaybe; + subscriptionId?: InputMaybe; taskType?: InputMaybe; }; @@ -1745,6 +1749,7 @@ export type Subscriptions = { sourceUrl: Scalars['String']['output']; subscriber?: Maybe; subscriberId: Scalars['Int']['output']; + subscriberTask: SubscriberTasksConnection; subscriptionBangumi: SubscriptionBangumiConnection; subscriptionEpisode: SubscriptionEpisodeConnection; updatedAt: Scalars['String']['output']; @@ -1772,6 +1777,13 @@ export type SubscriptionsFeedArgs = { }; +export type SubscriptionsSubscriberTaskArgs = { + filters?: InputMaybe; + orderBy?: InputMaybe; + pagination?: InputMaybe; +}; + + export type SubscriptionsSubscriptionBangumiArgs = { filters?: InputMaybe; orderBy?: InputMaybe; @@ -1971,7 +1983,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{ }>; -export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, feed: { __typename?: 'FeedsConnection', nodes: Array<{ __typename?: 'Feeds', id: number, createdAt: string, updatedAt: string, token: string, feedType: FeedTypeEnum, feedSource: FeedSourceEnum }> }, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, homepage?: string | null }> } }> } }; +export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, feed: { __typename?: 'FeedsConnection', nodes: Array<{ __typename?: 'Feeds', id: number, createdAt: string, updatedAt: string, token: string, feedType: FeedTypeEnum, feedSource: FeedSourceEnum }> }, subscriberTask: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum }> }, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, homepage?: string | null }> } }> } }; export type SyncSubscriptionFeedsIncrementalMutationVariables = Exact<{ filter: SubscriptionsFilterInput; @@ -2030,7 +2042,7 @@ export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode; export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode; export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode; -export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"feedSource"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"feedSource"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTask"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx index 39ed52b..b3c289e 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx @@ -51,6 +51,7 @@ import { } from 'lucide-react'; import { useMemo } from 'react'; import { toast } from 'sonner'; +import { prettyTaskType } from '../tasks/-pretty-task-type'; import { SubscriptionSyncDialogContent } from './-sync'; export const Route = createFileRoute('/_app/subscriptions/detail/$id')({ @@ -212,18 +213,6 @@ function SubscriptionDetailRouteComponent() {
- - - - - -
+ +
+
+ + + + + + + +
+
+ {subscription.subscriberTask?.nodes && + subscription.subscriberTask.nodes.length > 0 ? ( + subscription.subscriberTask.nodes.map((task) => ( + + navigate({ + to: '/tasks/detail/$id', + params: { + id: task.id, + }, + }) + } + > +
+
+ +
+ + + {task.id} + + +
+ {task.status} +
+
+
+ )) + ) : ( +
+ No associated tasks now +
+ )} +
+
+ {subscription.bangumi?.nodes && subscription.bangumi.nodes.length > 0 && ( <> @@ -465,6 +512,7 @@ function SubscriptionDetailRouteComponent() { src={`/api/static${bangumi.posterLink}`} alt="Poster" className="h-full w-full object-cover" + loading="lazy" /> )} diff --git a/apps/webui/src/presentation/routes/_app/tasks/-pretty-task-type.ts b/apps/webui/src/presentation/routes/_app/tasks/-pretty-task-type.ts new file mode 100644 index 0000000..7db7163 --- /dev/null +++ b/apps/webui/src/presentation/routes/_app/tasks/-pretty-task-type.ts @@ -0,0 +1,3 @@ +export function prettyTaskType(taskType: string) { + return taskType.replace(/_/g, ' '); +} diff --git a/apps/webui/src/presentation/routes/_app/tasks/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/tasks/detail.$id.tsx index e63e53f..01c4f69 100644 --- a/apps/webui/src/presentation/routes/_app/tasks/detail.$id.tsx +++ b/apps/webui/src/presentation/routes/_app/tasks/detail.$id.tsx @@ -33,6 +33,7 @@ import { import { format } from 'date-fns'; import { ArrowLeft, RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; +import { prettyTaskType } from './-pretty-task-type'; import { getStatusBadge } from './-status-badge'; export const Route = createFileRoute('/_app/tasks/detail/$id')({ @@ -182,7 +183,9 @@ function TaskDetailRouteComponent() {
- {task.taskType} + + {prettyTaskType(task.taskType)} +
diff --git a/apps/webui/src/presentation/routes/_app/tasks/manage.tsx b/apps/webui/src/presentation/routes/_app/tasks/manage.tsx index d7ca568..52bdb59 100644 --- a/apps/webui/src/presentation/routes/_app/tasks/manage.tsx +++ b/apps/webui/src/presentation/routes/_app/tasks/manage.tsx @@ -42,6 +42,7 @@ import { } from '@/infra/errors/apollo'; import { useMemo, useState } from 'react'; import { toast } from 'sonner'; +import { prettyTaskType } from './-pretty-task-type'; import { getStatusBadge } from './-status-badge'; export const Route = createFileRoute('/_app/tasks/manage')({ @@ -202,7 +203,9 @@ function TaskManageRouteComponent() { # {task.id}
- {task.taskType} + + {prettyTaskType(task.taskType)} +
diff --git a/packages/fetch/src/client/core.rs b/packages/fetch/src/client/core.rs index 8b24052..dd333c4 100644 --- a/packages/fetch/src/client/core.rs +++ b/packages/fetch/src/client/core.rs @@ -176,7 +176,7 @@ impl HttpClient { let accept_invalid_certs = proxy .accept_invalid_certs .as_ref() - .map(|b| b.as_bool()) + .map(|b| *b) .unwrap_or_default(); let proxy = proxy.clone().into_proxy()?; if let Some(proxy) = proxy { @@ -307,7 +307,7 @@ impl HttpClient { let accept_invalid_certs = proxy .accept_invalid_certs .as_ref() - .map(|b| b.as_bool()) + .map(|b| *b) .unwrap_or_default(); let proxy = proxy.clone().into_proxy().unwrap_or_default(); if let Some(proxy) = proxy { diff --git a/packages/fetch/src/client/proxy.rs b/packages/fetch/src/client/proxy.rs index 1730448..baaf911 100644 --- a/packages/fetch/src/client/proxy.rs +++ b/packages/fetch/src/client/proxy.rs @@ -2,22 +2,24 @@ use axum::http::{HeaderMap, HeaderValue}; use reqwest::{NoProxy, Proxy}; use serde::{Deserialize, Serialize}; use serde_with::{NoneAsEmptyString, serde_as}; -use util::BooleanLike; use crate::HttpClientError; #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HttpClientProxyConfig { + #[serde(default)] #[serde_as(as = "NoneAsEmptyString")] pub server: Option, + #[serde(default)] #[serde_as(as = "NoneAsEmptyString")] pub auth_header: Option, #[serde(with = "http_serde::option::header_map")] pub headers: Option, + #[serde(default)] #[serde_as(as = "NoneAsEmptyString")] pub no_proxy: Option, - pub accept_invalid_certs: Option, + pub accept_invalid_certs: Option, } impl HttpClientProxyConfig { diff --git a/packages/util/src/lib.rs b/packages/util/src/lib.rs index 689510d..0e466ec 100644 --- a/packages/util/src/lib.rs +++ b/packages/util/src/lib.rs @@ -1,5 +1,3 @@ pub mod errors; -pub mod loose; pub use errors::OptDynErr; -pub use loose::BooleanLike; diff --git a/packages/util/src/loose.rs b/packages/util/src/loose.rs deleted file mode 100644 index be33123..0000000 --- a/packages/util/src/loose.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum BooleanLike { - Boolean(bool), - String(String), - Number(i32), -} - -impl BooleanLike { - pub fn as_bool(&self) -> bool { - match self { - BooleanLike::Boolean(b) => *b, - BooleanLike::String(s) => s.to_lowercase() == "true", - BooleanLike::Number(n) => *n != 0, - } - } -}