From aec6f5f118aea366a4190a0545c0f472a93eff0f Mon Sep 17 00:00:00 2001 From: inference-bot Date: Mon, 14 Sep 2026 14:19:02 -0600 Subject: [PATCH] Dashboard UI: 'Your membership' + identity, richer usage (tokens/requests/per-model), logout, Member Home link, footer, fix loading placeholders --- app/main.py | 143 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 37 deletions(-) diff --git a/app/main.py b/app/main.py index 8650a35..1d9bc97 100644 --- a/app/main.py +++ b/app/main.py @@ -2,10 +2,10 @@ Security model: - Authentication is done by Cloudron's proxyAuth wall (SSO). Cloudron injects - the authenticated user's identity via the X-Forwarded-User header. + the authenticated user's identity via the X-Remote-User header (username). - This app holds NO privileged credentials. It calls the member portal's broker endpoints, authenticated with a shared BROKER_SECRET, and scopes - every request to the logged-in member's email. + every request to the logged-in member. Environment (all provided by Cloudron env vars): PORTAL_BASE — base URL of the member portal (e.g. https://portal.inference.coop) @@ -59,13 +59,11 @@ def get_user_identity(request: Request) -> str: # --------------------------------------------------------------------------- def broker_headers(identity: str) -> dict: - # Pass the raw identity (username or email); the portal resolves it to an - # email via its own Cloudron user list. return {"X-Broker-Secret": BROKER_SECRET, "X-Member-User": identity} async def broker_get(identity: str, path: str) -> dict: - async with httpx.AsyncClient(timeout=15.0) as client: + async with httpx.AsyncClient(timeout=20.0) as client: r = await client.get(f"{PORTAL_BASE}{path}", headers=broker_headers(identity)) if r.status_code == 401: raise HTTPException(500, "Portal rejected broker credentials") @@ -77,7 +75,7 @@ async def broker_get(identity: str, path: str) -> dict: async def broker_post(identity: str, path: str, payload: dict | None = None) -> dict: - async with httpx.AsyncClient(timeout=15.0) as client: + async with httpx.AsyncClient(timeout=20.0) as client: r = await client.post(f"{PORTAL_BASE}{path}", headers=broker_headers(identity), json=payload or {}) if r.status_code in (401, 403): raise HTTPException(r.status_code, r.text) @@ -87,7 +85,7 @@ async def broker_post(identity: str, path: str, payload: dict | None = None) -> async def broker_delete(identity: str, path: str) -> dict: - async with httpx.AsyncClient(timeout=15.0) as client: + async with httpx.AsyncClient(timeout=20.0) as client: r = await client.delete(f"{PORTAL_BASE}{path}", headers=broker_headers(identity)) if r.status_code in (401, 403): raise HTTPException(r.status_code, r.text) @@ -160,13 +158,20 @@ async def api_revoke_key(name: str, request: Request): # UI (brand-matched, self-contained, no external requests) # --------------------------------------------------------------------------- -def render_page(email: str) -> str: - return """ +def _esc(s: str) -> str: + return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) + + +def render_page(identity: str) -> str: + return PAGE_HTML.replace("@@IDENTITY@@", _esc(identity)) + + +PAGE_HTML = """ -Inference Cooperative · Your account +Inference Cooperative · Your membership @@ -288,24 +312,35 @@ def render_page(email: str) -> str:

Inference Cooperative

-

Your account

+
Your membership
+
Signed in as @@IDENTITY@@
+
+
+ Member Home + Log out

Usage

- — - of your balance used + — + of your balance used +
+
+
—
remaining
+
—
tokens used
+
—
requests
-
Loading…
+
Shared across chat and API. Top-ups appear here automatically.
+

API keys

Keys let you use the co-op's AI from your own tools. They draw from the same balance as chat.

-
Loading…
+
No API keys yet.
@@ -314,6 +349,13 @@ def render_page(email: str) -> str:
+ +