From b36d38b26f82edb985af78b19ce86b7f1a70fb7e Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 19 Jun 2026 04:26:43 -0700 Subject: [PATCH] [eric] publish: outputs publish fields + publishApi (preflight/publish/unpublish) --- .../src/app/components/share/publishApi.ts | 52 +++++++++++++++++++ frontend/src/shared/state/outputsSlice.ts | 21 +++++++- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/components/share/publishApi.ts diff --git a/frontend/src/app/components/share/publishApi.ts b/frontend/src/app/components/share/publishApi.ts new file mode 100644 index 00000000..80da81c1 --- /dev/null +++ b/frontend/src/app/components/share/publishApi.ts @@ -0,0 +1,52 @@ +// Fetch helpers for the app-publishing endpoints. The global interceptor in +// shared/config.ts attaches the bearer, so we never set it here. The /publish +// endpoints return a result object (ok / blocked / error) rather than HTTP errors +// for the normal flow, so callers inspect the body. +import { API_BASE } from '@/shared/config'; + +import { ReviewSummary } from './shareTypes'; + +const OUTPUTS_API = `${API_BASE}/outputs`; + +export interface PublishResult { + ok: boolean; + published_slug?: string | null; + published_url?: string | null; + /** True when the AST safety net blocked a non-force publish; review carries why. */ + blocked?: boolean; + review?: ReviewSummary | null; + error?: string | null; +} + +export async function publishPreflight(outputId: string): Promise { + const res = await fetch(`${OUTPUTS_API}/publish/preflight`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ output_id: outputId }), + }); + if (!res.ok) throw new Error("We couldn't check this app."); + const data = await res.json(); + return data.review as ReviewSummary; +} + +export async function publishApp( + outputId: string, + opts: { slug?: string; force?: boolean } = {}, +): Promise { + const res = await fetch(`${OUTPUTS_API}/publish`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ output_id: outputId, slug: opts.slug, force: !!opts.force }), + }); + if (!res.ok) throw new Error('Publishing failed. Please try again.'); + return (await res.json()) as PublishResult; +} + +export async function unpublishApp(outputId: string): Promise { + const res = await fetch(`${OUTPUTS_API}/unpublish`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ output_id: outputId }), + }); + if (!res.ok) throw new Error("We couldn't unpublish this app."); +} diff --git a/frontend/src/shared/state/outputsSlice.ts b/frontend/src/shared/state/outputsSlice.ts index 8ce1fee9..107b2457 100644 --- a/frontend/src/shared/state/outputsSlice.ts +++ b/frontend/src/shared/state/outputsSlice.ts @@ -21,6 +21,17 @@ export interface Output { workspace_id?: string | null; created_at: string; updated_at: string; + /** App publishing to {slug}.openswarm.dev. Server-managed; mirrored here after a publish/unpublish. */ + published_slug?: string | null; + published_url?: string | null; + publish_status?: 'publishing' | 'published' | 'error' | null; +} + +export interface PublishStatePatch { + id: string; + published_slug?: string | null; + published_url?: string | null; + publish_status?: Output['publish_status']; } export function getFrontendCode(output: Output): string { @@ -133,6 +144,14 @@ const outputsSlice = createSlice({ const existing = state.items[incoming.id]; state.items[incoming.id] = existing ? { ...existing, ...incoming } : incoming; }, + /** Reflect a publish/unpublish result onto an existing Output without a refetch. */ + setOutputPublishState(state, action: { payload: PublishStatePatch; type: string }) { + const o = state.items[action.payload.id]; + if (!o) return; + if ('published_slug' in action.payload) o.published_slug = action.payload.published_slug ?? null; + if ('published_url' in action.payload) o.published_url = action.payload.published_url ?? null; + if ('publish_status' in action.payload) o.publish_status = action.payload.publish_status ?? null; + }, }, extraReducers: (builder) => { builder @@ -150,5 +169,5 @@ const outputsSlice = createSlice({ }, }); -export const { upsertOutput } = outputsSlice.actions; +export const { upsertOutput, setOutputPublishState } = outputsSlice.actions; export default outputsSlice.reducer;