Skip to content

Commit 67ea4b6

Browse files
authored
Merge pull request #20 from msalinas92/feature/cachebolt-ui
Feature/cachebolt UI
2 parents c05fea7 + 86c91e1 commit 67ea4b6

File tree

24 files changed

+5967
-8
lines changed

24 files changed

+5967
-8
lines changed

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ jobs:
6767
steps:
6868
- uses: actions/checkout@v4
6969

70+
- name: Set up Node.js
71+
uses: actions/setup-node@v4
72+
with:
73+
node-version: 20
74+
75+
- name: Build Astro UI
76+
run: |
77+
cd ui
78+
npm ci
79+
npm run build
80+
cd ..
81+
7082
- name: Install cross-compilers and dependencies
7183
if: runner.os == 'Linux'
7284
run: |

Cargo.lock

Lines changed: 90 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ metrics-exporter-prometheus = "0.17.0"
5858
futures = "0.3"
5959
rand = "0.8"
6060
tower = "0.5.2"
61-
61+
tower-http = { version = "0.4", features = ["cors"] }
62+
rust-embed = "8.2.0"
63+
mime_guess = "2.0"
6264

6365
[dev-dependencies]
6466
tokio = { version = "1", features = ["macros", "rt-multi-thread", "test-util"] }

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,29 @@ docker run --rm -p 3000:3000 \
6060
- ⏱️ Latency-based failover policies (regex route rules)
6161
- 🧠 Smart fallback if upstreams are slow or unavailable
6262

