Build rich admin dashboards using Supabase PostgREST API + vanilla JS + D3/Chart.js. No build step, no framework — just HTML files served by FastAPI StaticFiles.
FastAPI Server
├── api.py (thin proxy to Supabase REST)
├── static/
│ ├── shell.js + shell.css (shared theme)
│ ├── dashboard/
│ │ ├── index.html (main page)
│ │ ├── agents.html
│ │ ├── skills.html
│ │ └── ...
Create a thin FastAPI proxy that wraps Supabase REST calls. This keeps the Supabase key server-side.
import httpx
from fastapi import FastAPI
SUPABASE_URL = os.environ["SUPABASE_URL"]
SUPABASE_KEY = os.environ["SUPABASE_SERVICE_KEY"]
HEADERS = {"apikey": SUPABASE_KEY, "Authorization": f"Bearer {SUPABASE_KEY}"}
app = FastAPI()
@app.get("/api/mc/{table}")
async def get_table(table: str, select: str = "*", limit: int = 100, offset: int = 0):
allowed = {"ai_agents", "skills", "knowledge_vault", "tools", "workflows"}
if table not in allowed:
raise HTTPException(403, "Table not allowed")
url = f"{SUPABASE_URL}/rest/v1/{table}?select={select}&limit={limit}&offset={offset}"
async with httpx.AsyncClient() as client:
r = await client.get(url, headers={**HEADERS, "Prefer": "count=exact"})
return r.json()
Tables with large text columns (system_prompt, embeddings) will timeout if you SELECT *. Always specify columns:
?select=id,name,type,status,created_at
Add select parameter to every API endpoint and default to lightweight fields.
Create shell.js and shell.css that every page imports:
// shell.js
function createShell(pageTitle, navItems) {
// Returns: sidebar (collapsible) + top bar + main content area
// navItems: [{label, href, icon, active}]
}
function createCard(title, content, footer) {
// Dark-themed card with header, body, optional footer
}
function createTable(headers, rows, options) {
// Sortable, searchable table with pagination
}
function mcFetch(endpoint, params = {}) {
// Wrapper: fetch(`/api/mc/${endpoint}?${new URLSearchParams(params)}`)
// Handles errors, loading states
}
function createSearchBar(placeholder, onSearch) {
// Debounced search input
}
CSS variables for consistent theming:
:root {
--bg-primary: #0a0a0f;
--bg-card: #12121a;
--bg-hover: #1a1a2e;
--text-primary: #e0e0e0;
--text-secondary: #888;
--accent: #6c63ff;
--accent-glow: rgba(108, 99, 255, 0.3);
--border: #2a2a3e;
--success: #4caf50;
--warning: #ff9800;
--danger: #f44336;
}
┌─────────────────────────────┐
│ Search bar + Filter chips │
├─────────────────────────────┤
│ Stats row (total, active, │
│ by type) │
├─────────────────────────────┤
│ Sortable table or card grid │
│ with pagination │
└─────────────────────────────┘
Use D3.js force-directed graph:
Use Chart.js:
from fastapi.staticfiles import StaticFiles
# Mount AFTER API routes
app.mount("/static/mc", StaticFiles(directory="static/mc"), name="mc-static")
# Convenience page routes
@app.get("/mc/{page}")
async def serve_mc_page(page: str):
return FileResponse(f"static/mc/{page}")
@app.get("/mc/")
async def serve_mc_index():
return FileResponse("static/mc/index.html")
mcFetch('/api/mc/agents') when mcFetch already prepends /api/mc/. Fix: mcFetch('agents') or make mcFetch accept both.```python
app.add_middleware(CORSMiddleware, allow_origins=[""], allow_methods=[""], allow_headers=["*"])
```
select parameter + limit + offset. Never SELECT * on tables with 1000+ rows or text columns > 1KB.Works on any platform that serves Python (Railway, Fly, Render):
共 1 个版本