feat: fully offline web interface with embedded Font Awesome

- Replace CDN with local Font Awesome files
- Add favicon handler
- Embed all static assets (CSS, fonts, logo)
- Update README with Font Awesome license attribution
- Bump version to 0.2.3
This commit is contained in:
2026-04-17 18:16:20 +03:00
parent 70d3e6c6cd
commit 6f13679dd9
16 changed files with 8858 additions and 19 deletions

View File

@@ -11,6 +11,7 @@ use serde::Serialize;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::Mutex;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer;
#[derive(Clone)]
@@ -33,43 +34,32 @@ impl JobInfo {
let uid = job.get("uid")?.as_str()?.to_string();
let state = job.get("state")?.as_str()?.to_string();
// Извлекаем имя выходного файла из postrender actions
let outfile_name = job
.get("actions")
.and_then(|a| a.get("postrender"))
.and_then(|p| p.as_array())
.and_then(|arr| {
// Ищем действие copy (в нём финальный путь)
arr.iter()
.find_map(|action| {
// Проверяем, что это действие copy
action
.get("module")
.and_then(|m| m.as_str())
.filter(|&m| m == "@nexrender/action-copy")
.and_then(|_| {
// Извлекаем output из copy
action.get("output").and_then(|o| o.as_str())
})
.and_then(|_| action.get("output").and_then(|o| o.as_str()))
})
// Если copy не найдено, пробуем encode
.or_else(|| {
arr.iter()
.find_map(|action| action.get("output").and_then(|o| o.as_str()))
})
})
.map(|path| {
// Извлекаем только имя файла из полного пути
std::path::Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path)
.to_string()
})
.unwrap_or_else(|| {
// Если не удалось извлечь, используем UID
format!("job_{}", uid)
});
.unwrap_or_else(|| format!("job_{}", uid));
Some(JobInfo {
uid,
@@ -87,7 +77,6 @@ impl JobInfo {
}
}
// web.rs - функция run_web_server
pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
let web_port = config.web_port;
let state = AppState {
@@ -97,7 +86,10 @@ pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
let app = Router::new()
.route("/", get(index_page))
.route("/favicon.ico", get(favicon))
.route("/static/style.css", get(style_css))
.route("/static/fontawesome/all.min.css", get(fontawesome_css))
.nest_service("/static/fontawesome/webfonts", ServeDir::new("src/static/fontawesome/webfonts"))
.route("/assets/logo.png", get(logo_png))
.route("/api/jobs", get(list_jobs))
.route("/api/generate", post(generate_jobs))
@@ -123,10 +115,17 @@ async fn style_css() -> impl IntoResponse {
([(CONTENT_TYPE, "text/css")], include_str!("static/style.css"))
}
async fn fontawesome_css() -> impl IntoResponse {
([(CONTENT_TYPE, "text/css")], include_str!("static/fontawesome/all.min.css"))
}
async fn logo_png() -> impl IntoResponse {
([(CONTENT_TYPE, "image/png")], include_bytes!("../assets/logo.png").as_slice())
}
async fn favicon() -> impl IntoResponse {
([(CONTENT_TYPE, "image/png")], include_bytes!("../assets/logo.png").as_slice())
}
async fn list_jobs(State(state): State<AppState>) -> Result<Json<Vec<JobInfo>>, AppError> {
let jobs_json = fetch_all_jobs(&state.config.nexrender_api_url)
@@ -195,7 +194,6 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
let client = Client::new();
let api_url = &state.config.nexrender_api_url;
// Получаем все задания
let jobs = fetch_all_jobs(api_url)
.await
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
@@ -205,7 +203,6 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
let state = job.get("state").and_then(|s| s.as_str()).unwrap_or("");
if state == "queued" || state == "started" || state == "processing" {
if let Some(uid) = job.get("uid").and_then(|u| u.as_str()) {
// Отправляем DELETE запрос для остановки задания
let _ = client.delete(&format!("{}/{}", api_url, uid)).send().await;
stopped += 1;
log::info!("Stopped job: {}", uid);
@@ -217,11 +214,10 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
Ok((StatusCode::OK, format!("Stopped {} jobs", stopped)))
}
// Error handling
struct AppError(StatusCode, String);
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
(self.0, self.1).into_response()
}
}
}