Adds a new Stacks feature: upload/edit docker-compose YAML, deploy as atomic units, browse revisions, roll back, and stream logs. Backend in internal/stack + internal/api/stacks.go, persistent storage in internal/store/stacks.go. Stacks pages (list, new, detail) use a modern Forge aesthetic — Instrument Serif display type, JetBrains Mono for meta/code, indigo ember accents, dot-grid hero, registration marks on hover, terminal panel for logs. Palette is sourced from the app's existing design tokens so the feature remains consistent with the rest of Tinyforge. Fonts self-hosted via @fontsource/instrument-serif and @fontsource/jetbrains-mono to satisfy the strict CSP.
This commit is contained in:
@@ -771,4 +771,80 @@ export function getStaticSiteStorage(
|
||||
return get<import('./types').StaticSiteStorageUsage>(`/api/sites/${siteId}/storage`);
|
||||
}
|
||||
|
||||
// ── Stacks (docker-compose) ─────────────────────────────────────────
|
||||
|
||||
import type { Stack, StackRevision, StackService } from './types';
|
||||
|
||||
export function listStacks(signal?: AbortSignal): Promise<Stack[]> {
|
||||
return get<Stack[]>('/api/stacks', signal);
|
||||
}
|
||||
|
||||
export function getStack(id: string, signal?: AbortSignal): Promise<Stack> {
|
||||
return get<Stack>(`/api/stacks/${id}`, signal);
|
||||
}
|
||||
|
||||
export function createStack(data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
yaml: string;
|
||||
deploy?: boolean;
|
||||
}): Promise<{ stack: Stack; revision: StackRevision }> {
|
||||
return post<{ stack: Stack; revision: StackRevision }>('/api/stacks', data);
|
||||
}
|
||||
|
||||
export function updateStack(id: string, data: { name?: string; description?: string }): Promise<Stack> {
|
||||
return put<Stack>(`/api/stacks/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteStack(id: string, removeVolumes = false): Promise<{ deleted: string }> {
|
||||
const qs = removeVolumes ? '?remove_volumes=true' : '';
|
||||
return del<{ deleted: string }>(`/api/stacks/${id}${qs}`);
|
||||
}
|
||||
|
||||
export function listStackRevisions(id: string, signal?: AbortSignal): Promise<StackRevision[]> {
|
||||
return get<StackRevision[]>(`/api/stacks/${id}/revisions`, signal);
|
||||
}
|
||||
|
||||
export function getStackRevision(id: string, revId: string): Promise<StackRevision> {
|
||||
return get<StackRevision>(`/api/stacks/${id}/revisions/${revId}`);
|
||||
}
|
||||
|
||||
export function createStackRevision(id: string, yaml: string): Promise<StackRevision> {
|
||||
return post<StackRevision>(`/api/stacks/${id}/revisions`, { yaml });
|
||||
}
|
||||
|
||||
export function rollbackStack(id: string, revId: string): Promise<StackRevision> {
|
||||
return post<StackRevision>(`/api/stacks/${id}/rollback/${revId}`);
|
||||
}
|
||||
|
||||
export function stopStack(id: string): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(`/api/stacks/${id}/stop`);
|
||||
}
|
||||
|
||||
export function startStack(id: string): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(`/api/stacks/${id}/start`);
|
||||
}
|
||||
|
||||
export function getStackServices(id: string, signal?: AbortSignal): Promise<StackService[]> {
|
||||
return get<StackService[]>(`/api/stacks/${id}/services`, signal);
|
||||
}
|
||||
|
||||
export async function getStackLogs(
|
||||
id: string,
|
||||
service?: string,
|
||||
tail = 200
|
||||
): Promise<string> {
|
||||
const params = new URLSearchParams();
|
||||
if (service) params.set('service', service);
|
||||
params.set('tail', String(tail));
|
||||
const token = getAuthToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const res = await fetch(`/api/stacks/${id}/logs?${params.toString()}`, { headers });
|
||||
if (!res.ok) {
|
||||
throw new ApiError(`Failed to fetch logs: ${res.status}`, res.status);
|
||||
}
|
||||
return res.text();
|
||||
}
|
||||
|
||||
export { ApiError };
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"settings": "Settings",
|
||||
"logout": "Log out",
|
||||
"dns": "DNS Records",
|
||||
"sites": "Sites"
|
||||
"sites": "Sites",
|
||||
"stacks": "Stacks"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"settings": "Настройки",
|
||||
"logout": "Выйти",
|
||||
"dns": "DNS-записи",
|
||||
"sites": "Сайты"
|
||||
"sites": "Сайты",
|
||||
"stacks": "Стеки"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Панель управления",
|
||||
|
||||
@@ -372,6 +372,40 @@ export interface StaticSiteStorageUsage {
|
||||
|
||||
export type StaticSiteStatus = 'idle' | 'syncing' | 'deployed' | 'failed' | 'stopped';
|
||||
|
||||
export type StackStatus = 'stopped' | 'deploying' | 'running' | 'failed';
|
||||
|
||||
export interface Stack {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
compose_project_name: string;
|
||||
status: StackStatus;
|
||||
error: string;
|
||||
current_revision_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface StackRevision {
|
||||
id: string;
|
||||
stack_id: string;
|
||||
revision: number;
|
||||
yaml: string;
|
||||
author: string;
|
||||
deploy_id: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface StackService {
|
||||
Name: string;
|
||||
Service: string;
|
||||
State: string;
|
||||
Status: string;
|
||||
Health: string;
|
||||
ExitCode: number;
|
||||
}
|
||||
|
||||
export type GitProvider = '' | 'gitea' | 'github' | 'gitlab';
|
||||
|
||||
/** An encrypted environment variable for a static site's Deno backend. */
|
||||
|
||||
Reference in New Issue
Block a user