63+
64+
## 🖥️ Web UI (Built-in Admin Interface)
65+
66+
CacheBolt comes with a built-in **graphical admin interface** accessible via your browser.
67+
This lightweight UI is bundled directly into the binary—no additional server or frontend hosting is required.
68+
69+
### 🔗 Accessing the UI
70+
71+
Once CacheBolt is running, open:
72+
73+
http://localhost:3000/cb-admin
74+
75+
### 🧰 Available Features
76+
77+
- 🧠 **View memory cache entries** in real time
78+
- 🧹 **Clear in-memory or persistent cache** with a single click
79+
- 📊 **Inspect memory usage, TTL, and sizes**
80+
- 🛠️ Fully static and bundled – served directly from the binary
81+
82+
This interface is useful for both **debugging** and **administrative operations** in production environments.
83+
84+
![CacheBolt Web UI](https://raw.githubusercontent.com/msalinas92/CacheBolt/refs/heads/master/docs/screenshot-1.png)
85+
6386
---
6487
## 🔁 Request Flow
6588

docs/screenshot-1.png

165 KB
Loading

src/admin/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@
1313
// limitations under the License.
1414

1515
pub mod clean;
16-
pub mod status_memory;
16+
pub mod status_memory;
17+
pub mod ui;

src/admin/ui.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use axum::{
2+
extract::Path,
3+
http::{header, Response, StatusCode},
4+
response::IntoResponse,
5+
};
6+
use mime_guess::from_path;
7+
use rust_embed::RustEmbed;
8+
9+
#[derive(RustEmbed)]
10+
#[folder = "ui/dist/cb-admin/"] // Ruta relativa al Cargo.toml
11+
pub struct EmbeddedAssets;
12+
13+
/// Servidor de archivos embebidos para `/cb-admin/*path`
14+
/// Soporta rutas como:
15+
/// - `/cb-admin`
16+
/// - `/cb-admin/`
17+
/// - `/cb-admin/index.html`
18+
/// - `/cb-admin/cache` -> `cache/index.html`
19+
/// - `/cb-admin/cache/` -> `cache/index.html`
20+
pub async fn embedded_ui_handler(Path(path): Path<String>) -> impl IntoResponse {
21+
tracing::info!("📦 UI embedded request for: {}", path);
22+
23+
// Elimina "/" inicial para estandarizar
24+
let clean_path = path.trim_start_matches('/');
25+
26+
// Lógica para resolver correctamente rutas tipo `/cb-admin/cache`
27+
let resolved_path = if clean_path.is_empty() {
28+
"index.html".to_string()
29+
} else if EmbeddedAssets::get(clean_path).is_some() {
30+
clean_path.to_string()
31+
} else {
32+
let with_index = format!("{}/index.html", clean_path);
33+
if EmbeddedAssets::get(&with_index).is_some() {
34+
with_index
35+
} else {
36+
clean_path.to_string() // Intento final (puede fallar)
37+
}
38+
};
39+
40+
// Buscar el archivo embebido
41+
match EmbeddedAssets::get(&resolved_path) {
42+
Some(content) => {
43+
let mime = from_path(&resolved_path).first_or_octet_stream();
44+
Response::builder()
45+
.header(header::CONTENT_TYPE, mime.as_ref())
46+
.body(axum::body::Body::from(content.data.into_owned()))
47+
.unwrap()
48+
}
49+
None => {
50+
// Fallback a index.html para SPA si existe
51+
if let Some(index) = EmbeddedAssets::get("index.html") {
52+
return Response::builder()
53+
.header(header::CONTENT_TYPE, "text/html")
54+
.body(axum::body::Body::from(index.data.into_owned()))
55+
.unwrap();
56+
}
57+
58+
// Si ni siquiera hay index, error 404
59+
Response::builder()
60+
.status(StatusCode::NOT_FOUND)
61+
.body(axum::body::Body::from("404 Not Found"))
62+
.unwrap()
63+
}
64+
}
65+
}
66+
67+
/// Sirve el archivo `index.html` directamente para rutas `/cb-admin` o `/cb-admin/`
68+
pub async fn embedded_ui_index() -> impl IntoResponse {
69+
match EmbeddedAssets::get("index.html") {
70+
Some(content) => Response::builder()
71+
.header(header::CONTENT_TYPE, "text/html")
72+
.body(axum::body::Body::from(content.data.into_owned()))
73+
.unwrap(),
74+
None => Response::builder()
75+
.status(StatusCode::NOT_FOUND)
76+
.body("404 Not Found".into())
77+
.unwrap(),
78+
}
79+
}

src/main.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@
1717
// ----------------------
1818
// These are internal modules for handling the proxy logic, caching layers,
1919
// configuration loading, and in-memory eviction based on memory pressure.
20+
mod admin;
2021
mod config;
2122
mod eviction;
2223
mod memory;
2324
mod proxy;
2425
mod rules;
2526
mod storage;
26-
mod admin;
2727

2828
// ----------------------
2929
// External dependencies
3030
// ----------------------
31-
use axum::{Router, routing::get, routing::delete}; // Axum: Web framework for routing and request handling
31+
use axum::{Router, routing::delete, routing::get}; // Axum: Web framework for routing and request handling
3232
use hyper::Server; // Hyper: High-performance HTTP server
3333
use std::{net::SocketAddr, process::exit}; // Network + system utilities
3434

@@ -38,6 +38,7 @@ use tracing_subscriber::EnvFilter; // Log filtering via LOG_LEVEL
3838

3939
use crate::admin::clean::invalidate_handler;
4040
use crate::admin::status_memory::get_memory_cache_status;
41+
use crate::admin::ui::{embedded_ui_handler, embedded_ui_index};
4142
// ----------------------
4243
// Internal dependencies
4344
// ----------------------
@@ -46,6 +47,9 @@ use crate::eviction::start_background_eviction_task; // Memory pressure eviction
4647
use crate::storage::{azure, gcs, s3}; // Persistent storage backends
4748
use metrics_exporter_prometheus::PrometheusBuilder;
4849

50+
use hyper::http::{HeaderValue, Method, header};
51+
use tower_http::cors::CorsLayer;
52+
4953
/// ----------------------------
5054
/// CLI ARGUMENT STRUCTURE
5155
/// ----------------------------
@@ -191,12 +195,21 @@ async fn main() {
191195
// 7. Define Axum router with a single wildcard route
192196
// All incoming GET requests will be handled by the proxy logic.
193197
// ------------------------------------------------------
198+
let cors = CorsLayer::new()
199+
.allow_origin("http://localhost:4321".parse::<HeaderValue>().unwrap()) // o use HeaderValue::from_static(...)
200+
.allow_methods([Method::GET, Method::POST, Method::DELETE])
201+
.allow_headers([header::CONTENT_TYPE]);
202+
194203
let app = Router::new()
204+
.route("/cb-admin/api/cache", delete(invalidate_handler))
205+
.route("/cb-admin/api/status", get(get_memory_cache_status))
206+
.route("/cb-admin", get(embedded_ui_index))
207+
.route("/cb-admin/", get(embedded_ui_index))
208+
.route("/cb-admin/*path", get(embedded_ui_handler))
195209
.route("/metrics", get(move || async move { handle.render() }))
196-
.route("/", get(proxy::proxy_handler))
210+
.route("/", get(proxy::proxy_handler))
197211
.route("/*path", get(proxy::proxy_handler))
198-
.route("/admin/cache", delete(invalidate_handler))
199-
.route("/admin/status-memory", get(get_memory_cache_status));
212+
.layer(cors);
200213

201214
// ------------------------------------------------------
202215
// 8. Bind the server to all interfaces on port 3000

ui/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# build output
2+
dist/
3+
4+
# generated types
5+
.astro/
6+
7+
# dependencies
8+
node_modules/
9+
10+
# logs
11+
npm-debug.log*
12+
yarn-debug.log*
13+
yarn-error.log*
14+
pnpm-debug.log*
15+
16+
# environment variables
17+
.env
18+
.env.production
19+
20+
# macOS-specific files
21+
.DS_Store
22+
23+
# jetbrains setting folder
24+
.idea/

0 commit comments

Comments
 (0)