Add dashboard metrics, activity log, and dark theme

- Add DashboardMetrics for tracking downloads/uploads/cache hits per registry
- Add ActivityLog for recent activity with bounded size (50 entries)
- Instrument Docker, npm, Maven, and Cargo handlers with metrics
- Add /api/ui/dashboard endpoint with global stats and activity
- Implement dark theme dashboard with real-time polling (5s interval)
- Add mount points table showing registry paths and proxy upstreams
This commit is contained in:
2026-01-26 16:21:25 +00:00
parent f1cda800a2
commit a13d7b8cfc
11 changed files with 1013 additions and 88 deletions

View File

@@ -1,3 +1,4 @@
use crate::activity_log::{ActionType, ActivityEntry};
use crate::AppState;
use axum::{
extract::{Path, State},
@@ -37,7 +38,17 @@ async fn download(
crate_name, version, crate_name, version
);
match state.storage.get(&key).await {
Ok(data) => (StatusCode::OK, data).into_response(),
Ok(data) => {
state.metrics.record_download("cargo");
state.metrics.record_cache_hit();
state.activity.push(ActivityEntry::new(
ActionType::Pull,
format!("{}@{}", crate_name, version),
"cargo",
"LOCAL",
));
(StatusCode::OK, data).into_response()
}
Err(_) => StatusCode::NOT_FOUND.into_response(),
}
}

View File

@@ -1,3 +1,4 @@
use crate::activity_log::{ActionType, ActivityEntry};
use crate::validation::{validate_digest, validate_docker_name, validate_docker_reference};
use crate::AppState;
use axum::{
@@ -75,12 +76,22 @@ async fn download_blob(
let key = format!("docker/{}/blobs/{}", name, digest);
match state.storage.get(&key).await {
Ok(data) => (
StatusCode::OK,
[(header::CONTENT_TYPE, "application/octet-stream")],
data,
)
.into_response(),
Ok(data) => {
state.metrics.record_download("docker");
state.metrics.record_cache_hit();
state.activity.push(ActivityEntry::new(
ActionType::Pull,
format!("{}@{}", name, &digest[..19.min(digest.len())]),
"docker",
"LOCAL",
));
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/octet-stream")],
data,
)
.into_response()
}
Err(_) => StatusCode::NOT_FOUND.into_response(),
}
}
@@ -176,6 +187,13 @@ async fn upload_blob(
let key = format!("docker/{}/blobs/{}", name, digest);
match state.storage.put(&key, &data).await {
Ok(()) => {
state.metrics.record_upload("docker");
state.activity.push(ActivityEntry::new(
ActionType::Push,
format!("{}@{}", name, &digest[..19.min(digest.len())]),
"docker",
"LOCAL",
));
let location = format!("/v2/{}/blobs/{}", name, digest);
(StatusCode::CREATED, [(header::LOCATION, location)]).into_response()
}
@@ -197,6 +215,15 @@ async fn get_manifest(
let key = format!("docker/{}/manifests/{}.json", name, reference);
match state.storage.get(&key).await {
Ok(data) => {
state.metrics.record_download("docker");
state.metrics.record_cache_hit();
state.activity.push(ActivityEntry::new(
ActionType::Pull,
format!("{}:{}", name, reference),
"docker",
"LOCAL",
));
// Calculate digest for Docker-Content-Digest header
use sha2::Digest;
let digest = format!("sha256:{:x}", sha2::Sha256::digest(&data));
@@ -245,6 +272,14 @@ async fn put_manifest(
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
state.metrics.record_upload("docker");
state.activity.push(ActivityEntry::new(
ActionType::Push,
format!("{}:{}", name, reference),
"docker",
"LOCAL",
));
let location = format!("/v2/{}/manifests/{}", name, reference);
(
StatusCode::CREATED,

View File

@@ -1,3 +1,4 @@
use crate::activity_log::{ActionType, ActivityEntry};
use crate::AppState;
use axum::{
body::Bytes,
@@ -19,8 +20,19 @@ pub fn routes() -> Router<Arc<AppState>> {
async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> Response {
let key = format!("maven/{}", path);
// Extract artifact name for logging (last 2-3 path components)
let artifact_name = path.split('/').rev().take(3).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("/");
// Try local storage first
if let Ok(data) = state.storage.get(&key).await {
state.metrics.record_download("maven");
state.metrics.record_cache_hit();
state.activity.push(ActivityEntry::new(
ActionType::CacheHit,
artifact_name,
"maven",
"CACHE",
));
return with_content_type(&path, data).into_response();
}
@@ -30,6 +42,15 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
match fetch_from_proxy(&url, state.config.maven.proxy_timeout).await {
Ok(data) => {
state.metrics.record_download("maven");
state.metrics.record_cache_miss();
state.activity.push(ActivityEntry::new(
ActionType::ProxyFetch,
artifact_name,
"maven",
"PROXY",
));
// Cache in local storage (fire and forget)
let storage = state.storage.clone();
let key_clone = key.clone();
@@ -53,8 +74,21 @@ async fn upload(
body: Bytes,
) -> StatusCode {
let key = format!("maven/{}", path);
// Extract artifact name for logging
let artifact_name = path.split('/').rev().take(3).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("/");
match state.storage.put(&key, &body).await {
Ok(()) => StatusCode::CREATED,
Ok(()) => {
state.metrics.record_upload("maven");
state.activity.push(ActivityEntry::new(
ActionType::Push,
artifact_name,
"maven",
"LOCAL",
));
StatusCode::CREATED
}
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}

View File

@@ -1,3 +1,4 @@
use crate::activity_log::{ActionType, ActivityEntry};
use crate::AppState;
use axum::{
body::Bytes,
@@ -29,8 +30,25 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
format!("npm/{}/metadata.json", path)
};
// Extract package name for logging
let package_name = if is_tarball {
path.split("/-/").next().unwrap_or(&path).to_string()
} else {
path.clone()
};
// Try local storage first
if let Ok(data) = state.storage.get(&key).await {
if is_tarball {
state.metrics.record_download("npm");
state.metrics.record_cache_hit();
state.activity.push(ActivityEntry::new(
ActionType::CacheHit,
package_name,
"npm",
"CACHE",
));
}
return with_content_type(is_tarball, data).into_response();
}
@@ -45,6 +63,17 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
};
if let Ok(data) = fetch_from_proxy(&url, state.config.npm.proxy_timeout).await {
if is_tarball {
state.metrics.record_download("npm");
state.metrics.record_cache_miss();
state.activity.push(ActivityEntry::new(
ActionType::ProxyFetch,
package_name,
"npm",
"PROXY",
));
}
// Cache in local storage (fire and forget)
let storage = state.storage.clone();
let key_clone = key.clone();