From 4b81f25ec0b657460b5702bf9babd0397e945345 Mon Sep 17 00:00:00 2001 From: nbkdoesntknowcoding Date: Thu, 9 Jul 2026 21:53:41 +0530 Subject: [PATCH 1/2] =?UTF-8?q?fix(web):=20restore=20Settings=20=E2=86=92?= =?UTF-8?q?=20Community=20tab=20(self-host)=20+=20add=20get-key=20email=20?= =?UTF-8?q?form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the tab removal (#85) for the PUBLIC/self-host build: on self-host the operator IS the user, and this tab is their only in-app path to enable publishing. Restores the nav entry, the page, and the CommunitySettings component, and adds a 'Get a free community key' email form — it posts to the hosted licensing service (PUBLIC_COMMUNITY_LICENSE_URL) which signs + emails a free community key the operator then sets as COMMUNITY_HUB_KEY. Browse/import needs no key and already works out of the box; this is only the publish-setup path. Cloud (private repo) intentionally does NOT carry this tab. Co-Authored-By: Claude Opus 4.8 Signed-off-by: nbkdoesntknowcoding --- apps/web/src/components/flows/FlowHeader.tsx | 2 +- .../components/settings/CommunitySettings.tsx | 137 ++++++++++++++++++ apps/web/src/layouts/SettingsLayout.astro | 1 + .../src/pages/app/settings/community.astro | 17 +++ 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/settings/CommunitySettings.tsx create mode 100644 apps/web/src/pages/app/settings/community.astro diff --git a/apps/web/src/components/flows/FlowHeader.tsx b/apps/web/src/components/flows/FlowHeader.tsx index f7e510fcd..750b35671 100644 --- a/apps/web/src/components/flows/FlowHeader.tsx +++ b/apps/web/src/components/flows/FlowHeader.tsx @@ -259,7 +259,7 @@ function PublishToCommunityModal({ flow, onClose }: { flow: Flow; onClose: () => headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tags }), }); - if (res.status === 400) { setErr('Publishing to the community hub is not configured on this instance.'); return; } + if (res.status === 400) { setErr('No community key configured. Get one in Settings → Community, then set COMMUNITY_HUB_KEY.'); return; } if (res.status === 409) { setErr('Publish a version of this flow first, then publish it to the community.'); return; } if (res.status === 403) { setErr('Only workspace owners, admins, and editors can publish.'); return; } if (!res.ok) { setErr('Publish failed. Try again.'); return; } diff --git a/apps/web/src/components/settings/CommunitySettings.tsx b/apps/web/src/components/settings/CommunitySettings.tsx new file mode 100644 index 000000000..962b04eae --- /dev/null +++ b/apps/web/src/components/settings/CommunitySettings.tsx @@ -0,0 +1,137 @@ +/** + * Community settings (Community Flows — Phase 3, P3-6). Self-host surface. + * + * Shows this instance's community-hub status. The publish credential + * (COMMUNITY_HUB_KEY) is an instance env var, so setting it stays out-of-band + * (env + restart) — there is no browser-writable secret here by design. But we + * DO surface a "get a free key" action: it emails a signed community-license key + * (via our hosted licensing service) that the operator then sets as + * COMMUNITY_HUB_KEY. This is the self-hoster's setup path for publishing. + */ +import { type JSX, useEffect, useState } from 'react'; + +const ink = 'var(--ink, #e7e7e9)'; +const soft = 'var(--ink-soft, #a1a1aa)'; +const muted = 'var(--ink-muted, #71717a)'; +const line = 'var(--line, rgba(255,255,255,0.1))'; +const surface = 'var(--surface, rgba(255,255,255,0.02))'; +const mono = { fontFamily: 'var(--mono, monospace)', fontSize: 12 } as const; + +// OUR hosted licensing service — the only outbound call, by explicit user action. +const LICENSE_SERVICE_URL = + (import.meta.env.PUBLIC_COMMUNITY_LICENSE_URL as string | undefined) ?? 'https://mnema.app/community-license'; + +interface Config { + enabled: boolean; + hub_url: string; + can_publish: boolean; +} + +function Row({ label, children }: { label: string; children: React.ReactNode }): JSX.Element { + return ( +
+ {label} + {children} +
+ ); +} + +export function CommunitySettings(): JSX.Element { + const [cfg, setCfg] = useState(null); + const [err, setErr] = useState(null); + + // "Get a free key" email form. + const [email, setEmail] = useState(''); + const [keyState, setKeyState] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle'); + const [keyErr, setKeyErr] = useState(''); + + useEffect(() => { + void (async () => { + try { + const res = await fetch('/api/community/config', { credentials: 'include' }); + if (!res.ok) throw new Error(String(res.status)); + setCfg((await res.json()) as Config); + } catch { + setErr('Could not load community settings.'); + } + })(); + }, []); + + async function requestKey(e: React.FormEvent): Promise { + e.preventDefault(); + setKeyState('sending'); + setKeyErr(''); + try { + const r = await fetch(LICENSE_SERVICE_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ email: email.trim() }), + }); + if (r.ok) { setKeyState('sent'); return; } + setKeyErr(r.status === 429 ? 'Too many requests — try again later.' : 'Could not send. Check the email and try again.'); + setKeyState('error'); + } catch { + setKeyErr('Network error reaching the licensing service.'); + setKeyState('error'); + } + } + + if (err) return
{err}
; + if (!cfg) return
Loading…
; + + return ( +
+
+ {cfg.enabled ? 'Enabled' : 'Disabled'} + {cfg.hub_url} + + {cfg.can_publish ? ( + Available ✓ + ) : ( + Key not configured + )} + +
+ +

+ Browsing and importing community flows works out of the box. To publish your own flows, + this instance needs a community-license key set as the COMMUNITY_HUB_KEY environment + variable. To point at a different hub or disable the feature entirely, set{' '} + COMMUNITY_HUB_URL / COMMUNITY_HUB_ENABLED. +

+ + {!cfg.can_publish && ( +
+
Get a free community key
+ {keyState === 'sent' ? ( +

+ Check your inbox for the signed key, then set it as COMMUNITY_HUB_KEY in this + instance's environment and restart the API. Publishing turns on once it's set. +

+ ) : ( + <> +

+ We'll email you a free signed community-license key. Set it as COMMUNITY_HUB_KEY to + enable publishing from this instance. +

+
+ setEmail(e.target.value)} + style={{ flex: 1, padding: '8px 10px', borderRadius: 8, border: `1px solid ${line}`, background: 'var(--bg, #1b1b1e)', color: 'inherit', fontSize: 14 }} + /> + +
+ {keyErr &&

{keyErr}

} + + )} +
+ )} +
+ ); +} diff --git a/apps/web/src/layouts/SettingsLayout.astro b/apps/web/src/layouts/SettingsLayout.astro index bb42d323d..195a26736 100644 --- a/apps/web/src/layouts/SettingsLayout.astro +++ b/apps/web/src/layouts/SettingsLayout.astro @@ -44,6 +44,7 @@ const NAV_SECTIONS = [ items: [ { href: '/app/settings/workspace', label: 'Workspace' }, { href: '/app/settings/api-keys', label: 'API Keys' }, + { href: '/app/settings/community', label: 'Community' }, { href: '/app/settings/dev', label: 'Dev / AgentLens' }, ], }, diff --git a/apps/web/src/pages/app/settings/community.astro b/apps/web/src/pages/app/settings/community.astro new file mode 100644 index 000000000..09d6d29a2 --- /dev/null +++ b/apps/web/src/pages/app/settings/community.astro @@ -0,0 +1,17 @@ +--- +import SettingsLayout from '../../../layouts/SettingsLayout.astro'; +import { CommunitySettings } from '../../../components/settings/CommunitySettings'; + +const auth = Astro.locals.auth; +if (!auth) { + return Astro.redirect('/auth/login'); +} +--- + + +

