[eric] publish: outputs publish fields + publishApi (preflight/publish/unpublish)

This commit is contained in:
ciregenz
2026-06-19 04:26:43 -07:00
parent a64e18e5d2
commit b36d38b26f
2 changed files with 72 additions and 1 deletions
@@ -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<ReviewSummary> {
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<PublishResult> {
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<void> {
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.");
}
+20 -1
View File
@@ -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;