feat: update tag and timeline share panel copy for clarity and conciseness
Some checks failed
docker-images / resolve-build-targets (push) Successful in 7s
ui-regression / playwright-regression (push) Failing after 13m4s
docker-images / build-and-push (admin) (push) Successful in 1m17s
docker-images / build-and-push (backend) (push) Successful in 28m13s
docker-images / build-and-push (frontend) (push) Successful in 47s
docker-images / submit-indexnow (push) Successful in 13s

style: enhance global CSS for better responsiveness of terminal chips and navigation pills

test: remove inline subscription test and add maintenance mode access code test

feat: implement media library picker dialog for selecting images from the media library

feat: add media URL controls for uploading and managing media assets

feat: add migration for music_enabled and maintenance_mode settings in site settings

feat: implement maintenance mode functionality with access control

feat: create maintenance page with access code input and error handling

chore: add TypeScript declaration for QR code module
This commit is contained in:
2026-04-02 23:05:49 +08:00
parent 6a50dd478c
commit 9665c933b5
94 changed files with 5266 additions and 1612 deletions

View File

@@ -1,14 +1,11 @@
use chrono::Utc;
use loco_rs::{
bgworker::BackgroundWorker,
prelude::*,
};
use loco_rs::{bgworker::BackgroundWorker, prelude::*};
use sea_orm::{
ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, Order,
PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set,
ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, Order, PaginatorTrait,
QueryFilter, QueryOrder, QuerySelect, Set,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::{Value, json};
use crate::{
models::_entities::{notification_deliveries, worker_jobs},
@@ -213,7 +210,10 @@ fn can_cancel_status(status: &str, cancel_requested: bool) -> bool {
}
fn can_retry_status(status: &str) -> bool {
matches!(status, JOB_STATUS_FAILED | JOB_STATUS_CANCELLED | JOB_STATUS_SUCCEEDED)
matches!(
status,
JOB_STATUS_FAILED | JOB_STATUS_CANCELLED | JOB_STATUS_SUCCEEDED
)
}
fn to_job_record(item: worker_jobs::Model) -> WorkerJobRecord {
@@ -256,15 +256,17 @@ fn catalog_entries() -> Vec<WorkerCatalogEntry> {
(TASK_SEND_MONTHLY_DIGEST, JOB_KIND_TASK, true, true),
]
.into_iter()
.map(|(worker_name, job_kind, supports_cancel, supports_retry)| WorkerCatalogEntry {
worker_name: worker_name.to_string(),
job_kind: job_kind.to_string(),
label: label_for(worker_name),
description: description_for(worker_name),
queue_name: queue_name_for(worker_name),
supports_cancel,
supports_retry,
})
.map(
|(worker_name, job_kind, supports_cancel, supports_retry)| WorkerCatalogEntry {
worker_name: worker_name.to_string(),
job_kind: job_kind.to_string(),
label: label_for(worker_name),
description: description_for(worker_name),
queue_name: queue_name_for(worker_name),
supports_cancel,
supports_retry,
},
)
.collect()
}
@@ -311,7 +313,10 @@ async fn dispatch_download(args_ctx: AppContext, args: DownloadWorkerArgs) {
}
}
async fn dispatch_notification_delivery(args_ctx: AppContext, args: NotificationDeliveryWorkerArgs) {
async fn dispatch_notification_delivery(
args_ctx: AppContext,
args: NotificationDeliveryWorkerArgs,
) {
let worker = NotificationDeliveryWorker::build(&args_ctx);
if let Err(error) = worker.perform(args).await {
tracing::warn!("notification delivery worker execution failed: {error}");
@@ -326,7 +331,9 @@ async fn enqueue_download_worker(ctx: &AppContext, args: DownloadWorkerArgs) ->
Ok(())
}
Err(error) => {
tracing::warn!("download worker queue unavailable, falling back to local task: {error}");
tracing::warn!(
"download worker queue unavailable, falling back to local task: {error}"
);
tokio::spawn(dispatch_download(ctx.clone(), args));
Ok(())
}
@@ -344,7 +351,9 @@ async fn enqueue_notification_worker(
Ok(())
}
Err(error) => {
tracing::warn!("notification worker queue unavailable, falling back to local task: {error}");
tracing::warn!(
"notification worker queue unavailable, falling back to local task: {error}"
);
tokio::spawn(dispatch_notification_delivery(ctx.clone(), args));
Ok(())
}
@@ -442,17 +451,19 @@ pub async fn get_overview(ctx: &AppContext) -> Result<WorkerOverview> {
_ => {}
}
let entry = grouped.entry(item.worker_name.clone()).or_insert_with(|| WorkerStats {
worker_name: item.worker_name.clone(),
job_kind: item.job_kind.clone(),
label: label_for(&item.worker_name),
queued: 0,
running: 0,
succeeded: 0,
failed: 0,
cancelled: 0,
last_job_at: None,
});
let entry = grouped
.entry(item.worker_name.clone())
.or_insert_with(|| WorkerStats {
worker_name: item.worker_name.clone(),
job_kind: item.job_kind.clone(),
label: label_for(&item.worker_name),
queued: 0,
running: 0,
succeeded: 0,
failed: 0,
cancelled: 0,
last_job_at: None,
});
match item.status.as_str() {
JOB_STATUS_QUEUED => entry.queued += 1,
@@ -473,18 +484,35 @@ pub async fn get_overview(ctx: &AppContext) -> Result<WorkerOverview> {
}
pub async fn list_jobs(ctx: &AppContext, query: WorkerJobListQuery) -> Result<WorkerJobListResult> {
let mut db_query = worker_jobs::Entity::find().order_by(worker_jobs::Column::CreatedAt, Order::Desc);
let mut db_query =
worker_jobs::Entity::find().order_by(worker_jobs::Column::CreatedAt, Order::Desc);
if let Some(status) = query.status.map(|value| value.trim().to_string()).filter(|value| !value.is_empty()) {
if let Some(status) = query
.status
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
{
db_query = db_query.filter(worker_jobs::Column::Status.eq(status));
}
if let Some(job_kind) = query.job_kind.map(|value| value.trim().to_string()).filter(|value| !value.is_empty()) {
if let Some(job_kind) = query
.job_kind
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
{
db_query = db_query.filter(worker_jobs::Column::JobKind.eq(job_kind));
}
if let Some(worker_name) = query.worker_name.map(|value| value.trim().to_string()).filter(|value| !value.is_empty()) {
if let Some(worker_name) = query
.worker_name
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
{
db_query = db_query.filter(worker_jobs::Column::WorkerName.eq(worker_name));
}
if let Some(search) = query.search.map(|value| value.trim().to_string()).filter(|value| !value.is_empty()) {
if let Some(search) = query
.search
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
{
db_query = db_query.filter(
Condition::any()
.add(worker_jobs::Column::WorkerName.contains(search.clone()))
@@ -830,6 +858,9 @@ pub async fn retry_job(
)
.await
}
_ => Err(Error::BadRequest(format!("不支持重试任务:{}", item.worker_name))),
_ => Err(Error::BadRequest(format!(
"不支持重试任务:{}",
item.worker_name
))),
}
}