feat(attachments): R2 toggle, storage metrics, and demo R2 config

Add an ATTACHMENTS_ENABLED switch (default on when R2 is bound) via a
central getAttachmentBucket helper, surface R2 + estimated KV usage
against the free tier on the status page and /api/stats (refreshed by the
hourly cron), let setup.sh create and wire the R2 bucket, and bind the
demo bucket so the deployed demo has attachments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-23 17:33:50 +02:00
parent 7226e718f7
commit f150d40c45
19 changed files with 387 additions and 23 deletions
+40
View File
@@ -1,6 +1,8 @@
import { Context } from "hono";
import { Env } from "../types";
import { getStats } from "../utils/stats";
import { formatBytes } from "../utils/format";
import { R2_FREE_TIER_BYTES, KV_FREE_TIER_BYTES } from "../config/constants";
import { Layout } from "./admin/ui";
function formatDateTime(iso?: string): string {
@@ -43,6 +45,11 @@ function formatUptime(iso?: string): string {
return `${days} ${days === 1 ? "day" : "days"}`;
}
function tierPercent(used: number, total: number): number {
if (total <= 0) return 0;
return Math.round((used / total) * 100);
}
type Tone = "success" | "danger";
type StatProps = {
@@ -83,6 +90,11 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
? (stats.emails_received / stats.feeds_created).toFixed(1)
: "—";
const kvBytes = stats.kv_bytes_estimated;
const kvPercent = tierPercent(kvBytes ?? 0, KV_FREE_TIER_BYTES);
const r2Bytes = stats.attachments_bytes;
const r2Percent = tierPercent(r2Bytes ?? 0, R2_FREE_TIER_BYTES);
return c.html(
<Layout title="Status" label="status">
<div class="container fade-in">
@@ -167,6 +179,34 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
</div>
</section>
<section class="stat-section">
<h2 class="stat-section-title">Storage</h2>
<div class="stats-grid">
<Stat
label="KV space used (est.)"
value={kvBytes === undefined ? "—" : formatBytes(kvBytes)}
title={`${kvPercent}% of 1 GB free tier — estimate`}
tone={kvPercent >= 80 ? "danger" : undefined}
/>
{stats.attachments_enabled ? (
<>
<Stat
label="Attachments stored"
value={stats.attachments_count ?? "—"}
/>
<Stat
label="R2 space used"
value={r2Bytes === undefined ? "—" : formatBytes(r2Bytes)}
title={`${r2Percent}% of 10 GB free tier`}
tone={r2Percent >= 80 ? "danger" : undefined}
/>
</>
) : (
<Stat label="Attachments (R2)" value="Off" />
)}
</div>
</section>
<section class="stat-section">
<h2 class="stat-section-title">Instance</h2>
<div class="stats-grid">