feat(workload): global Containers tab + frontend client

Adds the user-visible piece of the Workload refactor:

- web/src/lib/types.ts          — Workload, Container, ContainerView,
                                  App, WorkloadKind, ContainerState
- web/src/lib/api.ts            — listWorkloads, getWorkload,
                                  listWorkloadContainers, setWorkloadAppID,
                                  listContainers (with filter),
                                  CRUD for apps
- web/src/lib/i18n/{en,ru}.json — nav.containers
- web/src/routes/+layout.svelte — "Containers" nav item between Stacks
                                  and Deploy, IconContainer
- web/src/routes/containers/+page.svelte — global Containers table:
    * filter chips for kind (project/stack/site) and state
    * client-side search across workload name / role / image /
      subdomain / container ID prefix
    * Workload column links to the kind-specific detail page,
      resolved through a one-time /api/workloads call to map
      workload_id → ref_id
    * existing /containers/stale route untouched

The page renders against the live database now — boot backfill
populated workload rows from existing projects/stacks/sites,
the deployer dual-writes containers on every deploy, and the
30s reconciler keeps the index in sync with `docker ps`.
This commit is contained in:
2026-05-09 14:02:20 +03:00
parent 0acbcda084
commit 3e28588f10
6 changed files with 417 additions and 4 deletions
+69 -1
View File
@@ -1,7 +1,10 @@
import type {
ApiEnvelope,
App,
Container,
ContainerStats,
ContainerStatsSample,
ContainerView,
SystemStats,
SystemStatsSample,
TopContainerSample,
@@ -30,7 +33,9 @@ import type {
BrowseResult,
DnsZone,
DnsRecordView,
BackupInfo
BackupInfo,
Workload,
WorkloadKind
} from './types';
// ── Helpers ─────────────────────────────────────────────────────────
@@ -1047,4 +1052,67 @@ export async function getStackLogs(
return res.text();
}
// ── Workloads ───────────────────────────────────────────────────────
export function listWorkloads(kind?: WorkloadKind, signal?: AbortSignal): Promise<Workload[]> {
const path = kind ? `/api/workloads?kind=${encodeURIComponent(kind)}` : '/api/workloads';
return get<Workload[]>(path, signal);
}
export function getWorkload(id: string, signal?: AbortSignal): Promise<Workload> {
return get<Workload>(`/api/workloads/${id}`, signal);
}
export function listWorkloadContainers(id: string, signal?: AbortSignal): Promise<Container[]> {
return get<Container[]>(`/api/workloads/${id}/containers`, signal);
}
export function setWorkloadAppID(id: string, appID: string): Promise<Workload> {
return request<Workload>(`/api/workloads/${id}/app`, {
method: 'PATCH',
body: JSON.stringify({ app_id: appID })
});
}
// ── Containers (global index) ───────────────────────────────────────
export interface ListContainersFilter {
workload_id?: string;
kind?: WorkloadKind;
state?: string;
app_id?: string;
}
export function listContainers(filter: ListContainersFilter = {}, signal?: AbortSignal): Promise<ContainerView[]> {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(filter)) {
if (v) params.set(k, String(v));
}
const qs = params.toString();
const path = qs ? `/api/containers?${qs}` : '/api/containers';
return get<ContainerView[]>(path, signal);
}
// ── Apps ────────────────────────────────────────────────────────────
export function listApps(signal?: AbortSignal): Promise<App[]> {
return get<App[]>('/api/apps', signal);
}
export function getApp(id: string, signal?: AbortSignal): Promise<App> {
return get<App>(`/api/apps/${id}`, signal);
}
export function createApp(data: { name: string; description?: string }): Promise<App> {
return post<App>('/api/apps', data);
}
export function updateApp(id: string, data: { name: string; description?: string }): Promise<App> {
return put<App>(`/api/apps/${id}`, data);
}
export function deleteApp(id: string): Promise<void> {
return del<void>(`/api/apps/${id}`);
}
export { ApiError };