Initial Headless Functions v2

This commit is contained in:
Joey Pillunat 2025-10-24 13:32:26 +02:00
parent 615c5c9edd
commit a41f765211
4 changed files with 90 additions and 36 deletions

View file

@ -13,7 +13,7 @@ path = "Core/main.rs"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1"
tokio-stream = { version = "0.1", features = ["sync"] }
tower-http = { version = "0.5", features = ["fs"] }
crossterm = "0.27"
ratatui = "0.26"

View file

@ -3,6 +3,7 @@ use futures::StreamExt;
use chromiumoxide::browser::{Browser, BrowserConfig};
use chromiumoxide::cdp::browser_protocol::page::NavigateParams;
use chromiumoxide::cdp::browser_protocol::input::InsertTextParams;
use tokio::sync::broadcast;
use crate::state::{SharedState, HeadlessPhase};
use crate::config::read_config;
@ -110,27 +111,33 @@ impl PageHandle {
}
}
pub async fn run_test(ip: &str) -> Result<(), Box<dyn std::error::Error>> {
println!("[Headless] Starte Test für {ip}");
pub async fn run_test(ip: &str, log: broadcast::Sender<String>) -> Result<(), Box<dyn std::error::Error>> {
let _ = log.send(format!("[Headless] Starte Test für {ip}"));
// config lesen, um Pfad zu Chrome zu bekommen
let exe_dir = std::env::current_exe()?.parent().unwrap().to_path_buf();
let cfg = read_config(&exe_dir)?;
let chrome_exe = Path::new(&cfg.chrome.path);
let cfg = crate::config::read_config(&exe_dir)?;
let chrome_exe = std::path::Path::new(&cfg.chrome.path);
let _ = log.send(format!("[Headless] Chrome: {}", chrome_exe.display()));
let manager = HeadlessManager::new(chrome_exe, &cfg.chrome.args).await?;
let page = manager.new_page().await?;
let url_https = format!("https://{ip}/");
let url_http = format!("http://{ip}/");
// Zielseite öffnen
let url = format!("https://{ip}/");
page.goto(&url).await?;
let _ = log.send(format!("[Headless] goto {url_https}"));
if page.goto(&url_https).await.is_err() {
let _ = log.send(format!("[Headless] HTTPS fehlgeschlagen, versuche HTTP"));
let _ = log.send(format!("[Headless] goto {url_http}"));
page.goto(&url_http).await?;
}
// kurz warten & Text auslesen
let _ = log.send("[Headless] warte auf <h1> …".to_string());
let _ = page.wait_for_selector("h1", 5000).await?;
if let Some(h1) = page.get_text("h1").await? {
println!("[Headless] H1: {}", h1);
} else {
println!("[Headless] Keine H1 gefunden");
match page.get_text("h1").await? {
Some(h1) => { let _ = log.send(format!("[Headless] H1: {h1}")); }
None => { let _ = log.send("[Headless] Keine H1 gefunden".to_string()); }
}
Ok(())

View file

@ -19,26 +19,39 @@ pub const DEFAULT_INDEX_HTML: &str = r#"
<button id="btn-start">Start</button>
<p id="status" class="status"></p>
</section>
<section class="card">
<h2>Konsole</h2>
<pre id="console" class="console"></pre>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>
"#;
pub const DEFAULT_APP_JS: &str = r#"
document.addEventListener('DOMContentLoaded', () => {
const ip = document.getElementById('ip');
const btn = document.getElementById('btn-start');
const status = document.getElementById('status');
const consoleEl = document.getElementById('console');
// SSE: Logs anhören
const es = new EventSource('/api/logs');
es.onmessage = (ev) => {
consoleEl.textContent += ev.data + '\n';
consoleEl.scrollTop = consoleEl.scrollHeight;
};
es.onerror = () => {
// optional: Reconnect-Info
};
btn.addEventListener('click', async () => {
const val = (ip.value || '').trim();
if (!val) {
status.textContent = 'Bitte IP eingeben.';
return;
}
status.textContent = `Starte Erkennung für ${val} ...`;
if (!val) { status.textContent = 'Bitte IP eingeben.'; return; }
status.textContent = `Starte`;
try {
const res = await fetch('/api/start', {
@ -46,18 +59,17 @@ document.addEventListener('DOMContentLoaded', () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ip: val })
});
const text = await res.text();
status.textContent = text || 'Anfrage gesendet.';
status.textContent = text || 'OK';
} catch (err) {
console.error(err);
status.textContent = 'Fehler beim Senden der Anfrage.';
status.textContent = 'Fehler: ' + (err?.message || err);
}
});
});
"#;
pub const DEFAULT_STYLES_CSS: &str = r#"
* { box-sizing: border-box; }
body { margin: 0; font: 16px system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
@ -74,4 +86,8 @@ button { margin-top: 12px; padding: 10px 14px; border-radius: 8px; border: 1px s
background: #3056ff; color: white; cursor: pointer; }
button:hover { filter: brightness(1.05); }
.status { margin-top: 10px; color: #9db4ff; }
.console { margin: 0; margin-top: 10px; padding: 10px; height: 220px; overflow: auto;
background: #0b0f19; border: 1px solid #1f2740; border-radius: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
white-space: pre-wrap; }
"#;

View file

@ -1,3 +1,5 @@
mod assets;
use std::{
env, fs,
net::SocketAddr,
@ -7,12 +9,14 @@ use axum::{
routing::{get, post},
extract::Json,
Router,
response::Html,
response::{Html, sse::{Sse, Event, KeepAlive}},
};
use tower_http::services::ServeDir;
use serde::Deserialize;
use tokio::sync::broadcast;
use tokio_stream::wrappers::BroadcastStream;
use futures::StreamExt;
mod assets;
use assets::{DEFAULT_INDEX_HTML, DEFAULT_APP_JS, DEFAULT_STYLES_CSS};
use crate::{state::SharedState, headless};
@ -38,26 +42,47 @@ fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> {
Ok(())
}
#[derive(Clone)]
struct HttpState {
shared: SharedState,
log_tx: broadcast::Sender<String>,
}
#[derive(Deserialize)]
struct StartRequest {
ip: String,
}
async fn start_job(Json(req): Json<StartRequest>) -> String {
async fn start_job(
axum::extract::State(st): axum::extract::State<HttpState>,
Json(req): Json<StartRequest>,
) {
let ip = req.ip.trim().to_string();
println!("[WebServer] Starte Headless-Test für {}", ip);
// Klon für den Task
let ip_for_task = ip.clone();
let tx_for_task = st.log_tx.clone();
tokio::spawn(async move {
if let Err(e) = headless::run_test(&ip_for_task).await {
eprintln!("[Headless Error] {}", e);
if let Err(e) = headless::run_test(&ip, tx_for_task.clone()).await {
let _ = tx_for_task.send(format!("[Headless Error] {e}"));
} else {
let _ = tx_for_task.send(format!("[Headless] Fertig für {ip}"));
}
});
}
format!("Starte Setup für {}", ip)
async fn stream_logs(
axum::extract::State(st): axum::extract::State<HttpState>
) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
let rx = st.log_tx.subscribe();
let stream = BroadcastStream::new(rx)
.filter_map(|res| async move { res.ok() })
.map(|msg| Ok(Event::default().data(msg)));
Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(std::time::Duration::from_secs(10))
.text("💓"),
)
}
// Port wird jetzt von außen übergeben
@ -67,6 +92,10 @@ pub async fn start(state: SharedState, port: u16)
let dir = exe_dir();
rebuild_web_assets(&dir)?;
// Broadcast-Kanal für Web-Logs
let (log_tx, _rx) = broadcast::channel::<String>(200);
let http_state = HttpState { shared: state.clone(), log_tx: log_tx.clone() };
let web_root = dir.join(WEB_DIR);
let static_root = web_root.clone();
let index_path = web_root.join("index.html");
@ -83,9 +112,11 @@ pub async fn start(state: SharedState, port: u16)
}
}
}))
.route("/api/start", post(start_job)) // 👈 Neue Route
.route("/api/start", post(start_job)) // Start-Button
.route("/api/logs", get(stream_logs)) // SSE-Konsole
.nest_service("/static", ServeDir::new(static_root))
.route("/healthz", get(|| async { "ok" }));
.route("/healthz", get(|| async { "ok" }))
.with_state(http_state);
{
let mut s = state.lock().unwrap();