mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
Frontend build improvements, support CSP, add icons
This commit is contained in:
Generated
+53
-1
@@ -198,7 +198,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"nom",
|
||||
"nom 7.1.3",
|
||||
"num-rational",
|
||||
"v_frame",
|
||||
]
|
||||
@@ -795,10 +795,12 @@ dependencies = [
|
||||
"decomp-dev-images",
|
||||
"image",
|
||||
"itertools 0.14.0",
|
||||
"libsystemd",
|
||||
"maud",
|
||||
"mime",
|
||||
"objdiff-core",
|
||||
"prost",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1887,6 +1889,24 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsystemd"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b85fe9dc49de659d05829fdf72b5770c0a5952d1055c34a39f6d4e932bce175d"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"nom 8.0.0",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror 2.0.12",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
@@ -2012,6 +2032,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -2089,6 +2118,19 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -2099,6 +2141,15 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
@@ -4433,6 +4484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -24,6 +24,7 @@ mime = "0.3"
|
||||
objdiff-core = { version = "2.5", features = ["bindings"] }
|
||||
#objdiff-core = { path = "../objdiff/objdiff-core", features = ["bindings"] }
|
||||
octocrab = { version = "0.44", features = ["stream"] }
|
||||
regex = "1.11"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "gzip", "zstd", "http2", "rustls-tls-native-roots"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# This is a configuration file for the bacon tool
|
||||
#
|
||||
# Complete help on configuration: https://dystroy.org/bacon/config/
|
||||
#
|
||||
# You may check the current default at
|
||||
# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml
|
||||
|
||||
default_job = "run-long"
|
||||
@@ -17,7 +17,7 @@ http = "1.3"
|
||||
jsonwebtoken = "9.3"
|
||||
objdiff-core.workspace = true
|
||||
octocrab.workspace = true
|
||||
regex = "1.11"
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2 = "0.10"
|
||||
|
||||
@@ -16,6 +16,7 @@ itertools = "0.14"
|
||||
maud.workspace = true
|
||||
mime.workspace = true
|
||||
objdiff-core.workspace = true
|
||||
regex.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -39,3 +40,6 @@ image.workspace = true
|
||||
# proto
|
||||
bytes = "1.7"
|
||||
prost = "0.13"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libsystemd = "0.7"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
http::{HeaderMap, StatusCode, header::REFERER},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use decomp_dev_auth::{CurrentUser, GITHUB_OAUTH_STATE, RETURN_TO, generate_nonce};
|
||||
use decomp_dev_core::{AppError, config::GitHubConfig};
|
||||
use decomp_dev_core::{AppError, config::GitHubConfig, util::UrlExt};
|
||||
use maud::{DOCTYPE, html};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::handlers::common::{chunks, header};
|
||||
use crate::handlers::common::{Load, TemplateContext};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct LoginQuery {
|
||||
@@ -17,9 +17,11 @@ pub struct LoginQuery {
|
||||
|
||||
pub async fn login(
|
||||
session: Session,
|
||||
Query(LoginQuery { return_to }): Query<LoginQuery>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<LoginQuery>,
|
||||
State(config): State<GitHubConfig>,
|
||||
current_user: Option<CurrentUser>,
|
||||
mut ctx: TemplateContext,
|
||||
) -> Result<Response, AppError> {
|
||||
if current_user.is_some() {
|
||||
return Ok(Redirect::to("/").into_response());
|
||||
@@ -28,29 +30,27 @@ pub async fn login(
|
||||
tracing::warn!("No GitHub OAuth config found");
|
||||
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "No GitHub OAuth config").into_response());
|
||||
};
|
||||
let nonce = generate_nonce();
|
||||
session.insert(GITHUB_OAUTH_STATE, nonce.clone()).await?;
|
||||
if let Some(return_to) = return_to {
|
||||
if return_to.starts_with('/') {
|
||||
session.insert(RETURN_TO, return_to).await?;
|
||||
}
|
||||
let oauth_state = generate_nonce();
|
||||
session.insert(GITHUB_OAUTH_STATE, oauth_state.clone()).await?;
|
||||
if let Some(return_to) = calc_return_to(&headers, query) {
|
||||
session.insert(RETURN_TO, return_to).await?;
|
||||
}
|
||||
let mut redirect_url = url::Url::parse("https://github.com/login/oauth/authorize")?;
|
||||
{
|
||||
let mut query = redirect_url.query_pairs_mut();
|
||||
query.append_pair("client_id", &config.client_id);
|
||||
query.append_pair("redirect_uri", &config.redirect_uri);
|
||||
query.append_pair("state", &nonce);
|
||||
query.append_pair("state", &oauth_state);
|
||||
}
|
||||
Ok(html! {
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { "Logging in... • decomp.dev" }
|
||||
meta http-equiv="refresh" content=(format!("0;URL={redirect_url}"));
|
||||
(header())
|
||||
(chunks("main", true).await)
|
||||
(ctx.header().await)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
}
|
||||
body {
|
||||
.loading-container {
|
||||
@@ -58,11 +58,33 @@ pub async fn login(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.into_response())
|
||||
};
|
||||
Ok((ctx, rendered).into_response())
|
||||
}
|
||||
|
||||
pub async fn logout(session: Session) -> Result<Response, AppError> {
|
||||
pub async fn logout(
|
||||
session: Session,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<LoginQuery>,
|
||||
) -> Result<Response, AppError> {
|
||||
session.flush().await?;
|
||||
if let Some(return_to) = calc_return_to(&headers, query) {
|
||||
return Ok(Redirect::to(&return_to).into_response());
|
||||
}
|
||||
Ok(Redirect::to("/").into_response())
|
||||
}
|
||||
|
||||
fn calc_return_to(headers: &HeaderMap, LoginQuery { return_to }: LoginQuery) -> Option<String> {
|
||||
let mut return_to = return_to.or_else(|| {
|
||||
if headers.get("sec-fetch-site").and_then(|h| h.to_str().ok()) == Some("same-origin") {
|
||||
headers
|
||||
.get(REFERER)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| url::Url::parse(s).ok())
|
||||
.map(|u| u.path_and_query().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
return_to.take_if(|s| s.starts_with('/'))
|
||||
}
|
||||
|
||||
+344
-156
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
use std::convert::Infallible;
|
||||
|
||||
use axum::{
|
||||
extract::{FromRequestParts, OptionalFromRequestParts, Request},
|
||||
http::{Extensions, HeaderValue, StatusCode, header::CONTENT_TYPE, request::Parts},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use decomp_dev_auth::generate_nonce;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Nonce(pub String);
|
||||
|
||||
impl Nonce {
|
||||
fn from_extensions(extensions: &Extensions) -> Option<Self> {
|
||||
extensions.get::<Nonce>().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for Nonce
|
||||
where S: Send + Sync
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(req: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Self::from_extensions(&req.extensions)
|
||||
.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Nonce not found"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> OptionalFromRequestParts<S> for Nonce
|
||||
where S: Send + Sync
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
req: &mut Parts,
|
||||
_state: &S,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
Ok(Self::from_extensions(&req.extensions))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExtraDomains(pub Vec<String>);
|
||||
|
||||
pub async fn csp_middleware(mut req: Request, next: Next) -> Response {
|
||||
let nonce = generate_nonce();
|
||||
req.extensions_mut().insert(Nonce(nonce.clone()));
|
||||
let mut response = next.run(req).await;
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
if content_type.starts_with("text/html") {
|
||||
let extra_domains =
|
||||
response.extensions().get::<ExtraDomains>().map(|e| e.0.clone()).unwrap_or_default();
|
||||
let response_headers = response.headers_mut();
|
||||
let mut header = "default-src 'none';base-uri 'none';script-src ".to_string();
|
||||
let nonce_value = format!("'nonce-{}'", nonce);
|
||||
header.push_str(&nonce_value);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// tower-livereload script
|
||||
header.push_str(" 'sha256-L/4du8mXhXqvOm9Re02dTBSI4mWBbsqtG8F+xh3jiJc='");
|
||||
}
|
||||
header.push_str(";style-src ");
|
||||
header.push_str(&nonce_value);
|
||||
header.push_str(";img-src 'self' data:");
|
||||
for domain in &extra_domains {
|
||||
header.push(' ');
|
||||
header.push_str(domain);
|
||||
}
|
||||
header.push_str(";font-src 'self'");
|
||||
for domain in &extra_domains {
|
||||
header.push(' ');
|
||||
header.push_str(domain);
|
||||
}
|
||||
header.push_str(";connect-src 'self'");
|
||||
for domain in &extra_domains {
|
||||
header.push(' ');
|
||||
header.push_str(domain);
|
||||
if let Some(domain) = domain.strip_prefix("https://") {
|
||||
header.push_str(&format!(" wss://{domain}"));
|
||||
} else if let Some(domain) = domain.strip_prefix("http://") {
|
||||
header.push_str(&format!(" ws://{domain}"));
|
||||
}
|
||||
}
|
||||
header.push_str(";manifest-src 'self'");
|
||||
response_headers.insert("Content-Security-Policy", header.parse().unwrap());
|
||||
response_headers
|
||||
.insert("Cross-Origin-Embedder-Policy", HeaderValue::from_static("require-corp"));
|
||||
response_headers
|
||||
.insert("Cross-Origin-Opener-Policy", HeaderValue::from_static("same-origin"));
|
||||
response_headers
|
||||
.insert("Referrer-Policy", HeaderValue::from_static("strict-origin-when-cross-origin"));
|
||||
response_headers.insert("X-Frame-Options", HeaderValue::from_static("DENY"));
|
||||
}
|
||||
let response_headers = response.headers_mut();
|
||||
response_headers.insert("X-Content-Type-Options", HeaderValue::from_static("nosniff"));
|
||||
response
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
Form,
|
||||
@@ -19,15 +17,14 @@ use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
handlers::common::{chunks, footer, header, nav_links},
|
||||
handlers::common::{Load, TemplateContext, nav_links},
|
||||
};
|
||||
|
||||
pub async fn manage(
|
||||
mut ctx: TemplateContext,
|
||||
State(state): State<AppState>,
|
||||
current_user: CurrentUser,
|
||||
) -> Result<Markup, AppError> {
|
||||
let start = Instant::now();
|
||||
|
||||
) -> Result<Response, AppError> {
|
||||
let projects = state
|
||||
.db
|
||||
.get_projects()
|
||||
@@ -37,15 +34,15 @@ pub async fn manage(
|
||||
.sorted_by(|a, b| a.project.name().cmp(&b.project.name()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(html! {
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { "Manage • decomp.dev" }
|
||||
(header())
|
||||
(chunks("main", true).await)
|
||||
(chunks("manage", true).await)
|
||||
(ctx.header().await)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
(ctx.chunks("manage", Load::Deferred).await)
|
||||
}
|
||||
body {
|
||||
header {
|
||||
@@ -72,9 +69,10 @@ pub async fn manage(
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer(start, Some(¤t_user)))
|
||||
(ctx.footer(Some(¤t_user)))
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok((ctx, rendered).into_response())
|
||||
}
|
||||
|
||||
fn project_fragment(info: &ProjectInfo) -> Markup {
|
||||
@@ -94,20 +92,20 @@ fn project_fragment(info: &ProjectInfo) -> Markup {
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
ctx: TemplateContext,
|
||||
State(state): State<AppState>,
|
||||
current_user: CurrentUser,
|
||||
) -> Result<Response, AppError> {
|
||||
render_new(&state, ¤t_user, None, None).await
|
||||
render_new(ctx, &state, ¤t_user, None, None).await
|
||||
}
|
||||
|
||||
async fn render_new(
|
||||
mut ctx: TemplateContext,
|
||||
state: &AppState,
|
||||
current_user: &CurrentUser,
|
||||
message: Option<&str>,
|
||||
prefill: Option<&Project>,
|
||||
) -> Result<Response, AppError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let projects = state.db.get_projects().await?;
|
||||
|
||||
let repos = current_user
|
||||
@@ -136,15 +134,15 @@ async fn render_new(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(html! {
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { "New Project • decomp.dev" }
|
||||
(header())
|
||||
(chunks("main", true).await)
|
||||
(chunks("manage", true).await)
|
||||
(ctx.header().await)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
(ctx.chunks("manage", Load::Deferred).await)
|
||||
}
|
||||
body {
|
||||
header {
|
||||
@@ -198,9 +196,10 @@ async fn render_new(
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer(start, Some(current_user)))
|
||||
(ctx.footer(Some(current_user)))
|
||||
}
|
||||
}.into_response())
|
||||
};
|
||||
Ok((ctx, rendered).into_response())
|
||||
}
|
||||
|
||||
fn platform_options(current_platform: Option<&str>) -> Markup {
|
||||
@@ -228,6 +227,7 @@ pub struct NewForm {
|
||||
}
|
||||
|
||||
pub async fn new_save(
|
||||
ctx: TemplateContext,
|
||||
State(state): State<AppState>,
|
||||
current_user: CurrentUser,
|
||||
Form(form): Form<NewForm>,
|
||||
@@ -245,6 +245,7 @@ pub async fn new_save(
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to fetch repository: {:?}", e);
|
||||
return render_new(
|
||||
ctx,
|
||||
&state,
|
||||
¤t_user,
|
||||
Some("Failed to fetch repository information."),
|
||||
@@ -267,6 +268,7 @@ pub async fn new_save(
|
||||
};
|
||||
if repo.permissions.as_ref().is_none_or(|p| !p.admin) {
|
||||
return render_new(
|
||||
ctx,
|
||||
&state,
|
||||
¤t_user,
|
||||
Some("You do not have admin permissions on this repository."),
|
||||
@@ -279,7 +281,7 @@ pub async fn new_save(
|
||||
Ok(workflow_id) => workflow_id,
|
||||
Err(e) => {
|
||||
let message = e.to_string();
|
||||
return render_new(&state, ¤t_user, Some(&message), Some(&project)).await;
|
||||
return render_new(ctx, &state, ¤t_user, Some(&message), Some(&project)).await;
|
||||
}
|
||||
};
|
||||
project.workflow_id = Some(workflow_id);
|
||||
@@ -292,8 +294,8 @@ pub async fn manage_project(
|
||||
Path(params): Path<ProjectParams>,
|
||||
State(state): State<AppState>,
|
||||
current_user: CurrentUser,
|
||||
) -> Result<Markup, AppError> {
|
||||
let start = Instant::now();
|
||||
ctx: TemplateContext,
|
||||
) -> Result<Response, AppError> {
|
||||
let Some(project_info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await?
|
||||
else {
|
||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||
@@ -302,7 +304,7 @@ pub async fn manage_project(
|
||||
return Err(AppError::Status(StatusCode::FORBIDDEN));
|
||||
}
|
||||
|
||||
Ok(render_manage_project(start, &state, &project_info, ¤t_user, Message::None).await)
|
||||
render_manage_project(ctx, &state, &project_info, ¤t_user, Message::None).await
|
||||
}
|
||||
|
||||
enum Message {
|
||||
@@ -324,12 +326,12 @@ fn render_message(message: &Message) -> Markup {
|
||||
}
|
||||
|
||||
async fn render_manage_project(
|
||||
start: Instant,
|
||||
mut ctx: TemplateContext,
|
||||
state: &AppState,
|
||||
project_info: &ProjectInfo,
|
||||
current_user: &CurrentUser,
|
||||
message: Message,
|
||||
) -> Markup {
|
||||
) -> Result<Response, AppError> {
|
||||
let project_short_name = project_info.project.short_name();
|
||||
let project_manage_path =
|
||||
format!("/manage/{}/{}", project_info.project.owner, project_info.project.repo);
|
||||
@@ -349,15 +351,15 @@ async fn render_manage_project(
|
||||
None
|
||||
};
|
||||
|
||||
html! {
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { (project_short_name) " • Manage" }
|
||||
(header())
|
||||
(chunks("main", true).await)
|
||||
(chunks("manage", true).await)
|
||||
(ctx.header().await)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
(ctx.chunks("manage", Load::Deferred).await)
|
||||
}
|
||||
body {
|
||||
header {
|
||||
@@ -458,9 +460,10 @@ async fn render_manage_project(
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer(start, Some(current_user)))
|
||||
(ctx.footer(Some(current_user)))
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok((ctx, rendered).into_response())
|
||||
}
|
||||
|
||||
fn form_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
@@ -535,11 +538,11 @@ pub async fn manage_project_save(
|
||||
}
|
||||
|
||||
pub async fn manage_project_refresh(
|
||||
ctx: TemplateContext,
|
||||
Path(params): Path<ProjectParams>,
|
||||
State(state): State<AppState>,
|
||||
current_user: CurrentUser,
|
||||
) -> Result<Markup, AppError> {
|
||||
let start = Instant::now();
|
||||
) -> Result<Response, AppError> {
|
||||
let Some(project_info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await?
|
||||
else {
|
||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||
@@ -563,5 +566,5 @@ pub async fn manage_project_refresh(
|
||||
Message::Error(format!("Failed to refresh project: {}", e))
|
||||
}
|
||||
};
|
||||
Ok(render_manage_project(start, &state, &project_info, ¤t_user, message).await)
|
||||
render_manage_project(ctx, &state, &project_info, ¤t_user, message).await
|
||||
}
|
||||
|
||||
@@ -2,17 +2,20 @@ use std::str::FromStr;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
http::{HeaderMap, header},
|
||||
extract::Request,
|
||||
http::{HeaderMap, HeaderValue, header},
|
||||
routing::{get, post},
|
||||
};
|
||||
use decomp_dev_images::image_mime_from_ext;
|
||||
use mime::Mime;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
mod auth;
|
||||
mod common;
|
||||
pub mod csp;
|
||||
mod manage;
|
||||
mod project;
|
||||
mod report;
|
||||
@@ -20,7 +23,31 @@ mod treemap;
|
||||
|
||||
pub fn build_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest_service("/static", ServeDir::new("dist/static"))
|
||||
.nest_service(
|
||||
"/static",
|
||||
<ServeDir as ServiceExt<Request>>::map_response(
|
||||
ServeDir::new("dist/static"),
|
||||
|mut response| {
|
||||
// Cache static (hashed) files for a year, mark immutable
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
||||
);
|
||||
response
|
||||
},
|
||||
),
|
||||
)
|
||||
.fallback_service(<ServeDir as ServiceExt<Request>>::map_response(
|
||||
ServeDir::new("dist"),
|
||||
|mut response| {
|
||||
// Cache non-hashed public files for a day, mark must-revalidate
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=86400, must-revalidate"),
|
||||
);
|
||||
response
|
||||
},
|
||||
))
|
||||
.route("/robots.txt", get(common::get_robots))
|
||||
.route("/api/github/webhook", post(decomp_dev_github::webhook::webhook))
|
||||
.route("/api/github/oauth", get(decomp_dev_auth::oauth))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{str::FromStr, sync::Arc, time::Instant};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
@@ -20,16 +20,14 @@ use url::Url;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
handlers::common::{
|
||||
chunks, code_progress_sections, date, footer, header, nav_links, size, timeago,
|
||||
},
|
||||
handlers::common::{Load, ProgressSections, TemplateContext, date, nav_links, size, timeago},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ProjectInfoContext {
|
||||
project: Project,
|
||||
commit: Commit,
|
||||
measures: Measures,
|
||||
code_progress: ProgressSections,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -59,12 +57,12 @@ pub struct ProgressSection {
|
||||
}
|
||||
|
||||
pub async fn get_projects(
|
||||
mut ctx: TemplateContext,
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<ProjectsQuery>,
|
||||
FullUri(uri): FullUri,
|
||||
current_user: Option<CurrentUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
let start = Instant::now();
|
||||
let projects = state.db.get_projects().await?;
|
||||
let mut out = projects
|
||||
.iter()
|
||||
@@ -74,6 +72,7 @@ pub async fn get_projects(
|
||||
project: p.project.clone(),
|
||||
commit: commit.clone(),
|
||||
measures: Default::default(),
|
||||
code_progress: Default::default(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -108,6 +107,7 @@ pub async fn get_projects(
|
||||
Ok((info, Ok(Some(file)))) => {
|
||||
if let Some(c) = out.iter_mut().find(|i| i.project.id == info.project.id) {
|
||||
c.measures = *file.report.measures(info.project.default_category.as_deref());
|
||||
c.code_progress = ctx.code_progress_sections(&c.measures);
|
||||
}
|
||||
}
|
||||
Ok((info, Ok(None))) => {
|
||||
@@ -122,6 +122,11 @@ pub async fn get_projects(
|
||||
}
|
||||
}
|
||||
|
||||
let mut combined_styles = ProgressSections { nonce: ctx.nonce.clone(), ..Default::default() };
|
||||
for info in &mut out {
|
||||
combined_styles.width_classes.append(&mut info.code_progress.width_classes);
|
||||
}
|
||||
|
||||
let current_sort_key = query.sort.as_deref().unwrap_or("updated");
|
||||
let current_sort = SORT_OPTIONS
|
||||
.iter()
|
||||
@@ -161,8 +166,8 @@ pub async fn get_projects(
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { "Projects • decomp.dev" }
|
||||
(header())
|
||||
(chunks("main", true).await)
|
||||
(ctx.header().await)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
meta name="description" content="Decompilation progress reports";
|
||||
meta property="og:title" content="Decompilation progress reports";
|
||||
meta property="og:description" content="Progress reports for matching decompilation projects";
|
||||
@@ -221,15 +226,16 @@ pub async fn get_projects(
|
||||
}
|
||||
}
|
||||
}
|
||||
(combined_styles)
|
||||
@for project in out {
|
||||
(project_fragment(project, current_sort, &canonical_url))
|
||||
}
|
||||
}
|
||||
(footer(start, current_user.as_ref()))
|
||||
(ctx.footer(current_user.as_ref()))
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(rendered.into_response())
|
||||
Ok((ctx, rendered).into_response())
|
||||
}
|
||||
|
||||
fn project_fragment(
|
||||
@@ -270,7 +276,7 @@ fn project_fragment(
|
||||
}
|
||||
}
|
||||
}
|
||||
(code_progress_sections(&info.measures))
|
||||
(info.code_progress)
|
||||
small class="muted" {
|
||||
span title=(date(info.commit.timestamp)) { "Updated " (timeago(info.commit.timestamp)) }
|
||||
" in commit "
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, iter, time::Instant};
|
||||
use std::{borrow::Cow, iter};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
@@ -18,7 +18,7 @@ use decomp_dev_images::{
|
||||
treemap::{layout_units, unit_color},
|
||||
};
|
||||
use image::ImageFormat;
|
||||
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||
use maud::{DOCTYPE, PreEscaped, html};
|
||||
use mime::Mime;
|
||||
use objdiff_core::bindings::report::{Measures, ReportCategory, ReportUnit};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -28,9 +28,7 @@ use url::Url;
|
||||
use super::{parse_accept, treemap};
|
||||
use crate::{
|
||||
AppState,
|
||||
handlers::common::{
|
||||
chunks, code_progress_sections, data_progress_sections, footer, header, nav_links, size,
|
||||
},
|
||||
handlers::common::{Load, TemplateContext, escape_script, nav_links, size},
|
||||
proto::{PROTOBUF, Protobuf},
|
||||
};
|
||||
|
||||
@@ -190,8 +188,8 @@ pub async fn get_report(
|
||||
FullUri(uri): FullUri,
|
||||
State(state): State<AppState>,
|
||||
current_user: Option<CurrentUser>,
|
||||
ctx: TemplateContext,
|
||||
) -> Result<Response, AppError> {
|
||||
let start = Instant::now();
|
||||
let (params, ext) = extract_extension(params);
|
||||
let acceptable = parse_accept(&headers, ext.as_deref());
|
||||
if acceptable.is_empty() {
|
||||
@@ -230,11 +228,9 @@ pub async fn get_report(
|
||||
let scope = apply_scope(&report, &project_info, &query)?;
|
||||
match query.mode.as_deref().unwrap_or("report").to_ascii_lowercase().as_str() {
|
||||
"shield" => mode_shield(&scope, query, &acceptable),
|
||||
"report" => mode_report(&scope, &state, uri, query, start, &acceptable, current_user).await,
|
||||
"report" => mode_report(&scope, &state, uri, query, ctx, &acceptable, current_user).await,
|
||||
"measures" => mode_measures(&scope, &acceptable),
|
||||
"history" => {
|
||||
mode_history(&scope, &state, uri, query, start, &acceptable, current_user).await
|
||||
}
|
||||
"history" => mode_history(&scope, &state, uri, query, ctx, &acceptable, current_user).await,
|
||||
_ => Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
||||
}
|
||||
}
|
||||
@@ -245,7 +241,7 @@ async fn mode_report(
|
||||
state: &AppState,
|
||||
uri: Uri,
|
||||
query: ReportQuery,
|
||||
start: Instant,
|
||||
ctx: TemplateContext,
|
||||
acceptable: &[Mime],
|
||||
current_user: Option<CurrentUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
@@ -253,8 +249,7 @@ async fn mode_report(
|
||||
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||
{
|
||||
let rendered = render_report(scope, state, uri, current_user, start).await?;
|
||||
return Ok(rendered.into_response());
|
||||
return render_report(scope, state, uri, current_user, ctx).await;
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||
let flattened = scope.report.report.flatten();
|
||||
return Ok(Json(flattened).into_response());
|
||||
@@ -340,7 +335,7 @@ async fn mode_history(
|
||||
state: &AppState,
|
||||
uri: Uri,
|
||||
query: ReportQuery,
|
||||
start: Instant,
|
||||
ctx: TemplateContext,
|
||||
acceptable: &[Mime],
|
||||
current_user: Option<CurrentUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
@@ -385,7 +380,7 @@ async fn mode_history(
|
||||
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||
{
|
||||
let rendered = render_history(scope, state, uri, current_user, start, result).await?;
|
||||
let rendered = render_history(scope, state, uri, current_user, ctx, result).await?;
|
||||
return Ok(rendered.into_response());
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||
return Ok(Json(result).into_response());
|
||||
@@ -536,8 +531,8 @@ async fn render_report(
|
||||
state: &AppState,
|
||||
uri: Uri,
|
||||
current_user: Option<CurrentUser>,
|
||||
start: Instant,
|
||||
) -> Result<Markup> {
|
||||
mut ctx: TemplateContext,
|
||||
) -> Result<Response, AppError> {
|
||||
let Scope { report, project_info, measures, current_category, current_unit, units, label } =
|
||||
scope;
|
||||
|
||||
@@ -656,22 +651,28 @@ async fn render_report(
|
||||
} else {
|
||||
project_info.project.name()
|
||||
};
|
||||
let project_short_name = if let Some(label) = label {
|
||||
Cow::Owned(format!("{} ({})", project_info.project.short_name(), label))
|
||||
let project_short_name = project_info.project.short_name();
|
||||
let project_short_name_with_label = if let Some(label) = label {
|
||||
Cow::Owned(format!("{} ({})", project_short_name, label))
|
||||
} else {
|
||||
Cow::Borrowed(project_info.project.short_name())
|
||||
Cow::Borrowed(project_short_name)
|
||||
};
|
||||
|
||||
Ok(html! {
|
||||
// Load blocking resources first so we don't duplicate them
|
||||
let header = ctx.header().await;
|
||||
let report_chunks = ctx.chunks("report", Load::Blocking).await;
|
||||
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { (project_short_name) " • Progress Report" }
|
||||
(header())
|
||||
(chunks("main", true).await)
|
||||
title { (project_short_name_with_label) " • Progress Report" }
|
||||
(header)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
(ctx.chunks("report", Load::Preload).await)
|
||||
meta name="description" content=(format!("Decompilation progress report for {project_name}"));
|
||||
meta property="og:title" content=(format!("{project_short_name} is {:.2}% decompiled", measures.matched_code_percent));
|
||||
meta property="og:title" content=(format!("{project_short_name_with_label} is {:.2}% decompiled", measures.matched_code_percent));
|
||||
meta property="og:description" content=(format!("Decompilation progress report for {project_name}"));
|
||||
meta property="og:image" content=(image_url);
|
||||
meta property="og:url" content=(canonical_url);
|
||||
@@ -727,7 +728,7 @@ async fn render_report(
|
||||
}
|
||||
}
|
||||
}
|
||||
h3 { (format!("{project_short_name} is {:.2}% decompiled", measures.matched_code_percent)) }
|
||||
h3 { (format!("{project_short_name_with_label} is {:.2}% decompiled", measures.matched_code_percent)) }
|
||||
@if current_unit.is_none() && measures.complete_code_percent > 0.0 {
|
||||
h4 class="muted" { (format!("{:.2}% fully linked", measures.complete_code_percent)) }
|
||||
}
|
||||
@@ -751,14 +752,14 @@ async fn render_report(
|
||||
"Code "
|
||||
small class="muted" { "(" (size(measures.total_code)) ")" }
|
||||
}
|
||||
(code_progress_sections(&measures))
|
||||
(ctx.code_progress_sections(&measures))
|
||||
}
|
||||
@if measures.total_data > 0 {
|
||||
h6 class="report-header" {
|
||||
"Data "
|
||||
small class="muted" { "(" (size(measures.total_data)) ")" }
|
||||
}
|
||||
(data_progress_sections(&measures))
|
||||
(ctx.data_progress_sections(&measures))
|
||||
}
|
||||
h6 class="report-header" { "Commit" }
|
||||
div {
|
||||
@@ -774,24 +775,24 @@ async fn render_report(
|
||||
div role="group" {
|
||||
@if let Some(prev_commit_path) = prev_commit_path {
|
||||
a role="button" class="outline secondary" href=(prev_commit_path) {
|
||||
span .icon-left-open {}
|
||||
span .icon-left-open .md {}
|
||||
" Previous"
|
||||
}
|
||||
} @else {
|
||||
button disabled class="outline secondary" {
|
||||
span .icon-left-open {}
|
||||
span .icon-left-open .md {}
|
||||
" Previous"
|
||||
}
|
||||
}
|
||||
@if let Some(next_commit_path) = next_commit_path {
|
||||
a role="button" class="outline secondary" href=(next_commit_path) {
|
||||
"Next "
|
||||
span .icon-right-open {}
|
||||
span .icon-right-open .md {}
|
||||
}
|
||||
} @else {
|
||||
button disabled class="outline secondary" {
|
||||
"Next "
|
||||
span .icon-right-open {}
|
||||
span .icon-right-open .md {}
|
||||
}
|
||||
}
|
||||
@if let Some(latest_commit_path) = latest_commit_path {
|
||||
@@ -825,22 +826,25 @@ async fn render_report(
|
||||
}
|
||||
}
|
||||
}
|
||||
(chunks("report", false).await)
|
||||
script {
|
||||
(PreEscaped(r#"document.write('<canvas id="treemap" width="100%"></canvas>');drawTreemap("treemap","#))
|
||||
canvas #treemap {}
|
||||
(report_chunks)
|
||||
script nonce=[ctx.nonce.as_deref()] {
|
||||
(PreEscaped(r#"window.units="#))
|
||||
(escape_script(&serde_json::to_string(&units)?))
|
||||
(PreEscaped(r#";drawTreemap("treemap","#))
|
||||
(current_unit.is_none())
|
||||
","
|
||||
(PreEscaped(serde_json::to_string(&units)?))
|
||||
");"
|
||||
(PreEscaped(r#",window.units)"#))
|
||||
}
|
||||
noscript {
|
||||
style nonce=[ctx.nonce.as_deref()] { "canvas{display:none}" }
|
||||
img #treemap src=(image_url) alt="Progress graph";
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer(start, current_user.as_ref()))
|
||||
(ctx.footer(current_user.as_ref()))
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok((ctx, rendered).into_response())
|
||||
}
|
||||
|
||||
async fn render_history(
|
||||
@@ -848,9 +852,9 @@ async fn render_history(
|
||||
_state: &AppState,
|
||||
uri: Uri,
|
||||
current_user: Option<CurrentUser>,
|
||||
start: Instant,
|
||||
mut ctx: TemplateContext,
|
||||
result: Vec<ReportHistoryEntry>,
|
||||
) -> Result<Markup> {
|
||||
) -> Result<Response, AppError> {
|
||||
let Scope { report, project_info, measures, current_category, current_unit, units: _, label } =
|
||||
scope;
|
||||
|
||||
@@ -905,22 +909,28 @@ async fn render_history(
|
||||
} else {
|
||||
project_info.project.name()
|
||||
};
|
||||
let project_short_name = if let Some(label) = label {
|
||||
Cow::Owned(format!("{} ({})", project_info.project.short_name(), label))
|
||||
let project_short_name = project_info.project.short_name();
|
||||
let project_short_name_with_label = if let Some(label) = label {
|
||||
Cow::Owned(format!("{} ({})", project_short_name, label))
|
||||
} else {
|
||||
Cow::Borrowed(project_info.project.short_name())
|
||||
Cow::Borrowed(project_short_name)
|
||||
};
|
||||
|
||||
Ok(html! {
|
||||
// Load blocking resources first so we don't duplicate them
|
||||
let header = ctx.header().await;
|
||||
let history_chunks = ctx.chunks("history", Load::Blocking).await;
|
||||
|
||||
let rendered = html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { (project_short_name) " • Progress History" }
|
||||
(header())
|
||||
(chunks("main", true).await)
|
||||
title { (project_short_name_with_label) " • Progress History" }
|
||||
(header)
|
||||
(ctx.chunks("main", Load::Deferred).await)
|
||||
(ctx.chunks("history", Load::Preload).await)
|
||||
meta name="description" content=(format!("Decompilation progress history for {project_name}"));
|
||||
meta property="og:title" content=(format!("{project_short_name} is {:.2}% decompiled", measures.matched_code_percent));
|
||||
meta property="og:title" content=(format!("{project_short_name_with_label} is {:.2}% decompiled", measures.matched_code_percent));
|
||||
meta property="og:description" content=(format!("Decompilation progress history for {project_name}"));
|
||||
meta property="og:image" content=(image_url);
|
||||
meta property="og:url" content=(canonical_url);
|
||||
@@ -946,7 +956,7 @@ async fn render_history(
|
||||
}
|
||||
}
|
||||
main {
|
||||
h3 { "History for " (project_short_name) }
|
||||
h3 { "History for " (project_short_name_with_label) }
|
||||
details class="dropdown" title="Version" {
|
||||
summary { (report.version) }
|
||||
ul {
|
||||
@@ -969,11 +979,12 @@ async fn render_history(
|
||||
}
|
||||
}
|
||||
}
|
||||
(chunks("history", false).await)
|
||||
script {
|
||||
(PreEscaped(r#"document.write('<div id="chart" width="100%"></div>');renderChart("chart","#))
|
||||
(PreEscaped(serde_json::to_string(&result)?))
|
||||
(PreEscaped(r#");"#))
|
||||
#chart {}
|
||||
(history_chunks)
|
||||
script nonce=[ctx.nonce.as_deref()] {
|
||||
(PreEscaped(r#"window.historyData="#))
|
||||
(escape_script(&serde_json::to_string(&result)?))
|
||||
(PreEscaped(r#";renderChart("chart",window.historyData)"#))
|
||||
}
|
||||
hr;
|
||||
div role="group" {
|
||||
@@ -981,7 +992,8 @@ async fn render_history(
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer(start, current_user.as_ref()))
|
||||
(ctx.footer(current_user.as_ref()))
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok((ctx, rendered).into_response())
|
||||
}
|
||||
|
||||
+48
-11
@@ -16,6 +16,7 @@ use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, FromRef},
|
||||
http::{Method, Request, header},
|
||||
middleware,
|
||||
};
|
||||
use decomp_dev_core::config::{Config, GitHubConfig};
|
||||
use decomp_dev_db::Database;
|
||||
@@ -33,7 +34,7 @@ use tower_sessions_sqlx_store::SqliteStore;
|
||||
use tracing::{Level, Span};
|
||||
use tracing_subscriber::{EnvFilter, filter::LevelFilter};
|
||||
|
||||
use crate::handlers::build_router;
|
||||
use crate::handlers::{build_router, csp::csp_middleware};
|
||||
|
||||
#[derive(Clone, FromRef)]
|
||||
struct AppState {
|
||||
@@ -88,16 +89,51 @@ async fn main() {
|
||||
.await
|
||||
.expect("Failed to create scheduler");
|
||||
|
||||
// Build the router
|
||||
let port = state.config.server.port;
|
||||
let router = app(state, session_store).into_make_service_with_connect_info::<SocketAddr>();
|
||||
|
||||
// Create the listener
|
||||
let mut listener = None;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::fd::{FromRawFd, IntoRawFd};
|
||||
let fds = libsystemd::activation::receive_descriptors_with_names(false)
|
||||
.expect("Failed to receive fds");
|
||||
if let Some((fd, name)) = fds.into_iter().next() {
|
||||
tracing::info!("Listening on {}", name);
|
||||
let std_listener = unsafe { std::net::TcpListener::from_raw_fd(fd.into_raw_fd()) };
|
||||
std_listener.set_nonblocking(true).expect("Failed to set non-blocking");
|
||||
listener =
|
||||
Some(TcpListener::from_std(std_listener).expect("Failed to create listener"));
|
||||
}
|
||||
}
|
||||
let listener = match listener {
|
||||
Some(listener) => listener,
|
||||
None => {
|
||||
let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, port));
|
||||
tracing::info!("Listening on {}", addr);
|
||||
TcpListener::bind(addr).await.expect("bind error")
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
libsystemd::daemon::notify(false, &[libsystemd::daemon::NotifyState::Ready])
|
||||
.expect("Failed to notify");
|
||||
}
|
||||
|
||||
// Run our service
|
||||
let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, state.config.server.port));
|
||||
tracing::info!("Listening on {}", addr);
|
||||
axum::serve(
|
||||
TcpListener::bind(addr).await.expect("bind error"),
|
||||
app(state, session_store).into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
.expect("server error");
|
||||
axum::serve(listener, router)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
.expect("server error");
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
libsystemd::daemon::notify(false, &[libsystemd::daemon::NotifyState::Stopping])
|
||||
.expect("Failed to notify");
|
||||
}
|
||||
|
||||
scheduler.shutdown().await.expect("Failed to shut down scheduler");
|
||||
db.close().await;
|
||||
@@ -120,8 +156,9 @@ fn app(state: AppState, session_store: impl SessionStore + Clone) -> Router {
|
||||
SessionManagerLayer::new(session_store)
|
||||
.with_secure(false)
|
||||
.with_same_site(SameSite::Lax)
|
||||
.with_expiry(Expiry::OnInactivity(time::Duration::days(1))),
|
||||
.with_expiry(Expiry::OnInactivity(time::Duration::days(30))),
|
||||
)
|
||||
.layer(middleware::from_fn(csp_middleware))
|
||||
.compression();
|
||||
let router = build_router();
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
+51
-26
@@ -93,6 +93,12 @@ $breakpoints: (
|
||||
);
|
||||
@use "@picocss/pico/scss/colors" as *;
|
||||
|
||||
@media (min-width: map.get(map.get($breakpoints, "md"), "breakpoint")) {
|
||||
:root {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme=light],
|
||||
:root:not([data-theme=dark]) {
|
||||
--pico-code-kbd-color: #000;
|
||||
@@ -171,8 +177,11 @@ $breakpoints: (
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav {
|
||||
> ul:first-child {
|
||||
nav > ul {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:first-child {
|
||||
> li:first-child * {
|
||||
color: var(--pico-contrast);
|
||||
}
|
||||
@@ -185,6 +194,12 @@ nav {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: map.get(map.get($breakpoints, "lg"), "breakpoint")) {
|
||||
> li:has(+ .md):after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: map.get(map.get($breakpoints, "md"), "breakpoint")) {
|
||||
> li:not(:first-child) {
|
||||
display: none;
|
||||
@@ -203,13 +218,8 @@ nav {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@keyframes progress-stripes {
|
||||
0% {
|
||||
background-position: 0 0
|
||||
}
|
||||
to {
|
||||
background-position: calc(2.5rem) 0
|
||||
}
|
||||
pre {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
$progress-height: 2rem;
|
||||
@@ -223,7 +233,6 @@ $progress-height: 2rem;
|
||||
margin-bottom: var(--pico-spacing);
|
||||
|
||||
.progress-section.striped {
|
||||
//animation: progress-stripes 1s linear infinite;
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
var(--progress-stripe-color) 25%,
|
||||
@@ -238,25 +247,25 @@ $progress-height: 2rem;
|
||||
}
|
||||
|
||||
&.code {
|
||||
.progress-section:nth-child(1) {
|
||||
.progress-section:nth-of-type(1) {
|
||||
background-color: $jade-400;
|
||||
}
|
||||
|
||||
.progress-section:nth-child(2) {
|
||||
.progress-section:nth-of-type(2) {
|
||||
background-color: $jade-500;
|
||||
}
|
||||
|
||||
.progress-section:nth-child(3) {
|
||||
.progress-section:nth-of-type(3) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.data {
|
||||
.progress-section:nth-child(1) {
|
||||
.progress-section:nth-of-type(1) {
|
||||
background-color: $azure-400;
|
||||
}
|
||||
|
||||
.progress-section:nth-child(2) {
|
||||
.progress-section:nth-of-type(2) {
|
||||
background-color: $azure-500;
|
||||
}
|
||||
}
|
||||
@@ -323,13 +332,13 @@ $unit-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: map.get(map.get($breakpoints, "md"), "breakpoint")) {
|
||||
@media (max-width: map.get(map.get($breakpoints, "lg"), "breakpoint")) {
|
||||
.md {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: map.get(map.get($breakpoints, "md"), "breakpoint")) {
|
||||
@media (min-width: map.get(map.get($breakpoints, "lg"), "breakpoint")) {
|
||||
.sm {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -339,19 +348,30 @@ $unit-size: 0.75rem;
|
||||
margin-top: calc(var(--pico-block-spacing-vertical) * 2);
|
||||
}
|
||||
|
||||
footer {
|
||||
body > footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
// Log out button
|
||||
input[type="submit"] {
|
||||
padding: .3em;
|
||||
margin: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
width: auto;
|
||||
font-size: .875em;
|
||||
height: auto;
|
||||
form {
|
||||
display: inline;
|
||||
|
||||
input[type="submit"] {
|
||||
padding: .3em;
|
||||
margin: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
width: auto;
|
||||
font-size: .875em;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: map.get(map.get($breakpoints, "md"), "breakpoint")) {
|
||||
body > footer {
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,6 +436,7 @@ footer {
|
||||
.actions {
|
||||
float: right;
|
||||
position: relative;
|
||||
margin-inline-start: 1em;
|
||||
|
||||
.dropdown {
|
||||
summary {
|
||||
@@ -431,3 +452,7 @@ footer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
details.dropdown {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
if (import.meta.env.DEV) {
|
||||
// For Rspack-loaded scripts, we need to set the nonce attribute on script tags
|
||||
const currentScript = document.currentScript;
|
||||
if (currentScript?.nonce) {
|
||||
__webpack_nonce__ = currentScript.nonce;
|
||||
// Additionally, for HMR support with Cross-Origin-Embedder-Policy (COEP),
|
||||
// we need to set the crossorigin attribute on script tags inserted by HMR
|
||||
const originalAppendChild = Element.prototype.appendChild;
|
||||
Element.prototype.appendChild = function <T extends Node>(node: T): T {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'SCRIPT') {
|
||||
const script = node as unknown as HTMLScriptElement;
|
||||
script.setAttribute('crossorigin', '');
|
||||
}
|
||||
return originalAppendChild.call(this, node) as T;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Set the theme based on localStorage
|
||||
let theme: string | null = null;
|
||||
try {
|
||||
theme = localStorage.getItem('theme');
|
||||
} catch (_) {}
|
||||
if (theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
Vendored
+2
@@ -1,3 +1,5 @@
|
||||
/// <reference types="@rsbuild/core/types" />
|
||||
|
||||
type Unit = {
|
||||
name: string;
|
||||
fuzzy_match_percent: number;
|
||||
|
||||
+23
-19
@@ -15,19 +15,26 @@ function percentValue(
|
||||
return rawValue == null ? '' : `${rawValue.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
window.renderChart = (id: string, data: ReportHistoryEntry[]) => {
|
||||
function renderChart(id: string, data: ReportHistoryEntry[]) {
|
||||
const chart = document.getElementById(id);
|
||||
if (!chart) {
|
||||
console.error(`Chart element with id ${id} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
data.reverse();
|
||||
|
||||
function getSize() {
|
||||
const container = chart!.parentElement;
|
||||
if (container) {
|
||||
return { width: container.offsetWidth, height };
|
||||
}
|
||||
return { width: 600, height };
|
||||
}
|
||||
|
||||
const u = new uPlot(
|
||||
{
|
||||
id: id,
|
||||
width: 600,
|
||||
height: height,
|
||||
...getSize(),
|
||||
scales: {
|
||||
x: {
|
||||
time: true,
|
||||
@@ -95,25 +102,22 @@ window.renderChart = (id: string, data: ReportHistoryEntry[]) => {
|
||||
],
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
[
|
||||
data.map((e) => Date.parse(e.timestamp) / 1000),
|
||||
data.map((e) => e.measures.fuzzy_match_percent || null),
|
||||
data.map((e) => e.measures.matched_code_percent || null),
|
||||
data.map((e) => e.measures.matched_data_percent || null),
|
||||
data.map((e) => e.measures.complete_code_percent || null),
|
||||
data.map((e) => e.measures.complete_data_percent || null),
|
||||
],
|
||||
chart,
|
||||
);
|
||||
|
||||
function updateSize() {
|
||||
const container = chart?.parentElement;
|
||||
if (container) {
|
||||
u.setSize({ width: container.offsetWidth, height });
|
||||
}
|
||||
u.setSize(getSize());
|
||||
}
|
||||
|
||||
window.addEventListener('resize', updateSize);
|
||||
updateSize();
|
||||
u.setData([
|
||||
data.map((e) => Date.parse(e.timestamp) / 1000),
|
||||
data.map((e) => e.measures.fuzzy_match_percent || null),
|
||||
data.map((e) => e.measures.matched_code_percent || null),
|
||||
data.map((e) => e.measures.matched_data_percent || null),
|
||||
data.map((e) => e.measures.complete_code_percent || null),
|
||||
data.map((e) => e.measures.complete_data_percent || null),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
window.renderChart = renderChart;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user