Skip to content

Commit b7a2eb2

Browse files
committed
Polish HTML landing/docs with live metrics and repo link
1 parent 92afdc6 commit b7a2eb2

4 files changed

Lines changed: 156 additions & 30 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ FastAPI-powered micro CDN for storing and serving uploaded assets. Files are wri
1010
- Live metrics (uploads, downloads, cleanups) exposed on the home page.
1111
- SQLite by default, with environment overrides for production databases.
1212

13+
## Live Demo
14+
- Try the hosted instance at https://cdn.lunaticsm.web.id to see the animated metrics, HTML landing page, and API guide in action.
15+
1316
## Getting Started
1417

1518
```bash

app/main.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from datetime import datetime
23
from pathlib import Path
34
from string import Template
45
from urllib.parse import quote
@@ -93,9 +94,10 @@ async def home():
9394
"index.html",
9495
{
9596
"max_file_mb": f"{MAX_FILE_SIZE_MB:.1f}",
96-
"uploads": stats.get("uploads", 0),
97-
"downloads": stats.get("downloads", 0),
98-
"deleted": stats.get("deleted", 0),
97+
"uploads": str(stats.get("uploads", 0)),
98+
"downloads": str(stats.get("downloads", 0)),
99+
"deleted": str(stats.get("deleted", 0)),
100+
"year": str(datetime.utcnow().year),
99101
},
100102
)
101103
return HTMLResponse(content=html)
@@ -105,10 +107,18 @@ async def home():
105107
async def api_info():
106108
html = render_template(
107109
"api.html",
108-
{"max_file_mb": f"{MAX_FILE_SIZE_MB:.1f}", "rate_limit": RATE_LIMIT_PER_MINUTE},
110+
{"max_file_mb": f"{MAX_FILE_SIZE_MB:.1f}", "rate_limit": str(RATE_LIMIT_PER_MINUTE)},
109111
)
110112
return HTMLResponse(content=html)
111113

114+
115+
@app.get("/metrics", dependencies=[Depends(enforce_rate_limit)])
116+
def metrics_snapshot():
117+
data = metrics.snapshot()
118+
response = JSONResponse(data)
119+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
120+
return response
121+
112122
# --- Routes ---
113123

114124
@app.post("/upload", dependencies=[Depends(enforce_rate_limit)])

app/templates/api.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
<h1>AlterBase CDN API</h1>
8181
<p class="lead">
8282
Upload files and retrieve shareable URLs with a minimal HTTP API. Each client is limited to
83-
$rate_limit requests per minute and individual uploads are capped at $max_file_mb MB.
83+
${rate_limit} requests per minute and individual uploads are capped at ${max_file_mb} MB.
8484
</p>
8585

8686
<section>
@@ -93,7 +93,7 @@ <h2>Endpoints</h2>
9393
<ul>
9494
<li>
9595
<strong>POST /upload</strong><br />
96-
Multipart form request with <code>file</code> field (max $max_file_mb MB). Returns JSON metadata with
96+
Multipart form request with <code>file</code> field (max ${max_file_mb} MB). Returns JSON metadata with
9797
<code>id</code>, <code>url</code>, <code>size</code>, <code>type</code>.
9898
</li>
9999
<li>
@@ -124,6 +124,17 @@ <h2>Notes</h2>
124124
</ul>
125125
</section>
126126

127+
<section>
128+
<h2>Build Your Own</h2>
129+
<p>
130+
Clone the open source project from
131+
<a href="https://github.com/lunaticsm/Image-Uploader-API" target="_blank" rel="noopener"
132+
>github.com/lunaticsm/Image-Uploader-API</a
133+
>
134+
to deploy this stack on your own infrastructure.
135+
</p>
136+
</section>
137+
127138
<section>
128139
<a href="/">← Back to home</a>
129140
</section>

app/templates/index.html

Lines changed: 126 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,22 @@
1717
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1818
min-height: 100vh;
1919
display: flex;
20+
flex-direction: column;
2021
align-items: center;
2122
justify-content: center;
23+
gap: 2rem;
2224
background: radial-gradient(circle at top, #1e293b, #0f172a);
2325
color: #e2e8f0;
24-
padding: 2rem;
26+
padding: 2rem 1.5rem 3rem;
2527
}
2628
main {
27-
max-width: 540px;
28-
background: rgba(15, 23, 42, 0.8);
29-
border-radius: 20px;
30-
padding: 3rem 3.5rem;
29+
width: min(620px, 100%);
30+
background: rgba(15, 23, 42, 0.82);
31+
border-radius: 22px;
32+
padding: clamp(2.5rem, 5vw, 3.5rem);
3133
text-align: center;
3234
border: 1px solid rgba(148, 163, 184, 0.25);
33-
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.45);
35+
box-shadow: 0 26px 70px rgba(15, 23, 42, 0.5);
3436
}
3537
h1 {
3638
font-size: clamp(2rem, 5vw, 3rem);
@@ -43,29 +45,38 @@
4345
opacity: 0.85;
4446
margin-bottom: 2rem;
4547
}
46-
.metrics {
48+
.metrics-grid {
4749
display: grid;
48-
gap: 1rem;
49-
margin-bottom: 2rem;
50+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
51+
gap: 1.2rem;
52+
margin-bottom: 2.25rem;
53+
}
54+
.metric {
5055
text-align: left;
51-
background: rgba(15, 23, 42, 0.6);
52-
border-radius: 14px;
53-
padding: 1.5rem;
54-
border: 1px solid rgba(148, 163, 184, 0.2);
56+
background: rgba(15, 23, 42, 0.65);
57+
border-radius: 16px;
58+
padding: 1.35rem;
59+
border: 1px solid rgba(148, 163, 184, 0.18);
60+
backdrop-filter: blur(12px);
5561
}
56-
.metrics .label {
62+
.metric .label {
5763
display: block;
5864
font-size: 0.85rem;
5965
text-transform: uppercase;
6066
letter-spacing: 0.14em;
6167
opacity: 0.65;
6268
margin-bottom: 0.3rem;
6369
}
64-
.metrics .value {
65-
font-size: 1.6rem;
70+
.metric .value {
71+
font-size: 2rem;
6672
font-weight: 600;
6773
color: #f8fafc;
6874
}
75+
.metric .unit {
76+
font-size: 0.8rem;
77+
opacity: 0.6;
78+
margin-left: 0.25rem;
79+
}
6980
a {
7081
display: inline-block;
7182
text-decoration: none;
@@ -81,30 +92,121 @@
8192
transform: translateY(-2px);
8293
box-shadow: 0 24px 45px rgba(129, 140, 248, 0.45);
8394
}
95+
footer {
96+
font-size: 0.95rem;
97+
opacity: 0.75;
98+
display: flex;
99+
gap: 1rem;
100+
flex-wrap: wrap;
101+
justify-content: center;
102+
text-align: center;
103+
}
104+
footer .heart {
105+
color: #f87171;
106+
margin: 0 0.25rem;
107+
}
108+
@media (max-width: 540px) {
109+
body {
110+
padding: 1.75rem 1rem 2.5rem;
111+
}
112+
.metric .value {
113+
font-size: 1.7rem;
114+
}
115+
}
84116
</style>
85117
</head>
86118
<body>
87119
<main>
88120
<h1>AlterBase CDN</h1>
89121
<p>
90-
Fast minimal API for hosting images and assets. Upload files (up to $max_file_mb MB), retrieve shareable
122+
Fast minimal API for hosting images and assets. Upload files (up to ${max_file_mb} MB), retrieve shareable
91123
URLs, and keep storage tidy with automatic cleanup.
92124
</p>
93-
<div class="metrics">
94-
<div>
125+
<div class="metrics-grid">
126+
<div class="metric" data-key="uploads" data-value="${uploads}">
95127
<span class="label">Uploads</span>
96-
<span class="value">$uploads</span>
128+
<span class="value">0</span>
97129
</div>
98-
<div>
130+
<div class="metric" data-key="downloads" data-value="${downloads}">
99131
<span class="label">Downloads</span>
100-
<span class="value">$downloads</span>
132+
<span class="value">0</span>
101133
</div>
102-
<div>
134+
<div class="metric" data-key="deleted" data-value="${deleted}">
103135
<span class="label">Cleanups</span>
104-
<span class="value">$deleted</span>
136+
<span class="value">0</span>
105137
</div>
106138
</div>
107139
<a href="/api-info">View API Guide</a>
108140
</main>
141+
<footer>
142+
<span>Made by<span class="heart">❤️</span>lunaticsm</span>
143+
<span>© ${year} AlterBase CDN</span>
144+
</footer>
145+
<script>
146+
const metricCards = document.querySelectorAll(".metric");
147+
148+
function formatNumber(value) {
149+
if (Number.isNaN(value)) {
150+
return "0";
151+
}
152+
return value.toLocaleString("en-US");
153+
}
154+
155+
function scramble(el, finalValue) {
156+
const duration = 900;
157+
const start = performance.now();
158+
const maxRandom = Math.max(finalValue, 20);
159+
160+
function update(now) {
161+
const progress = (now - start) / duration;
162+
if (progress < 1) {
163+
const randomValue = Math.floor(Math.random() * maxRandom);
164+
el.textContent = formatNumber(randomValue);
165+
requestAnimationFrame(update);
166+
} else {
167+
el.textContent = formatNumber(finalValue);
168+
}
169+
}
170+
171+
requestAnimationFrame(update);
172+
}
173+
174+
function applyMetrics(data) {
175+
metricCards.forEach(function (card) {
176+
const key = card.getAttribute("data-key");
177+
const nextValue = Number(data[key] || 0);
178+
const current = Number(card.getAttribute("data-current") || "-1");
179+
if (current === nextValue) {
180+
return;
181+
}
182+
card.setAttribute("data-current", String(nextValue));
183+
const valueEl = card.querySelector(".value");
184+
scramble(valueEl, nextValue);
185+
});
186+
}
187+
188+
const initialMetrics = {};
189+
metricCards.forEach(function (card) {
190+
const key = card.getAttribute("data-key");
191+
initialMetrics[key] = Number(card.getAttribute("data-value") || "0");
192+
});
193+
applyMetrics(initialMetrics);
194+
195+
async function refreshMetrics() {
196+
try {
197+
const response = await fetch("/metrics", { cache: "no-store" });
198+
if (!response.ok) {
199+
return;
200+
}
201+
const payload = await response.json();
202+
applyMetrics(payload);
203+
} catch (err) {
204+
console.error("metrics refresh failed", err);
205+
}
206+
}
207+
208+
setTimeout(refreshMetrics, 250);
209+
setInterval(refreshMetrics, 15000);
210+
</script>
109211
</body>
110212
</html>

0 commit comments

Comments
 (0)