From bb371e1b712e229d20a5c57c2b01424c2c126373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=91=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Fri, 17 Apr 2026 12:54:43 +0300 Subject: [PATCH] v0.2.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 20 +- src/main.rs | 14 +- src/nexrender.rs | 1 - src/processor.rs | 3 - src/static/index.html | 549 ++++++++++++++++++++++++++++++----------- src/static/style.css | 555 ++++++++++++++++++++++++++++++++++++++++++ src/web.rs | 32 +++ 9 files changed, 1024 insertions(+), 154 deletions(-) create mode 100644 src/static/style.css diff --git a/Cargo.lock b/Cargo.lock index 53222ca..2b23e24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ae_anons" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "askama", diff --git a/Cargo.toml b/Cargo.toml index 62118ab..a802007 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ae_anons" -version = "0.2.0" +version = "0.2.2" edition = "2021" license = "MIT" license-file = "LICENSE" diff --git a/README.md b/README.md index 0f8f464..05f9583 100644 --- a/README.md +++ b/README.md @@ -342,15 +342,21 @@ RUST_LOG=debug ./target/release/ae_anons ae_anons/ ├── Cargo.toml ├── LICENSE -├── assets/ -│ └── logo.png ├── README.md ├── .env.example -└── src/ - ├── main.rs # Точка входа и оркестрация приложения - ├── config.rs # Управление конфигурацией - ├── nexrender.rs # Генерация заданий Nexrender и структура данных - └── synology.rs # Клиент API Synology +├── src/ +│ ├── main.rs +│ ├── config.rs +│ ├── nexrender.rs +│ ├── synology.rs +│ ├── processor.rs +│ └── web.rs +├── static/ +│ ├── index.html +│ └── style.css +└── assets/ + └── logo.png + ``` ## Зависимости diff --git a/src/main.rs b/src/main.rs index f78a737..0a553ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ mod config; mod nexrender; -mod synology; mod processor; -mod web; +mod synology; +mod web; use anyhow::Result; use clap::Parser; @@ -29,11 +29,17 @@ async fn main() -> Result<()> { let config = Config::from_env()?; if cli.web { - info!("Starting AE Anons web server v{}", env!("CARGO_PKG_VERSION")); + info!( + "Starting AE Anons web server v{}", + env!("CARGO_PKG_VERSION") + ); web::run_web_server(config).await?; } else { // По умолчанию или с флагом --once выполняем однократную обработку - info!("Starting AE Anons processor v{} (one-time mode)", env!("CARGO_PKG_VERSION")); + info!( + "Starting AE Anons processor v{} (one-time mode)", + env!("CARGO_PKG_VERSION") + ); let submitted = processor::process_spreadsheet(&config).await?; info!("Submitted {} jobs. Exiting.", submitted.len()); } diff --git a/src/nexrender.rs b/src/nexrender.rs index c9ac6d8..c649fba 100644 --- a/src/nexrender.rs +++ b/src/nexrender.rs @@ -302,7 +302,6 @@ impl JobData { variants } - // ЕДИНСТВЕННАЯ реализация метода pub fn to_nexrender_job(&self, config: &crate::config::Config) -> NexrenderJob { let template = if self.team_b.is_empty() { Template::single( diff --git a/src/processor.rs b/src/processor.rs index a3115d2..90ad180 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -11,8 +11,6 @@ use std::collections::HashMap; use std::io::Cursor; use std::path::Path; -// Добавить после импортов в processor.rs - #[derive(Debug, Clone)] pub struct SheetData { pub name: String, @@ -174,7 +172,6 @@ fn parse_sheet_dynamic_optimized( }) .collect(); - // Парсим строки данных let rows_data: Vec> = data_matrix .iter() .skip(1) diff --git a/src/static/index.html b/src/static/index.html index bacd5c9..3e8a5c4 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -1,214 +1,489 @@ - + + - AE Anons Control Panel - + + AE Anons - Nexrender Job Manager + + +
-

🎬 AE Anons - Nexrender Job Manager

- -
- - - - + +
+
+
+ + +
+

AE Anons - Nexrender Job Manager

+
+
+ +
+ +
+ + + + +
+ + Ready +
+
+ + +
+ +
- +
+ + +
+
+ + Auto-refresh: 60s + 60s +
-
- - - - - - - - - - - - - - -
Output FileStateCreatedUpdatedUID
Loading...
+ +
+ + + + + + + + + + + + + + + +
+ Output File + + State + + Created + + Updated + + UID +
+
+ +

Loading jobs...