Settings

+

Community

+

Browse and import community flows, and publish your own for others to use.

+ + +
From 9007c8b781760318b9ee3e813f47c2d91ee80793 Mon Sep 17 00:00:00 2001 From: nbkdoesntknowcoding Date: Thu, 9 Jul 2026 21:59:35 +0530 Subject: [PATCH 2/2] fix(web): clearer redeem placeholder + point Community tab at Billing for license redemption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RedeemLicense: the placeholder implied only the MNEMA-XXXX admin-code format, but community-license keys are long signed strings (the route accepts both). Neutral placeholder + copy that names both. - CommunitySettings: add a pointer clarifying that unlocking history/export is a separate per-workspace step done under Settings → Billing (vs the instance publish key here). Co-Authored-By: Claude Opus 4.8 Signed-off-by: nbkdoesntknowcoding --- apps/web/src/components/settings/CommunitySettings.tsx | 6 ++++++ apps/web/src/components/settings/RedeemLicense.tsx | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/settings/CommunitySettings.tsx b/apps/web/src/components/settings/CommunitySettings.tsx index 962b04eae..5af6fac7f 100644 --- a/apps/web/src/components/settings/CommunitySettings.tsx +++ b/apps/web/src/components/settings/CommunitySettings.tsx @@ -100,6 +100,12 @@ export function CommunitySettings(): JSX.Element { COMMUNITY_HUB_URL / COMMUNITY_HUB_ENABLED.

+

+ Looking to unlock version history and document export? That's a separate, + per-workspace step — redeem a community-license key under{' '} + Settings → Billing. +

+ {!cfg.can_publish && (
Get a free community key
diff --git a/apps/web/src/components/settings/RedeemLicense.tsx b/apps/web/src/components/settings/RedeemLicense.tsx index f9e8855e3..a72eeb112 100644 --- a/apps/web/src/components/settings/RedeemLicense.tsx +++ b/apps/web/src/components/settings/RedeemLicense.tsx @@ -24,9 +24,9 @@ export function RedeemLicense(): JSX.Element { return (
Have a license key?
-
Enter it to activate your plan.
+
Paste a plan key (MNEMA-…) or a community-license key to activate.
- setKey(e.target.value)} placeholder="MNEMA-XXXX-XXXX-XXXX-XXXX" + setKey(e.target.value)} placeholder="Paste your license or community key" style={{ flex: 1, padding: '7px 10px', borderRadius: 8, fontSize: 12.5, fontFamily: 'var(--mono, monospace)', border: '0.5px solid var(--border, rgba(0,0,0,0.08))', background: 'var(--surface, #fff)', color: 'var(--ink)' }} />