feat(cutover): hard legacy cutover — drop projects/stacks/sites/deploys
Build / build (push) Successful in 10m39s
Build / build (push) Successful in 10m39s
The clean-break delete that closes the workload-first refactor arc.
Net diff: ~30 backend files deleted, ~20 modified, ~12k LOC removed
on the Go side; entire /projects /stacks /sites /deploy frontend
trees gone; ~6.7k LOC removed on the Svelte/TypeScript side.
Backend
- API handlers gone: internal/api/{projects,stages,stage_env,stacks,
static_sites,deploys,instances,volume_browser}.go
- Store CRUD + tests gone: internal/store/{projects,stages,stage_env,
stacks,static_sites,static_site_secrets,deploys,poll_state,volumes,
workload_sync}.go (+ _test.go siblings)
- Legacy deployer pipeline gone: internal/deployer/{bluegreen,promote,
rollback,subdomain,resolver_test}.go; deployer.go trimmed to just the
dispatch surface used by the plugin pipeline
- internal/staticsite/{manager,healthcheck}.go and
internal/stack/manager.go gone (the rest of those packages stay as
helpers imported by the static + compose plugins)
- internal/registry/poller.go gone (legacy registry poller)
- internal/volume.ResolvePath gone; ResolveWorkloadPath stays
- internal/webhook: handleWebhook (project) + handleSiteWebhook (site)
gone; only POST /api/webhook/triggers/{secret} remains
- workload-side webhook URL handlers (getWorkloadWebhook +
regenerateWorkloadWebhook + EnsureWorkloadWebhookSecret +
SetWorkloadWebhookSecret + GetWorkloadByWebhookSecret) gone — they
minted URLs that would 404 against the new trigger-only ingress
- cmd/server/main.go: dropped staticsite.Manager, stack.Manager,
staticsite.HealthChecker, registry poller, SetSiteSyncTriggerer,
SetStaticSiteManager, SetStackManager, wireStaticBackend
- store/store.go: idempotent DROP TABLE IF EXISTS for every legacy
table (projects, stages, stage_env, volumes, deploys, deploy_logs,
poll_states, stacks, stack_revisions, stack_deploys, static_sites,
static_site_secrets); FK order children-then-parents
- store/models.go: dropped Project, Stage, Deploy, DeployLog, StageEnv,
Volume, StaticSite, StaticSiteSecret, Stack, StackRevision,
StackDeploy types; kept WorkloadKind constants as documented strings
- internal/store/helpers.go (new): BoolToInt, rowScanner,
GenerateWebhookSecret extracted from deleted CRUD files
- internal/api/secrets.go (new): forwards to store.GenerateWebhookSecret
so api + store paths share one secret-generation impl (no
panic-vs-UUID-fallback divergence)
- internal/reconciler/reconciler.go: dropped legacy stack-by-compose
+ static-site label paths; only canonical tinyforge.workload.id
dispatch remains
- providers (gitea_content/github_provider/gitlab_provider) gained
path-traversal rejection on every tree entry
- internal/webhook ParsedImage / ParseImageRef demoted to package-
private (no external callers)
Frontend
- /projects /stacks /sites /deploy routes deleted (entire trees)
- ProjectCard / InstanceCard / StaleContainerCard components deleted
- api.ts: dropped every project/stage/stack/site/deploy/instance
helper + types (Project, Stage, Stack, StaticSite, Deploy,
Instance, Volume, etc.); kept Workload, Container, App, Settings,
Registry, EventTrigger, LogScanRule, webhook envelopes
- WorkloadWebhook type + getWorkloadWebhook/regenerateWorkloadWebhook
api functions gone (mirror of the backend deletion above)
- web/src/routes/+layout.svelte: dropped /projects /sites /stacks
/deploy nav entries, trimmed quick-nav keymap
- web/src/routes/+page.svelte: dashboard rewrite — reads
listWorkloads + listContainers only; 4-card stat grid
(workloads/running/failed/stale) + recent workloads strip
- navCounts.ts, SystemHealthCard.svelte, ContainerLogs.svelte,
ContainerStats.svelte, StatusBadge.svelte, TagCombobox.svelte,
proxies/+page.svelte, containers/+page.svelte all rewired to the
workload-first surface
- AbortController plumbing on dashboard, nav-counts, stale page,
SystemHealthCard so navigation doesn't leave dangling fetches
- i18n: dropped projects.*, projectDetail.*, envEditor.*,
volumeEditor.*, volumeBrowser.*, quickDeploy.*, sites.*, stacks.*,
instance.*, confirm.* namespaces; en/ru parity preserved (1042
keys each)
Hardening from go-reviewer + security-reviewer + typescript-reviewer
subagent passes (0 CRITICAL across all three; 1 HIGH + ~12 MEDIUM
addressed inline before commit):
- Sec H1: dead-end workload webhook URL handlers (would mint URLs
that 404 the new trigger-only ingress) deleted across backend +
frontend
- Go M1: IsTerminalDeployStatus dropped (no production callers)
- Go M2: ParsedImage/ParseImageRef lowercased (in-package only)
- Go M6: generateWebhookSecret unified — api shim forwards to
store.GenerateWebhookSecret
- Doc/comment freshness: stage_id (no longer FK), ProxyRoute legacy
field names, workloadIDRow rationale, webhook_deliveries.target_type
enum, WebhookDeliveryLog component header
Doc
- WORKLOAD_REFACTOR_TODO: cutover marked DONE; all three Priority 1
items are now shipped. Next focus is Priority 3 polish (apps.* i18n
+ codemap entries) and Priority 4 tests.
Behavioral notes for operators upgrading from a pre-cutover build
- Existing rows in the dropped tables disappear on first boot.
- Legacy webhook URLs at /api/webhook/{secret} and
/api/webhook/sites/{secret} return 404; CI configs must repoint to
/api/webhook/triggers/{secret} (the trigger-split boot backfill
lifted any embedded workload secret onto a Trigger row, so the
secret value itself carries over).
- Frontend routes /projects /stacks /sites /deploy are gone; nav
links replaced with /apps and /triggers.
This commit is contained in:
+75
-552
@@ -8,27 +8,19 @@ import type {
|
||||
SystemStats,
|
||||
SystemStatsSample,
|
||||
TopContainerSample,
|
||||
Deploy,
|
||||
DeployLog,
|
||||
DockerHealth,
|
||||
ProxyHealth,
|
||||
EventLogEntry,
|
||||
EventLogStats,
|
||||
InspectResult,
|
||||
Instance,
|
||||
LocalImage,
|
||||
NpmCertificate,
|
||||
NpmAccessList,
|
||||
ProxyRoute,
|
||||
Project,
|
||||
ProjectDetail,
|
||||
Registry,
|
||||
RegistryImage,
|
||||
Settings,
|
||||
StaleContainer,
|
||||
Stage,
|
||||
StageEnv,
|
||||
Volume,
|
||||
VolumeScopeInfo,
|
||||
BrowseResult,
|
||||
DnsZone,
|
||||
@@ -174,125 +166,15 @@ function patch<T>(path: string, body: unknown): Promise<T> {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Projects ────────────────────────────────────────────────────────
|
||||
|
||||
export function listProjects(signal?: AbortSignal): Promise<Project[]> {
|
||||
return get<Project[]>('/api/projects', signal);
|
||||
}
|
||||
|
||||
export function getProject(id: string, signal?: AbortSignal): Promise<ProjectDetail> {
|
||||
return get<ProjectDetail>(`/api/projects/${id}`, signal);
|
||||
}
|
||||
|
||||
export function createProject(data: Partial<Project>): Promise<Project> {
|
||||
return post<Project>('/api/projects', data);
|
||||
}
|
||||
|
||||
export function updateProject(id: string, data: Partial<Project>): Promise<Project> {
|
||||
return put<Project>(`/api/projects/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteProject(id: string): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/projects/${id}`);
|
||||
}
|
||||
|
||||
// ── Stages ─────────────────────────────────────────────────────────
|
||||
|
||||
export function createStage(projectId: string, data: Partial<Stage>): Promise<Stage> {
|
||||
return post<Stage>(`/api/projects/${projectId}/stages`, data);
|
||||
}
|
||||
|
||||
export function updateStage(projectId: string, stageId: string, data: Partial<Stage>): Promise<Stage> {
|
||||
return put<Stage>(`/api/projects/${projectId}/stages/${stageId}`, data);
|
||||
}
|
||||
|
||||
export function deleteStage(projectId: string, stageId: string): Promise<void> {
|
||||
return del<void>(`/api/projects/${projectId}/stages/${stageId}`);
|
||||
}
|
||||
|
||||
// ── Instances ───────────────────────────────────────────────────────
|
||||
|
||||
export function listInstances(projectId: string, stageId: string, signal?: AbortSignal): Promise<Instance[]> {
|
||||
return get<Instance[]>(`/api/projects/${projectId}/stages/${stageId}/instances`, signal);
|
||||
}
|
||||
|
||||
export function deployInstance(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
imageTag: string
|
||||
): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(`/api/projects/${projectId}/stages/${stageId}/instances`, {
|
||||
image_tag: imageTag
|
||||
});
|
||||
}
|
||||
|
||||
export function removeInstance(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
instanceId: string
|
||||
): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(
|
||||
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}`
|
||||
);
|
||||
}
|
||||
|
||||
export function stopInstance(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
instanceId: string
|
||||
): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(
|
||||
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stop`
|
||||
);
|
||||
}
|
||||
|
||||
export function startInstance(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
instanceId: string
|
||||
): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(
|
||||
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/start`
|
||||
);
|
||||
}
|
||||
|
||||
export function restartInstance(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
instanceId: string
|
||||
): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(
|
||||
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/restart`
|
||||
);
|
||||
}
|
||||
|
||||
// ── Deploys ─────────────────────────────────────────────────────────
|
||||
|
||||
export function listDeploys(limit = 50, signal?: AbortSignal): Promise<Deploy[]> {
|
||||
return get<Deploy[]>(`/api/deploys?limit=${limit}`, signal);
|
||||
}
|
||||
|
||||
export function getDeployLogs(deployId: string): Promise<DeployLog[]> {
|
||||
return get<DeployLog[]>(`/api/deploys/${deployId}/logs`);
|
||||
}
|
||||
// ── Deploys (inspect only; quick-deploy retired with /deploy page) ────
|
||||
// `inspectImage` survives because the new-app wizard can use it to pre-fill
|
||||
// image port/healthcheck. `quickDeploy` (POST /api/deploy/quick) is gone:
|
||||
// it created a legacy Project + Stage in the now-dead path.
|
||||
|
||||
export function inspectImage(image: string): Promise<InspectResult> {
|
||||
return post<InspectResult>('/api/deploy/inspect', { image });
|
||||
}
|
||||
|
||||
export function quickDeploy(data: {
|
||||
name?: string;
|
||||
image: string;
|
||||
tag?: string;
|
||||
registry?: string;
|
||||
port?: number;
|
||||
force?: boolean;
|
||||
enable_proxy?: boolean;
|
||||
auto_deploy?: boolean;
|
||||
}): Promise<{ project: Project; status: string }> {
|
||||
return post<{ project: Project; status: string }>('/api/deploy/quick', data);
|
||||
}
|
||||
|
||||
// ── Registries ──────────────────────────────────────────────────────
|
||||
|
||||
export function listRegistries(): Promise<Registry[]> {
|
||||
@@ -335,7 +217,8 @@ export function updateSettings(data: Partial<Settings>): Promise<Settings> {
|
||||
return put<Settings>('/api/settings', data);
|
||||
}
|
||||
|
||||
// ── Webhooks ───────────────────────────────────────────────────────
|
||||
// ── Webhook envelopes ──────────────────────────────────────────────
|
||||
// These shapes are reused by the workload + trigger webhook flows.
|
||||
|
||||
export interface WebhookUrlResponse {
|
||||
webhook_url: string;
|
||||
@@ -348,49 +231,9 @@ export interface SigningSecretResponse {
|
||||
signing_secret: string;
|
||||
}
|
||||
|
||||
export function getProjectWebhook(projectId: string): Promise<WebhookUrlResponse> {
|
||||
return get<WebhookUrlResponse>(`/api/projects/${projectId}/webhook`);
|
||||
}
|
||||
|
||||
export function regenerateProjectWebhook(projectId: string): Promise<WebhookUrlResponse> {
|
||||
return post<WebhookUrlResponse>(`/api/projects/${projectId}/webhook/regenerate`);
|
||||
}
|
||||
|
||||
export function regenerateProjectSigningSecret(projectId: string): Promise<SigningSecretResponse> {
|
||||
return post<SigningSecretResponse>(`/api/projects/${projectId}/webhook/signing-secret/regenerate`);
|
||||
}
|
||||
|
||||
export async function disableProjectSigningSecret(projectId: string): Promise<void> {
|
||||
await del<void>(`/api/projects/${projectId}/webhook/signing-secret`);
|
||||
}
|
||||
|
||||
export async function setProjectRequireSignature(projectId: string, require: boolean): Promise<void> {
|
||||
await put<void>(`/api/projects/${projectId}/webhook/require-signature`, { require_signature: require });
|
||||
}
|
||||
|
||||
export function getStaticSiteWebhook(siteId: string): Promise<WebhookUrlResponse> {
|
||||
return get<WebhookUrlResponse>(`/api/sites/${siteId}/webhook`);
|
||||
}
|
||||
|
||||
export function regenerateStaticSiteWebhook(siteId: string): Promise<WebhookUrlResponse> {
|
||||
return post<WebhookUrlResponse>(`/api/sites/${siteId}/webhook/regenerate`);
|
||||
}
|
||||
|
||||
export function regenerateStaticSiteSigningSecret(siteId: string): Promise<SigningSecretResponse> {
|
||||
return post<SigningSecretResponse>(`/api/sites/${siteId}/webhook/signing-secret/regenerate`);
|
||||
}
|
||||
|
||||
export async function disableStaticSiteSigningSecret(siteId: string): Promise<void> {
|
||||
await del<void>(`/api/sites/${siteId}/webhook/signing-secret`);
|
||||
}
|
||||
|
||||
export async function setStaticSiteRequireSignature(siteId: string, require: boolean): Promise<void> {
|
||||
await put<void>(`/api/sites/${siteId}/webhook/require-signature`, { require_signature: require });
|
||||
}
|
||||
|
||||
export interface WebhookDelivery {
|
||||
id: number;
|
||||
target_type: 'project' | 'site';
|
||||
target_type: 'project' | 'site' | 'workload' | 'trigger';
|
||||
target_id: string;
|
||||
target_name: string;
|
||||
received_at: string;
|
||||
@@ -402,15 +245,10 @@ export interface WebhookDelivery {
|
||||
body_size: number;
|
||||
}
|
||||
|
||||
export function listProjectWebhookDeliveries(projectId: string, signal?: AbortSignal): Promise<WebhookDelivery[]> {
|
||||
return get<WebhookDelivery[]>(`/api/projects/${projectId}/webhook/deliveries`, signal);
|
||||
}
|
||||
|
||||
export function listStaticSiteWebhookDeliveries(siteId: string, signal?: AbortSignal): Promise<WebhookDelivery[]> {
|
||||
return get<WebhookDelivery[]>(`/api/sites/${siteId}/webhook/deliveries`, signal);
|
||||
}
|
||||
|
||||
// ── Outgoing-webhook signing & test ────────────────────────────────
|
||||
// ── Outgoing-webhook signing & test (settings tier only) ───────────
|
||||
// Per-project, per-stage, per-site tiers were dropped with the legacy
|
||||
// endpoints. Per-workload signing is exposed via the workload webhook
|
||||
// flow below.
|
||||
|
||||
export interface NotificationSecretResponse {
|
||||
secret: string;
|
||||
@@ -419,7 +257,7 @@ export interface NotificationSecretResponse {
|
||||
|
||||
export interface NotificationTestResult {
|
||||
url: string;
|
||||
tier: 'settings' | 'project' | 'stage' | 'site';
|
||||
tier: 'settings' | 'project' | 'stage' | 'site' | 'workload' | 'trigger';
|
||||
status_code: number;
|
||||
latency_ms: number;
|
||||
signature_sent: boolean;
|
||||
@@ -428,7 +266,6 @@ export interface NotificationTestResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Settings (global) tier.
|
||||
export function getSettingsNotificationSecret(): Promise<NotificationSecretResponse> {
|
||||
return get<NotificationSecretResponse>('/api/settings/notification-secret');
|
||||
}
|
||||
@@ -442,66 +279,14 @@ export function testSettingsNotification(): Promise<NotificationTestResult> {
|
||||
return post<NotificationTestResult>('/api/settings/notification-test');
|
||||
}
|
||||
|
||||
// Project tier.
|
||||
export function getProjectNotificationSecret(projectId: string): Promise<NotificationSecretResponse> {
|
||||
return get<NotificationSecretResponse>(`/api/projects/${projectId}/notification-secret`);
|
||||
}
|
||||
export function regenerateProjectNotificationSecret(projectId: string): Promise<NotificationSecretResponse> {
|
||||
return post<NotificationSecretResponse>(`/api/projects/${projectId}/notification-secret/regenerate`);
|
||||
}
|
||||
export function disableProjectNotificationSigning(projectId: string): Promise<NotificationSecretResponse> {
|
||||
return post<NotificationSecretResponse>(`/api/projects/${projectId}/notification-secret/disable`);
|
||||
}
|
||||
export function testProjectNotification(projectId: string): Promise<NotificationTestResult> {
|
||||
return post<NotificationTestResult>(`/api/projects/${projectId}/notification-test`);
|
||||
}
|
||||
|
||||
// Stage tier.
|
||||
export function getStageNotificationSecret(projectId: string, stageId: string): Promise<NotificationSecretResponse> {
|
||||
return get<NotificationSecretResponse>(`/api/projects/${projectId}/stages/${stageId}/notification-secret`);
|
||||
}
|
||||
export function regenerateStageNotificationSecret(projectId: string, stageId: string): Promise<NotificationSecretResponse> {
|
||||
return post<NotificationSecretResponse>(`/api/projects/${projectId}/stages/${stageId}/notification-secret/regenerate`);
|
||||
}
|
||||
export function disableStageNotificationSigning(projectId: string, stageId: string): Promise<NotificationSecretResponse> {
|
||||
return post<NotificationSecretResponse>(`/api/projects/${projectId}/stages/${stageId}/notification-secret/disable`);
|
||||
}
|
||||
export function testStageNotification(projectId: string, stageId: string): Promise<NotificationTestResult> {
|
||||
return post<NotificationTestResult>(`/api/projects/${projectId}/stages/${stageId}/notification-test`);
|
||||
}
|
||||
|
||||
// Static-site tier.
|
||||
export function getStaticSiteNotificationSecret(siteId: string): Promise<NotificationSecretResponse> {
|
||||
return get<NotificationSecretResponse>(`/api/sites/${siteId}/notification-secret`);
|
||||
}
|
||||
export function regenerateStaticSiteNotificationSecret(siteId: string): Promise<NotificationSecretResponse> {
|
||||
return post<NotificationSecretResponse>(`/api/sites/${siteId}/notification-secret/regenerate`);
|
||||
}
|
||||
export function disableStaticSiteNotificationSigning(siteId: string): Promise<NotificationSecretResponse> {
|
||||
return post<NotificationSecretResponse>(`/api/sites/${siteId}/notification-secret/disable`);
|
||||
}
|
||||
export function testStaticSiteNotification(siteId: string): Promise<NotificationTestResult> {
|
||||
return post<NotificationTestResult>(`/api/sites/${siteId}/notification-test`);
|
||||
}
|
||||
|
||||
// ── Proxy Routes ───────────────────────────────────────────────────
|
||||
|
||||
export function listProxyRoutes(): Promise<ProxyRoute[]> {
|
||||
return get<ProxyRoute[]>('/api/proxies');
|
||||
export function listProxyRoutes(signal?: AbortSignal): Promise<ProxyRoute[]> {
|
||||
return get<ProxyRoute[]>('/api/proxies', signal);
|
||||
}
|
||||
|
||||
// ── Docker Management ──────────────────────────────────────────────
|
||||
|
||||
export function fetchContainerLogs(
|
||||
projectId: string, stageId: string, instanceId: string, tail = 200
|
||||
): Promise<string[]> {
|
||||
return get<string[]>(`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/logs?tail=${tail}`);
|
||||
}
|
||||
|
||||
export function listProjectImages(projectId: string, signal?: AbortSignal): Promise<LocalImage[]> {
|
||||
return get<LocalImage[]>(`/api/projects/${projectId}/images`, signal);
|
||||
}
|
||||
|
||||
export function getUnusedImageStats(signal?: AbortSignal): Promise<{
|
||||
total_size_mb: number; count: number; threshold_mb: number; exceeded: boolean;
|
||||
}> {
|
||||
@@ -524,6 +309,15 @@ export function listNpmAccessLists(): Promise<NpmAccessList[]> {
|
||||
return get<NpmAccessList[]>('/api/settings/npm-access-lists');
|
||||
}
|
||||
|
||||
// ── Volume scopes (metadata only) ───────────────────────────────────
|
||||
// Per-project volume CRUD endpoints died with the legacy routes; the
|
||||
// workload volume endpoints cover the new path. Scope metadata stays
|
||||
// because the volume editor for workloads still needs it.
|
||||
|
||||
export function listVolumeScopes(): Promise<VolumeScopeInfo[]> {
|
||||
return get<VolumeScopeInfo[]>('/api/volumes/scopes');
|
||||
}
|
||||
|
||||
// ── DNS ────────────────────────────────────────────────────────────
|
||||
|
||||
export function testDnsConnection(provider: string, token: string, zoneId: string): Promise<{ success: boolean; error?: string }> {
|
||||
@@ -635,107 +429,48 @@ export function exportConfigUrl(): string {
|
||||
return '/api/config/export';
|
||||
}
|
||||
|
||||
// ── Stage Env Overrides ──────────────────────────────────────────────
|
||||
|
||||
export function listStageEnv(projectId: string, stageId: string): Promise<StageEnv[]> {
|
||||
return get<StageEnv[]>(`/api/projects/${projectId}/stages/${stageId}/env`);
|
||||
}
|
||||
|
||||
export function createStageEnv(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
data: { key: string; value: string; encrypted?: boolean }
|
||||
): Promise<StageEnv> {
|
||||
return post<StageEnv>(`/api/projects/${projectId}/stages/${stageId}/env`, data);
|
||||
}
|
||||
|
||||
export function updateStageEnv(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
envId: string,
|
||||
data: { key?: string; value?: string; encrypted?: boolean }
|
||||
): Promise<StageEnv> {
|
||||
return put<StageEnv>(`/api/projects/${projectId}/stages/${stageId}/env/${envId}`, data);
|
||||
}
|
||||
|
||||
export function deleteStageEnv(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
envId: string
|
||||
): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/projects/${projectId}/stages/${stageId}/env/${envId}`);
|
||||
}
|
||||
|
||||
// ── Volumes ──────────────────────────────────────────────────────────
|
||||
|
||||
export function listVolumes(projectId: string): Promise<Volume[]> {
|
||||
return get<Volume[]>(`/api/projects/${projectId}/volumes`);
|
||||
}
|
||||
|
||||
export function createVolume(
|
||||
projectId: string,
|
||||
data: { source: string; target: string; scope: string; name?: string; mode?: string }
|
||||
): Promise<Volume> {
|
||||
return post<Volume>(`/api/projects/${projectId}/volumes`, data);
|
||||
}
|
||||
|
||||
export function updateVolume(
|
||||
projectId: string,
|
||||
volId: string,
|
||||
data: { source?: string; target?: string; scope?: string; name?: string; mode?: string }
|
||||
): Promise<Volume> {
|
||||
return put<Volume>(`/api/projects/${projectId}/volumes/${volId}`, data);
|
||||
}
|
||||
|
||||
export function listVolumeScopes(): Promise<VolumeScopeInfo[]> {
|
||||
return get<VolumeScopeInfo[]>('/api/volumes/scopes');
|
||||
}
|
||||
|
||||
export function deleteVolume(
|
||||
projectId: string,
|
||||
volId: string
|
||||
): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/projects/${projectId}/volumes/${volId}`);
|
||||
}
|
||||
// ── Workload volume browse / download / upload ─────────────────────
|
||||
// The browse/download/upload helpers now target /api/workloads/{id}
|
||||
// instead of the deleted project-scoped path. Source/path/scope params
|
||||
// retain the same query keys for compatibility with the volume editor.
|
||||
|
||||
export function browseVolume(
|
||||
projectId: string,
|
||||
workloadId: string,
|
||||
volId: string,
|
||||
params?: { path?: string; stage?: string; tag?: string }
|
||||
params?: { path?: string; reference?: string }
|
||||
): Promise<BrowseResult> {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.path) query.set('path', params.path);
|
||||
if (params?.stage) query.set('stage', params.stage);
|
||||
if (params?.tag) query.set('tag', params.tag);
|
||||
if (params?.reference) query.set('reference', params.reference);
|
||||
const qs = query.toString();
|
||||
return get<BrowseResult>(`/api/projects/${projectId}/volumes/${volId}/browse${qs ? `?${qs}` : ''}`);
|
||||
return get<BrowseResult>(
|
||||
`/api/workloads/${workloadId}/volumes/${volId}/browse${qs ? `?${qs}` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
export function volumeDownloadUrl(
|
||||
projectId: string,
|
||||
workloadId: string,
|
||||
volId: string,
|
||||
params?: { path?: string; stage?: string; tag?: string }
|
||||
params?: { path?: string; reference?: string }
|
||||
): string {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.path) query.set('path', params.path);
|
||||
if (params?.stage) query.set('stage', params.stage);
|
||||
if (params?.tag) query.set('tag', params.tag);
|
||||
if (params?.reference) query.set('reference', params.reference);
|
||||
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
|
||||
if (token) query.set('token', token);
|
||||
const qs = query.toString();
|
||||
return `/api/projects/${projectId}/volumes/${volId}/download${qs ? `?${qs}` : ''}`;
|
||||
return `/api/workloads/${workloadId}/volumes/${volId}/download${qs ? `?${qs}` : ''}`;
|
||||
}
|
||||
|
||||
export async function uploadToVolume(
|
||||
projectId: string,
|
||||
workloadId: string,
|
||||
volId: string,
|
||||
files: FileList,
|
||||
params?: { path?: string; stage?: string; tag?: string }
|
||||
params?: { path?: string; reference?: string }
|
||||
): Promise<{ uploaded: string[]; count: number }> {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.path) query.set('path', params.path);
|
||||
if (params?.stage) query.set('stage', params.stage);
|
||||
if (params?.tag) query.set('tag', params.tag);
|
||||
if (params?.reference) query.set('reference', params.reference);
|
||||
const qs = query.toString();
|
||||
|
||||
const formData = new FormData();
|
||||
@@ -747,11 +482,14 @@ export async function uploadToVolume(
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`/api/projects/${projectId}/volumes/${volId}/upload${qs ? `?${qs}` : ''}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
const res = await fetch(
|
||||
`/api/workloads/${workloadId}/volumes/${volId}/upload${qs ? `?${qs}` : ''}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
|
||||
const envelope = await res.json();
|
||||
if (!envelope.success) throw new Error(envelope.error ?? 'Upload failed');
|
||||
@@ -779,8 +517,8 @@ export function fetchEventLog(params?: {
|
||||
return get<EventLogEntry[]>(`/api/events/log${qs ? `?${qs}` : ''}`);
|
||||
}
|
||||
|
||||
export function fetchEventLogStats(): Promise<EventLogStats> {
|
||||
return get<EventLogStats>('/api/events/log/stats');
|
||||
export function fetchEventLogStats(signal?: AbortSignal): Promise<EventLogStats> {
|
||||
return get<EventLogStats>('/api/events/log/stats', signal);
|
||||
}
|
||||
|
||||
export function deleteEvent(id: number): Promise<{ status: string }> {
|
||||
@@ -805,32 +543,7 @@ export function bulkCleanupStaleContainers(): Promise<{ deleted: number }> {
|
||||
return post<{ deleted: number }>('/api/containers/stale/cleanup');
|
||||
}
|
||||
|
||||
// ── Container Stats ────────────────────────────────────────────────
|
||||
|
||||
export function fetchContainerStats(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
instanceId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<ContainerStats> {
|
||||
return get<ContainerStats>(
|
||||
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stats`,
|
||||
signal
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchInstanceStatsHistory(
|
||||
projectId: string,
|
||||
stageId: string,
|
||||
instanceId: string,
|
||||
window = '2h',
|
||||
signal?: AbortSignal
|
||||
): Promise<ContainerStatsSample[]> {
|
||||
return get<ContainerStatsSample[]>(
|
||||
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stats/history?window=${encodeURIComponent(window)}`,
|
||||
signal
|
||||
);
|
||||
}
|
||||
// ── System Stats ───────────────────────────────────────────────────
|
||||
|
||||
export function fetchSystemStats(signal?: AbortSignal): Promise<SystemStats> {
|
||||
return get<SystemStats>('/api/system/stats', signal);
|
||||
@@ -851,212 +564,32 @@ export function fetchTopContainers(
|
||||
return get<TopContainerSample[]>(`/api/system/stats/top?by=${by}&limit=${limit}`, signal);
|
||||
}
|
||||
|
||||
export function fetchStaticSiteStats(id: string, signal?: AbortSignal): Promise<ContainerStats> {
|
||||
return get<ContainerStats>(`/api/sites/${id}/stats`, signal);
|
||||
}
|
||||
// ── Per-container stats (workload-scoped) ──────────────────────────
|
||||
// Project / stage / instance + static-site stats endpoints died with
|
||||
// the legacy routes. Use the workload-scoped endpoint for any per-
|
||||
// container CPU/memory drill-down.
|
||||
|
||||
export function fetchStaticSiteStatsHistory(
|
||||
id: string,
|
||||
window = '2h',
|
||||
export function fetchWorkloadContainerStats(
|
||||
workloadId: string,
|
||||
containerRowId: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<ContainerStatsSample[]> {
|
||||
return get<ContainerStatsSample[]>(
|
||||
`/api/sites/${id}/stats/history?window=${encodeURIComponent(window)}`,
|
||||
): Promise<ContainerStats> {
|
||||
return get<ContainerStats>(
|
||||
`/api/workloads/${workloadId}/containers/${containerRowId}/stats`,
|
||||
signal
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchStaticSiteLogs(id: string, tail = 200): Promise<string[]> {
|
||||
const result = await get<string[] | null>(`/api/sites/${id}/logs?tail=${tail}`);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
// ── Static Sites ──────────────────────────────────────────────────────
|
||||
|
||||
import type { StaticSite, StaticSiteSecret, FolderEntry, GitProvider, RepoInfo } from './types';
|
||||
|
||||
export function listStaticSites(signal?: AbortSignal): Promise<StaticSite[]> {
|
||||
return get<StaticSite[]>('/api/sites', signal);
|
||||
}
|
||||
|
||||
export function getStaticSite(id: string): Promise<StaticSite> {
|
||||
return get<StaticSite>(`/api/sites/${id}`);
|
||||
}
|
||||
|
||||
export function createStaticSite(data: Partial<StaticSite>): Promise<StaticSite> {
|
||||
return post<StaticSite>('/api/sites', data);
|
||||
}
|
||||
|
||||
export function updateStaticSite(id: string, data: Partial<StaticSite>): Promise<StaticSite> {
|
||||
return put<StaticSite>(`/api/sites/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteStaticSite(id: string): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/sites/${id}`);
|
||||
}
|
||||
|
||||
export function deployStaticSite(id: string): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(`/api/sites/${id}/deploy`);
|
||||
}
|
||||
|
||||
export function stopStaticSite(id: string): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(`/api/sites/${id}/stop`);
|
||||
}
|
||||
|
||||
export function startStaticSite(id: string): Promise<{ status: string }> {
|
||||
return post<{ status: string }>(`/api/sites/${id}/start`);
|
||||
}
|
||||
|
||||
export function listStaticSiteRepos(data: {
|
||||
provider?: string;
|
||||
gitea_url: string;
|
||||
access_token?: string;
|
||||
query?: string;
|
||||
}): Promise<RepoInfo[]> {
|
||||
return post<RepoInfo[]>('/api/sites/repos', data);
|
||||
}
|
||||
|
||||
export function detectStaticSiteProvider(url: string): Promise<{ provider: GitProvider }> {
|
||||
return post<{ provider: GitProvider }>('/api/sites/detect-provider', { url });
|
||||
}
|
||||
|
||||
export function testStaticSiteConnection(data: {
|
||||
provider?: string;
|
||||
gitea_url: string;
|
||||
access_token?: string;
|
||||
repo_owner: string;
|
||||
repo_name: string;
|
||||
}): Promise<{ status: string }> {
|
||||
return post<{ status: string }>('/api/sites/test-connection', data);
|
||||
}
|
||||
|
||||
export function listStaticSiteBranches(data: {
|
||||
provider?: string;
|
||||
gitea_url: string;
|
||||
access_token?: string;
|
||||
repo_owner: string;
|
||||
repo_name: string;
|
||||
}): Promise<string[]> {
|
||||
return post<string[]>('/api/sites/branches', data);
|
||||
}
|
||||
|
||||
export function listStaticSiteTree(data: {
|
||||
provider?: string;
|
||||
gitea_url: string;
|
||||
access_token?: string;
|
||||
repo_owner: string;
|
||||
repo_name: string;
|
||||
branch: string;
|
||||
}): Promise<FolderEntry[]> {
|
||||
return post<FolderEntry[]>('/api/sites/tree', data);
|
||||
}
|
||||
|
||||
export function listStaticSiteSecrets(siteId: string): Promise<StaticSiteSecret[]> {
|
||||
return get<StaticSiteSecret[]>(`/api/sites/${siteId}/secrets`);
|
||||
}
|
||||
|
||||
export function createStaticSiteSecret(
|
||||
siteId: string,
|
||||
data: { key: string; value: string; encrypted?: boolean }
|
||||
): Promise<StaticSiteSecret> {
|
||||
return post<StaticSiteSecret>(`/api/sites/${siteId}/secrets`, data);
|
||||
}
|
||||
|
||||
export function updateStaticSiteSecret(
|
||||
siteId: string,
|
||||
secretId: string,
|
||||
data: { key?: string; value?: string; encrypted?: boolean }
|
||||
): Promise<StaticSiteSecret> {
|
||||
return put<StaticSiteSecret>(`/api/sites/${siteId}/secrets/${secretId}`, data);
|
||||
}
|
||||
|
||||
export function deleteStaticSiteSecret(
|
||||
siteId: string,
|
||||
secretId: string
|
||||
): Promise<{ deleted: string }> {
|
||||
return del<{ deleted: string }>(`/api/sites/${siteId}/secrets/${secretId}`);
|
||||
}
|
||||
|
||||
export function getStaticSiteStorage(
|
||||
siteId: string
|
||||
): Promise<import('./types').StaticSiteStorageUsage> {
|
||||
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 function fetchWorkloadContainerStatsHistory(
|
||||
workloadId: string,
|
||||
containerRowId: string,
|
||||
window = '2h',
|
||||
signal?: AbortSignal
|
||||
): Promise<ContainerStatsSample[]> {
|
||||
return get<ContainerStatsSample[]>(
|
||||
`/api/workloads/${workloadId}/containers/${containerRowId}/stats/history?window=${encodeURIComponent(window)}`,
|
||||
signal
|
||||
);
|
||||
}
|
||||
|
||||
// ── Workloads ───────────────────────────────────────────────────────
|
||||
@@ -1126,20 +659,10 @@ export function deleteWorkloadEnv(id: string, envID: string): Promise<{ deleted:
|
||||
return del<{ deleted: string }>(`/api/workloads/${id}/env/${envID}`);
|
||||
}
|
||||
|
||||
export interface WorkloadWebhook {
|
||||
webhook_url: string;
|
||||
webhook_secret: string;
|
||||
has_signing_secret: boolean;
|
||||
webhook_require_signature: boolean;
|
||||
}
|
||||
|
||||
export function getWorkloadWebhook(id: string, signal?: AbortSignal): Promise<WorkloadWebhook> {
|
||||
return get<WorkloadWebhook>(`/api/workloads/${id}/webhook`, signal);
|
||||
}
|
||||
|
||||
export function regenerateWorkloadWebhook(id: string): Promise<WorkloadWebhook> {
|
||||
return post<WorkloadWebhook>(`/api/workloads/${id}/webhook/regenerate`);
|
||||
}
|
||||
// Workload-level webhook URL accessors were removed in the hard legacy
|
||||
// cutover: inbound webhooks are now first-class Triggers. To wire a
|
||||
// workload to inbound deploys, create or bind a Trigger via the
|
||||
// /triggers UI (which mints a /api/webhook/triggers/{secret} URL).
|
||||
|
||||
export function fetchWorkloadContainerLogs(
|
||||
workloadId: string,
|
||||
|
||||
Reference in New Issue
Block a user