+
+
+
+ \ No newline at end of file diff --git a/src/static/style.css b/src/static/style.css new file mode 100644 index 0000000..3b3e2e4 --- /dev/null +++ b/src/static/style.css @@ -0,0 +1,555 @@ +/* ======================================== + AE Anons - Nexrender Job Manager Styles + ======================================== */ + +/* CSS Variables - Light Theme (default) */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #fafafa; + --bg-tertiary: #f3f0f7; + --text-primary: #1a1a1a; + --text-secondary: #6b4f7c; + --border-color: #e0d4e8; + --accent-primary: #7c3aed; + --accent-secondary: #c2410c; + --accent-success: #10b981; + --accent-warning: #ea580c; + --accent-danger: #ef4444; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); + --header-bg: #7c3aed; + --header-text: #ffffff; +} + +/* Dark Theme */ +[data-theme="dark"] { + --bg-primary: #1a1025; + --bg-secondary: #251a30; + --bg-tertiary: #352545; + --text-primary: #f0e6ff; + --text-secondary: #c4a6d9; + --border-color: #4a3560; + --accent-primary: #a78bfa; + --accent-secondary: #f59e0b; + --accent-success: #34d399; + --accent-warning: #fbbf24; + --accent-danger: #f87171; + --header-bg: #a78bfa; + --header-text: #1a1025; +} + +/* Reset & Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; +} + +.container { + max-width: 100%; + margin: 0 auto; + padding: 24px; +} + +/* Header */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + flex-wrap: wrap; + gap: 16px; +} + +.header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.logo-container { + width: 48px; + height: 48px; + border-radius: 12px; + background: var(--accent-primary); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + box-shadow: var(--shadow-md); +} + +.logo { + width: 100%; + height: 100%; + object-fit: cover; +} + +.logo-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 24px; + font-weight: bold; +} + +.header h1 { + font-size: 28px; + font-weight: 600; + color: var(--accent-primary); + letter-spacing: -0.5px; +} + +.header-controls { + display: flex; + align-items: center; + gap: 12px; +} + +.theme-toggle { + background: var(--bg-secondary); + border: 2px solid var(--accent-primary); + border-radius: 24px; + padding: 8px 16px; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + color: var(--text-primary); + font-size: 14px; + box-shadow: var(--shadow-sm); +} + +.theme-toggle:hover { + box-shadow: var(--shadow-md); +} + +/* Action Bar */ +.action-bar { + background: var(--bg-secondary); + border-radius: 12px; + padding: 20px; + margin-bottom: 24px; + box-shadow: var(--shadow-md); + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + border: 1px solid var(--border-color); +} + +/* Buttons */ +.btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); + position: relative; + overflow: hidden; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.1); + opacity: 0; + transition: opacity 0.2s; +} + +.btn:hover::before { + opacity: 1; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.btn i { + font-size: 14px; +} + +/* Generate - Purple */ +.btn-primary { + background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); + color: white; + border: none; + box-shadow: 0 2px 8px rgba(124, 58, 237, 0.3); +} + +[data-theme="dark"] .btn-primary { + background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%); + color: #1a1025; + box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4); +} + +.btn-primary:hover { + box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4); +} + +/* Stop - Orange */ +.btn-warning { + background: linear-gradient(135deg, #ea580c 0%, #f97316 100%); + color: white; + border: none; + box-shadow: 0 2px 8px rgba(234, 88, 12, 0.3); +} + +[data-theme="dark"] .btn-warning { + background: linear-gradient(135deg, #f97316 0%, #fb923c 100%); + color: #1a1025; + box-shadow: 0 2px 8px rgba(249, 115, 22, 0.4); +} + +.btn-warning:hover { + box-shadow: 0 4px 12px rgba(234, 88, 12, 0.4); +} + +/* Cleanup - Plum */ +.btn-danger { + background: linear-gradient(135deg, #9d174d 0%, #be185d 100%); + color: white; + border: none; + box-shadow: 0 2px 8px rgba(157, 23, 77, 0.3); +} + +[data-theme="dark"] .btn-danger { + background: linear-gradient(135deg, #be185d 0%, #db2777 100%); + color: #f0e6ff; + box-shadow: 0 2px 8px rgba(190, 24, 93, 0.4); +} + +.btn-danger:hover { + box-shadow: 0 4px 12px rgba(157, 23, 77, 0.4); +} + +/* Refresh - Outline */ +.btn-outline { + background: transparent; + border: 2px solid #7c3aed; + color: #7c3aed; + box-shadow: none; +} + +[data-theme="dark"] .btn-outline { + border-color: #a78bfa; + color: #a78bfa; +} + +.btn-outline:hover { + background: #7c3aed; + color: white; + border-color: #7c3aed; +} + +[data-theme="dark"] .btn-outline:hover { + background: #a78bfa; + color: #1a1025; + border-color: #a78bfa; +} + +/* Status Message */ +.status-message { + margin-left: auto; + padding: 8px 16px; + background: var(--bg-tertiary); + border-radius: 20px; + font-size: 14px; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 8px; +} + +/* Stats Cards */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--bg-secondary); + border-radius: 12px; + padding: 20px; + box-shadow: var(--shadow-sm); + border: 1px solid var(--border-color); +} + +.stat-card h3 { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 8px; + font-weight: 500; +} + +.stat-card .value { + font-size: 32px; + font-weight: 700; + color: var(--accent-primary); +} + +/* Filter Bar */ +.filter-bar { + margin-bottom: 20px; + display: flex; + gap: 12px; + align-items: center; +} + +.search-wrapper { + flex: 1; + position: relative; +} + +.search-wrapper i { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); +} + +.search-input { + width: 100%; + padding: 12px 16px 12px 44px; + background: var(--bg-secondary); + border: 2px solid var(--border-color); + border-radius: 24px; + color: var(--text-primary); + font-size: 14px; +} + +.search-input:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1); +} + +.auto-refresh-badge { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--bg-tertiary); + border-radius: 20px; + font-size: 13px; + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +/* Table */ +.table-container { + background: var(--bg-secondary); + border-radius: 12px; + box-shadow: var(--shadow-lg); + overflow-x: auto; + border: 1px solid var(--border-color); +} + +table { + width: 100%; + border-collapse: collapse; + table-layout: auto; + min-width: 800px; +} + +th { + text-align: left; + padding: 16px; + background: var(--header-bg); + color: var(--header-text); + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; + cursor: pointer; + user-select: none; + position: relative; +} + +th:hover { + opacity: 0.9; +} + +th i { + margin-left: 8px; + font-size: 12px; + opacity: 0.7; +} + +th.sorted-asc i, +th.sorted-desc i { + opacity: 1; +} + +td { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + vertical-align: middle; +} + +tr:last-child td { + border-bottom: none; +} + +tr:hover { + background: var(--bg-tertiary); +} + +/* Job Filename */ +.job-filename { + font-family: 'Monaco', 'Menlo', 'Cascadia Code', 'Consolas', monospace; + font-size: 13px; + font-weight: 500; + line-height: 1.5; + word-break: break-word; + white-space: normal; + max-width: 100%; + color: var(--text-primary); +} + +/* Badges */ +.badge { + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + display: inline-block; + white-space: nowrap; +} + +.badge-finished { + background: rgba(16, 185, 129, 0.15); + color: var(--accent-success); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.badge-started, +.badge-processing { + background: rgba(124, 58, 237, 0.15); + color: var(--accent-primary); + border: 1px solid rgba(124, 58, 237, 0.3); +} + +.badge-queued { + background: rgba(234, 88, 12, 0.15); + color: var(--accent-warning); + border: 1px solid rgba(234, 88, 12, 0.3); +} + +.badge-error { + background: rgba(239, 68, 68, 0.15); + color: var(--accent-danger); + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.badge-pending { + background: rgba(245, 158, 11, 0.15); + color: var(--accent-secondary); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +/* Date & Time */ +.datetime { + font-family: 'Monaco', 'Menlo', monospace; + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; +} + +.uid { + font-family: 'Monaco', 'Menlo', monospace; + font-size: 11px; + color: var(--text-secondary); + white-space: nowrap; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--text-secondary); +} + +.empty-state i { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; + color: var(--accent-primary); +} + +/* Loading Spinner */ +.spinner { + width: 20px; + height: 20px; + border: 3px solid var(--border-color); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + display: inline-block; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Responsive */ +@media (max-width: 768px) { + .container { + padding: 16px; + } + + .header { + flex-direction: column; + align-items: flex-start; + } + + .header-left { + flex-wrap: wrap; + } + + .header h1 { + font-size: 20px; + } + + .action-bar { + flex-direction: column; + align-items: stretch; + } + + .status-message { + margin-left: 0; + } +} \ No newline at end of file diff --git a/src/web.rs b/src/web.rs index 9d93e78..f672eef 100644 --- a/src/web.rs +++ b/src/web.rs @@ -12,6 +12,7 @@ use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::Mutex; use tower_http::trace::TraceLayer; +use tower_http::services::ServeDir; #[derive(Clone)] pub struct AppState { @@ -99,6 +100,9 @@ pub async fn run_web_server(config: Config) -> anyhow::Result<()> { .route("/api/generate", post(generate_jobs)) .route("/api/cleanup", post(cleanup_jobs)) .route("/api/status", get(get_status)) + .route("/api/jobs/stop-all", post(stop_all_jobs)) + .nest_service("/static", ServeDir::new("src/static")) + .nest_service("/assets", ServeDir::new("assets")) .layer(TraceLayer::new_for_http()) .with_state(state); @@ -176,6 +180,34 @@ async fn get_status(State(state): State) -> Result) -> Result { + use reqwest::Client; + + 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()))?; + + let mut stopped = 0; + for job in jobs { + 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); + } + } + } + + log::info!("Stopped {} active jobs", stopped); + Ok((StatusCode::OK, format!("Stopped {} jobs", stopped))) +} + // Error handling struct AppError(StatusCode, String);