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,
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<!--
|
||||
Container log viewer with tail line limit and auto-scroll.
|
||||
|
||||
Works for both project instances and static sites — pass a `source`
|
||||
discriminated union to point at the right endpoint.
|
||||
Workload-scoped only after the hard cutover. The legacy `instance` and
|
||||
`site` source variants targeted /api/projects/.../logs and
|
||||
/api/sites/.../logs respectively — both gone with their routes.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import {
|
||||
fetchContainerLogs,
|
||||
fetchStaticSiteLogs,
|
||||
fetchWorkloadContainerLogs
|
||||
} from '$lib/api';
|
||||
import { fetchWorkloadContainerLogs } from '$lib/api';
|
||||
import { getAuthToken } from '$lib/auth';
|
||||
import { t } from '$lib/i18n';
|
||||
import { IconLoader, IconX } from '$lib/components/icons';
|
||||
|
||||
export type LogSource =
|
||||
| { kind: 'instance'; projectId: string; stageId: string; instanceId: string }
|
||||
| { kind: 'site'; siteId: string }
|
||||
| { kind: 'workload'; workloadId: string; containerRowId: string };
|
||||
export type LogSource = {
|
||||
kind: 'workload';
|
||||
workloadId: string;
|
||||
containerRowId: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
source: LogSource;
|
||||
@@ -60,23 +58,11 @@
|
||||
|
||||
function buildFollowUrl(token: string | null): string {
|
||||
const tokenParam = token ? `&token=${token}` : '';
|
||||
if (source.kind === 'instance') {
|
||||
return `/api/projects/${source.projectId}/stages/${source.stageId}/instances/${source.instanceId}/logs?follow=true&tail=0${tokenParam}`;
|
||||
}
|
||||
if (source.kind === 'workload') {
|
||||
return `/api/workloads/${source.workloadId}/containers/${source.containerRowId}/logs?follow=true&tail=0${tokenParam}`;
|
||||
}
|
||||
return `/api/sites/${source.siteId}/logs?follow=true&tail=0${tokenParam}`;
|
||||
return `/api/workloads/${source.workloadId}/containers/${source.containerRowId}/logs?follow=true&tail=0${tokenParam}`;
|
||||
}
|
||||
|
||||
async function fetchLogs(tail: number): Promise<string[]> {
|
||||
if (source.kind === 'instance') {
|
||||
return fetchContainerLogs(source.projectId, source.stageId, source.instanceId, tail);
|
||||
}
|
||||
if (source.kind === 'workload') {
|
||||
return fetchWorkloadContainerLogs(source.workloadId, source.containerRowId, tail);
|
||||
}
|
||||
return fetchStaticSiteLogs(source.siteId, tail);
|
||||
return fetchWorkloadContainerLogs(source.workloadId, source.containerRowId, tail);
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<!--
|
||||
Compact CPU/memory stats bars with an optional expandable history
|
||||
chart. Works for both project instances and static sites via the
|
||||
`source` discriminated union.
|
||||
chart. Workload-scoped only after the hard cutover — pass the
|
||||
workload + container-row identifiers; the legacy project-instance and
|
||||
static-site variants targeted dead endpoints.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { ContainerStats, ContainerStatsSample } from '$lib/types';
|
||||
@@ -12,9 +13,11 @@
|
||||
import Sparkline from './Sparkline.svelte';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
|
||||
export type StatsSource =
|
||||
| { kind: 'instance'; projectId: string; stageId: string; instanceId: string }
|
||||
| { kind: 'site'; siteId: string };
|
||||
export type StatsSource = {
|
||||
kind: 'workload';
|
||||
workloadId: string;
|
||||
containerRowId: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
source: StatsSource;
|
||||
@@ -34,23 +37,16 @@
|
||||
let expanded = $state(false);
|
||||
|
||||
async function fetchStats(signal: AbortSignal): Promise<ContainerStats> {
|
||||
if (source.kind === 'instance') {
|
||||
return api.fetchContainerStats(source.projectId, source.stageId, source.instanceId, signal);
|
||||
}
|
||||
return api.fetchStaticSiteStats(source.siteId, signal);
|
||||
return api.fetchWorkloadContainerStats(source.workloadId, source.containerRowId, signal);
|
||||
}
|
||||
|
||||
async function fetchHistory(signal: AbortSignal, win: string = historyWindow): Promise<ContainerStatsSample[]> {
|
||||
if (source.kind === 'instance') {
|
||||
return api.fetchInstanceStatsHistory(
|
||||
source.projectId,
|
||||
source.stageId,
|
||||
source.instanceId,
|
||||
win,
|
||||
signal
|
||||
);
|
||||
}
|
||||
return api.fetchStaticSiteStatsHistory(source.siteId, win, signal);
|
||||
return api.fetchWorkloadContainerStatsHistory(
|
||||
source.workloadId,
|
||||
source.containerRowId,
|
||||
win,
|
||||
signal
|
||||
);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
<!--
|
||||
Task 5: Instance card with inline status badges, icon action buttons, improved layout.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Instance } from '$lib/types';
|
||||
import StatusBadge from './StatusBadge.svelte';
|
||||
import ContainerStats from './ContainerStats.svelte';
|
||||
import ContainerLogs from './ContainerLogs.svelte';
|
||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||
import { IconPlay, IconStop, IconRestart, IconTrash, IconExternalLink, IconEvents } from '$lib/components/icons';
|
||||
import { t } from '$lib/i18n';
|
||||
import { fmt } from '$lib/format/datetime';
|
||||
import * as api from '$lib/api';
|
||||
|
||||
interface Props {
|
||||
instance: Instance;
|
||||
projectId: string;
|
||||
stageId: string;
|
||||
domain?: string;
|
||||
onchange?: () => void;
|
||||
}
|
||||
|
||||
const { instance, projectId, stageId, domain = '', onchange }: Props = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
let confirmAction = $state<'stop' | 'restart' | 'remove' | null>(null);
|
||||
let showLogs = $state(false);
|
||||
|
||||
const subdomainUrl = $derived(
|
||||
instance.subdomain && domain
|
||||
? `https://${instance.subdomain}.${domain}`
|
||||
: instance.subdomain ? `https://${instance.subdomain}` : ''
|
||||
);
|
||||
|
||||
const timeSinceCreated = $derived($fmt.relative(instance.created_at));
|
||||
|
||||
async function handleAction(action: 'stop' | 'start' | 'restart' | 'remove') {
|
||||
loading = true;
|
||||
error = '';
|
||||
confirmAction = null;
|
||||
try {
|
||||
switch (action) {
|
||||
case 'stop':
|
||||
await api.stopInstance(projectId, stageId, instance.id);
|
||||
break;
|
||||
case 'start':
|
||||
await api.startInstance(projectId, stageId, instance.id);
|
||||
break;
|
||||
case 'restart':
|
||||
await api.restartInstance(projectId, stageId, instance.id);
|
||||
break;
|
||||
case 'remove':
|
||||
await api.removeInstance(projectId, stageId, instance.id);
|
||||
break;
|
||||
}
|
||||
onchange?.();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : $t('instance.actionFailed');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function requestConfirm(action: 'stop' | 'restart' | 'remove') {
|
||||
confirmAction = action;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-4 shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)]">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate font-mono text-sm font-medium text-[var(--text-primary)]">
|
||||
{instance.image_tag}
|
||||
</span>
|
||||
<StatusBadge status={instance.state} size="sm" />
|
||||
</div>
|
||||
|
||||
{#if subdomainUrl}
|
||||
<a
|
||||
href={subdomainUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="mt-1.5 inline-flex items-center gap-1 text-xs text-[var(--text-link)] hover:text-[var(--text-link-hover)] transition-colors"
|
||||
>
|
||||
{instance.subdomain}
|
||||
<IconExternalLink size={12} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1.5 flex items-center gap-3 text-xs text-[var(--text-tertiary)]">
|
||||
<span class="rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 font-mono">:{instance.port}</span>
|
||||
<span>{timeSinceCreated}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="ml-3 flex items-center gap-1">
|
||||
{#if instance.state === 'running'}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-amber-50 hover:text-amber-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.stop')}
|
||||
disabled={loading}
|
||||
onclick={() => requestConfirm('stop')}
|
||||
>
|
||||
<IconStop size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-blue-50 hover:text-blue-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.restart')}
|
||||
disabled={loading}
|
||||
onclick={() => requestConfirm('restart')}
|
||||
>
|
||||
<IconRestart size={16} />
|
||||
</button>
|
||||
{:else if instance.state === 'stopped'}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-emerald-50 hover:text-emerald-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.start')}
|
||||
disabled={loading}
|
||||
onclick={() => handleAction('start')}
|
||||
>
|
||||
<IconPlay size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300 transition-all duration-150"
|
||||
title={$t('logs.title')}
|
||||
onclick={() => { showLogs = !showLogs; }}
|
||||
>
|
||||
<IconEvents size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-red-50 hover:text-red-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
|
||||
title={$t('common.remove')}
|
||||
disabled={loading}
|
||||
onclick={() => requestConfirm('remove')}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if instance.state === 'running'}
|
||||
<ContainerStats source={{ kind: 'instance', projectId, stageId: stageId, instanceId: instance.id }} />
|
||||
{/if}
|
||||
|
||||
{#if showLogs}
|
||||
<div class="mt-2">
|
||||
<ContainerLogs
|
||||
source={{ kind: 'instance', projectId, stageId: stageId, instanceId: instance.id }}
|
||||
onclose={() => { showLogs = false; }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<p class="mt-2 text-xs text-[var(--color-danger)]">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmAction !== null}
|
||||
title={confirmAction ? $t(`confirm.${confirmAction}Instance`) : ''}
|
||||
message={confirmAction ? $t(`instance.${confirmAction}Confirm`) : ''}
|
||||
confirmLabel={confirmAction ? $t(`confirm.${confirmAction}Action`) : ''}
|
||||
confirmVariant={confirmAction === 'remove' ? 'danger' : 'primary'}
|
||||
onconfirm={() => { if (confirmAction) handleAction(confirmAction); }}
|
||||
oncancel={() => { confirmAction = null; }}
|
||||
/>
|
||||
@@ -1,83 +0,0 @@
|
||||
<!--
|
||||
Task 4: Redesigned project card with status indicators, instance count badges, hover effects.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Project, Instance } from '$lib/types';
|
||||
import StatusBadge from './StatusBadge.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { IconContainer, IconBox } from '$lib/components/icons';
|
||||
|
||||
interface Props {
|
||||
project: Project;
|
||||
instances?: Instance[];
|
||||
}
|
||||
|
||||
const { project, instances = [] }: Props = $props();
|
||||
|
||||
const runningCount = $derived(instances.filter((i) => i.state === 'running').length);
|
||||
const stoppedCount = $derived(instances.filter((i) => i.state === 'stopped').length);
|
||||
const failedCount = $derived(instances.filter((i) => i.state === 'failed').length);
|
||||
const totalCount = $derived(instances.length);
|
||||
|
||||
const overallStatus = $derived.by<'failed' | 'running' | 'stopped'>(() => {
|
||||
if (failedCount > 0) return 'failed';
|
||||
if (runningCount > 0) return 'running';
|
||||
if (stoppedCount > 0) return 'stopped';
|
||||
return 'stopped';
|
||||
});
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/projects/{project.id}"
|
||||
class="group block rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)] transition-all duration-200 hover:border-[var(--color-brand-300)] hover:shadow-[var(--shadow-md)] hover:-translate-y-0.5"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--color-brand-50)] text-[var(--color-brand-600)] transition-colors group-hover:bg-[var(--color-brand-100)]">
|
||||
<IconBox size={16} />
|
||||
</div>
|
||||
<h3 class="truncate text-base font-semibold text-[var(--text-primary)]">{project.name}</h3>
|
||||
</div>
|
||||
<p class="mt-2 truncate font-mono text-xs text-[var(--text-tertiary)]">{project.image}</p>
|
||||
</div>
|
||||
<StatusBadge status={overallStatus} size="sm" />
|
||||
</div>
|
||||
|
||||
<!-- Instance count badges -->
|
||||
<div class="mt-4 flex items-center gap-3 text-sm">
|
||||
{#if totalCount > 0}
|
||||
<span class="inline-flex items-center gap-1.5 text-[var(--text-secondary)]">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-500"></span>
|
||||
<span class="text-xs font-medium">{runningCount}</span>
|
||||
</span>
|
||||
{#if stoppedCount > 0}
|
||||
<span class="inline-flex items-center gap-1.5 text-[var(--text-secondary)]">
|
||||
<span class="h-2 w-2 rounded-full bg-gray-400"></span>
|
||||
<span class="text-xs font-medium">{stoppedCount}</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if failedCount > 0}
|
||||
<span class="inline-flex items-center gap-1.5 text-[var(--text-secondary)]">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
<span class="text-xs font-medium">{failedCount}</span>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="ml-auto rounded-full bg-[var(--surface-card-hover)] px-2 py-0.5 text-xs font-medium text-[var(--text-tertiary)]">
|
||||
{totalCount} {totalCount === 1 ? $t('common.instance') : $t('common.instances')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-xs text-[var(--text-tertiary)]">{$t('projectDetail.noInstancesRunning')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="mt-3 flex items-center gap-3 text-xs text-[var(--text-tertiary)]">
|
||||
{#if project.port}
|
||||
<span class="rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 font-mono">:{project.port}</span>
|
||||
{/if}
|
||||
{#if project.healthcheck}
|
||||
<span class="truncate">{project.healthcheck}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,85 +0,0 @@
|
||||
<!--
|
||||
Card displaying a single stale container with cleanup action.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { StaleContainer } from '$lib/types';
|
||||
import { IconClock, IconTag, IconTrash } from '$lib/components/icons';
|
||||
import { t } from '$lib/i18n';
|
||||
import { fmt } from '$lib/format/datetime';
|
||||
|
||||
interface Props {
|
||||
container: StaleContainer;
|
||||
cleaning?: boolean;
|
||||
oncleanup: (id: string) => void;
|
||||
}
|
||||
|
||||
const { container, cleaning = false, oncleanup }: Props = $props();
|
||||
|
||||
const badgeClass = $derived(
|
||||
container.days_stale >= 14
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
);
|
||||
|
||||
const displayName = $derived(
|
||||
container.role
|
||||
? `${container.workload_name}-${container.role}-${container.container.image_tag}`
|
||||
: `${container.workload_name}-${container.container.image_tag}`
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)] transition-colors hover:bg-[var(--surface-card-hover)]">
|
||||
<!-- Header row -->
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="truncate text-sm font-semibold text-[var(--text-primary)]" title={displayName}>
|
||||
{displayName}
|
||||
</h3>
|
||||
<div class="mt-1.5 flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-[var(--color-brand-50)] px-2 py-0.5 text-xs font-medium text-[var(--color-brand-600)]">
|
||||
{container.workload_name}
|
||||
</span>
|
||||
{#if container.role}
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-[var(--surface-card-hover)] px-2 py-0.5 text-xs font-medium text-[var(--text-secondary)]">
|
||||
{container.role}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Days stale badge -->
|
||||
<span class="inline-flex flex-shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-xs font-semibold {badgeClass}">
|
||||
<IconClock size={12} />
|
||||
{container.days_stale} {$t('stale.daysStale')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-[var(--text-secondary)]">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<IconTag size={12} />
|
||||
{container.container.image_tag}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<IconClock size={12} />
|
||||
{$t('stale.lastAlive')}: {$fmt.shortDate(container.container.last_seen_at)}
|
||||
</span>
|
||||
<span class="rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 font-mono text-[10px]">
|
||||
{container.container.state}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup button -->
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
disabled={cleaning}
|
||||
onclick={() => oncleanup(container.container.id)}
|
||||
class="inline-flex items-center gap-1.5 rounded-lg border border-[var(--color-danger)] px-3 py-1.5 text-xs font-medium text-[var(--color-danger)] transition-colors hover:bg-[var(--color-danger-light)] disabled:opacity-50 active:animate-press"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
{$t('stale.cleanup')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { InstanceStatus, DeployStatus } from '$lib/types';
|
||||
|
||||
type Status = InstanceStatus | DeployStatus | string;
|
||||
// The badge accepts any status string — known container / deploy /
|
||||
// site states render with a colored variant; unknown values fall
|
||||
// through to the neutral pill. Typed as plain string after the
|
||||
// legacy InstanceStatus / DeployStatus unions were retired.
|
||||
type Status = string;
|
||||
|
||||
interface Props {
|
||||
status: Status;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<!--
|
||||
Dashboard summary card: container counts and recent errors.
|
||||
|
||||
Workload-first: pulls running/total counts straight from the global
|
||||
containers index instead of fanning out project → stage → instance
|
||||
queries the way the legacy implementation did.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Instance, EventLogStats } from '$lib/types';
|
||||
import type { EventLogStats } from '$lib/types';
|
||||
import * as api from '$lib/api';
|
||||
import { IconServer, IconAlert } from '$lib/components/icons';
|
||||
import { t } from '$lib/i18n';
|
||||
@@ -13,37 +17,24 @@
|
||||
let loading = $state(true);
|
||||
|
||||
$effect(() => {
|
||||
let cancelled = false;
|
||||
const ac = new AbortController();
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const [projects, eventStats] = await Promise.all([
|
||||
api.listProjects(),
|
||||
api.fetchEventLogStats().catch(() => ({ info: 0, warn: 0, error: 0, total: 0 }) as EventLogStats)
|
||||
const [containers, eventStats] = await Promise.all([
|
||||
api.listContainers({}, ac.signal).catch(() => []),
|
||||
api.fetchEventLogStats(ac.signal).catch(
|
||||
() => ({ info: 0, warn: 0, error: 0, total: 0 }) as EventLogStats
|
||||
)
|
||||
]);
|
||||
|
||||
// Gather all instances across projects/stages.
|
||||
const allInstances: Instance[] = [];
|
||||
for (const project of projects) {
|
||||
try {
|
||||
const detail = await api.getProject(project.id);
|
||||
for (const stage of detail.stages ?? []) {
|
||||
const instances = await api.listInstances(project.id, stage.id);
|
||||
allInstances.push(...instances);
|
||||
}
|
||||
} catch {
|
||||
// Skip projects that fail to load.
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
runningCount = allInstances.filter((i) => i.state === 'running').length;
|
||||
stoppedCount = allInstances.filter((i) => i.state !== 'running').length;
|
||||
recentErrors = eventStats.error;
|
||||
loading = false;
|
||||
}
|
||||
if (ac.signal.aborted) return;
|
||||
runningCount = containers.filter((c) => c.state === 'running').length;
|
||||
stoppedCount = containers.filter((c) => c.state !== 'running').length;
|
||||
recentErrors = eventStats.error;
|
||||
loading = false;
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
if (!ac.signal.aborted) {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
@@ -51,9 +42,7 @@
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
return () => ac.abort();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -62,7 +51,7 @@
|
||||
<h3 class="mb-4 text-sm font-semibold text-[var(--text-primary)]">{$t('systemHealth.title')}</h3>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<!-- Containers -->
|
||||
<a href="/projects" class="flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-[var(--surface-card-hover)]">
|
||||
<a href="/containers" class="flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-[var(--surface-card-hover)]">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-50 dark:bg-emerald-950/30 text-emerald-600">
|
||||
<IconServer size={18} />
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
>
|
||||
<span class="truncate font-mono">{item.tag}</span>
|
||||
<span class="ml-2 shrink-0 rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 text-[10px] text-[var(--text-tertiary)]">
|
||||
{item.source === 'registry' ? $t('projectDetail.registryTag') : $t('projectDetail.localTag')}
|
||||
{item.source === 'registry' ? $t('tagPicker.registry') : $t('tagPicker.local')}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<!--
|
||||
WebhookDeliveryLog
|
||||
|
||||
Recent inbound webhook activity panel. Used on the project + site detail
|
||||
pages so users can debug "why didn't my deploy fire?" without grepping
|
||||
daemon logs. Polls the audit table every 30s while the panel is mounted.
|
||||
Recent inbound webhook activity panel. Mounted on the Trigger detail
|
||||
page (the project + site detail pages it once served were dropped in
|
||||
the hard legacy cutover). Polls the audit table every 30s so users can
|
||||
debug "why didn't my deploy fire?" without grepping daemon logs.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
+30
-576
@@ -18,38 +18,27 @@
|
||||
"eventTriggers": "Triggers",
|
||||
"logScanRules": "Log Rules",
|
||||
"triggers": "Triggers",
|
||||
"projects": "Projects",
|
||||
"deploy": "Deploy",
|
||||
"proxies": "Proxies",
|
||||
"events": "Events",
|
||||
"settings": "Settings",
|
||||
"logout": "Log out",
|
||||
"dns": "DNS Records",
|
||||
"sites": "Sites",
|
||||
"stacks": "Stacks",
|
||||
"containers": "Containers"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"quickDeploy": "Quick Deploy",
|
||||
"totalProjects": "Total Projects",
|
||||
"runningInstances": "Running Instances",
|
||||
"failedInstances": "Failed Instances",
|
||||
"projects": "Projects",
|
||||
"newApp": "New app",
|
||||
"totalWorkloads": "Total Workloads",
|
||||
"runningContainers": "Running Containers",
|
||||
"failedContainers": "Failed Containers",
|
||||
"recentWorkloads": "Recent Workloads",
|
||||
"retry": "Retry",
|
||||
"noProjects": "No projects yet.",
|
||||
"addFirst": "Add your first project",
|
||||
"noWorkloads": "No workloads yet.",
|
||||
"noWorkloadsDesc": "Create an app and forge your first workload to get started.",
|
||||
"loadFailed": "Failed to load dashboard",
|
||||
"staleContainers": "Stale Containers",
|
||||
"unusedImagesWarning": "Unused Docker images are taking up disk space",
|
||||
"unusedImages": "unused images",
|
||||
"staticSites": "Static Sites",
|
||||
"totalSites": "Total Sites",
|
||||
"deployedSites": "deployed",
|
||||
"failedSites": "failed",
|
||||
"noSites": "No static sites yet.",
|
||||
"addFirstSite": "Deploy your first site",
|
||||
"viewAllSites": "View all sites",
|
||||
"systemHealth": "System health",
|
||||
"daemons": "Daemons",
|
||||
"systemResources": "System resources",
|
||||
@@ -92,240 +81,9 @@
|
||||
"retentionLabel": "Stats retention (hours)",
|
||||
"retentionHelp": "How long resource samples are kept. 0 disables collection. Range: 0–24h."
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projects",
|
||||
"addProject": "Add Project",
|
||||
"cancel": "Cancel",
|
||||
"newProject": "New Project",
|
||||
"name": "Name",
|
||||
"image": "Image",
|
||||
"port": "Port",
|
||||
"tagPicker": {
|
||||
"registry": "Registry",
|
||||
"created": "Created",
|
||||
"view": "View",
|
||||
"noProjects": "No projects configured yet.",
|
||||
"getStarted": "Click \"Add Project\" to get started.",
|
||||
"createProject": "Create Project",
|
||||
"creating": "Creating...",
|
||||
"healthcheck": "Healthcheck Path",
|
||||
"nameRequired": "Name and image are required.",
|
||||
"loadFailed": "Failed to load projects",
|
||||
"createFailed": "Failed to create project",
|
||||
"browseImages": "Browse Images",
|
||||
"selectImage": "Select an image",
|
||||
"noImages": "No images found",
|
||||
"loadingImages": "Loading images...",
|
||||
"imageLoadFailed": "Failed to load images",
|
||||
"alreadyAdded": "Already added",
|
||||
"portHelpText": "Auto-detected from EXPOSE if empty",
|
||||
"healthcheckHelpText": "Auto-detected from image if empty",
|
||||
"searchPlaceholder": "Search projects by name, image, or registry...",
|
||||
"noMatchingProjects": "No projects match your search."
|
||||
},
|
||||
"projectDetail": {
|
||||
"webhookTitle": "Project webhook",
|
||||
"webhookDesc": "POST an image reference to this URL from your CI pipeline to trigger a deploy. Stage routing uses each stage's tag pattern.",
|
||||
"outgoingWebhookTitle": "Outgoing webhook (project)",
|
||||
"outgoingWebhookDesc": "Where Tinyforge posts deploy events for this project. Stages can override; if none set, inherits from global settings.",
|
||||
"outgoingFallbackGlobal": "the global integrations setting",
|
||||
"notificationUrlLabel": "Outgoing webhook URL",
|
||||
"notificationUrlHelp": "Leave empty to inherit from global settings. Stages can override per-stage.",
|
||||
"stageNotificationUrlLabel": "Outgoing webhook URL (this stage)",
|
||||
"stageNotificationUrlHelp": "Leave empty to inherit from the project, then global settings.",
|
||||
"stageOutgoingTitle": "Outgoing webhook (stage)",
|
||||
"stageOutgoingDesc": "Where Tinyforge posts deploy events for this stage. Most-specific tier wins.",
|
||||
"stageFallbackLabel": "the project or global settings",
|
||||
"deleteProject": "Delete Project",
|
||||
"envVars": "Environment Variables",
|
||||
"volumes": "Volume Mounts",
|
||||
"stages": "Stages",
|
||||
"noStages": "No stages configured for this project.",
|
||||
"pattern": "Pattern",
|
||||
"autoDeploy": "auto-deploy",
|
||||
"requiresConfirm": "requires confirm",
|
||||
"instances": "instances",
|
||||
"deployNewVersion": "Deploy new version",
|
||||
"selectTag": "Select tag to deploy",
|
||||
"loadingTags": "Loading tags...",
|
||||
"chooseTag": "Choose a tag...",
|
||||
"enterTag": "Enter image tag (e.g., dev-abc123)",
|
||||
"registryTag": "Registry",
|
||||
"localTag": "Local",
|
||||
"alsoLocal": "Also available locally",
|
||||
"searchTags": "Search tags...",
|
||||
"deployTag": "Tag",
|
||||
"deploy": "Deploy",
|
||||
"deploying": "Deploying...",
|
||||
"recentDeploys": "Recent Deploys",
|
||||
"noDeployHistory": "No deploy history for this project.",
|
||||
"tag": "Tag",
|
||||
"status": "Status",
|
||||
"started": "Started",
|
||||
"finished": "Finished",
|
||||
"error": "Error",
|
||||
"noInstancesRunning": "No instances running",
|
||||
"deleteConfirmTitle": "Delete Project",
|
||||
"deleteConfirmMessage": "This will permanently delete the project '{name}' and all its stages, instances, and deploy history. This cannot be undone.",
|
||||
"loadFailed": "Failed to load project",
|
||||
"deleteFailed": "Failed to delete project",
|
||||
"deployFailed": "Deploy failed",
|
||||
"nameLabel": "Name *",
|
||||
"imageLabel": "Image *",
|
||||
"portLabel": "Port",
|
||||
"healthcheckLabel": "Healthcheck Path",
|
||||
"saving": "Saving...",
|
||||
"addStage": "Add Stage",
|
||||
"tagPattern": "Tag Pattern",
|
||||
"tagPatternHelp": "Glob pattern (e.g., dev-*, v*)",
|
||||
"maxInstances": "Max Instances",
|
||||
"autoDeployLabel": "Auto Deploy",
|
||||
"enableProxy": "Enable Proxy",
|
||||
"accessListId": "NPM Access List ID",
|
||||
"accessListIdHelp": "Override the global access list for this project. Clear to inherit from NPM settings.",
|
||||
"localImages": "Local Docker Images",
|
||||
"imageTag": "Tag",
|
||||
"imageId": "Image ID",
|
||||
"imageSize": "Size",
|
||||
"imageCreated": "Created",
|
||||
"cpuLimit": "CPU Limit (cores)",
|
||||
"cpuLimitHelp": "e.g., 0.5, 1, 2. Leave 0 for unlimited",
|
||||
"memoryLimit": "Memory Limit (MB)",
|
||||
"memoryLimitHelp": "e.g., 256, 512, 1024. Leave 0 for unlimited",
|
||||
"npmProxy": "NPM Proxy",
|
||||
"creating": "Creating...",
|
||||
"createStage": "Create Stage",
|
||||
"noProxy": "No Proxy",
|
||||
"deleteStage": "Delete stage",
|
||||
"deleteStageConfirm": "Delete stage \"{name}\"?",
|
||||
"stageCreated": "Stage \"{name}\" created",
|
||||
"stageUpdated": "Stage updated",
|
||||
"stageUpdateFailed": "Failed to update stage",
|
||||
"stageDeleted": "Stage \"{name}\" deleted",
|
||||
"projectUpdated": "Project updated",
|
||||
"updateFailed": "Failed to update project",
|
||||
"stageCreateFailed": "Failed to create stage",
|
||||
"stageDeleteFailed": "Failed to delete stage"
|
||||
},
|
||||
"envEditor": {
|
||||
"title": "Environment Variables",
|
||||
"description": "Manage per-stage environment variable overrides. Stage-level values override project-level defaults.",
|
||||
"stage": "Stage",
|
||||
"projectDefaults": "Project-Level Defaults",
|
||||
"noProjectEnv": "No project-level environment variables defined yet.",
|
||||
"stageOverrides": "Stage Overrides",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"secret": "Secret",
|
||||
"source": "Source",
|
||||
"actions": "Actions",
|
||||
"overridden": "overridden",
|
||||
"inherited": "inherited",
|
||||
"overridesProject": "overrides project",
|
||||
"stageOnly": "stage only",
|
||||
"edit": "Edit",
|
||||
"change": "Change",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"add": "Add",
|
||||
"adding": "Adding...",
|
||||
"noStages": "No stages configured. Add stages to the project first.",
|
||||
"loadFailed": "Failed to load project",
|
||||
"envAdded": "Environment variable added",
|
||||
"envUpdated": "Environment variable updated",
|
||||
"envDeleted": "Environment variable deleted",
|
||||
"addFailed": "Failed to add env var",
|
||||
"updateFailed": "Failed to update env var",
|
||||
"deleteFailed": "Failed to delete env var",
|
||||
"loadEnvFailed": "Failed to load env vars",
|
||||
"leaveEmptyToKeep": "Leave empty to keep current",
|
||||
"deleteTitle": "Delete Environment Variable",
|
||||
"deleteMessage": "Are you sure you want to delete this environment variable? This action cannot be undone."
|
||||
},
|
||||
"volumeEditor": {
|
||||
"title": "Volume Mounts",
|
||||
"description": "Configure volume mounts for containers. Choose a scope to control how volumes are shared between deploys.",
|
||||
"sourceHost": "Source (Host)",
|
||||
"targetContainer": "Target (Container)",
|
||||
"scope": "Scope",
|
||||
"nameColumn": "Name",
|
||||
"namePlaceholder": "e.g. shared-db",
|
||||
"requiresName": "requires name",
|
||||
"noHostPath": "no host path",
|
||||
"tmpfs": "tmpfs (in-memory)",
|
||||
"actions": "Actions",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"add": "Add",
|
||||
"adding": "Adding...",
|
||||
"scopeGuide": "Volume Scopes",
|
||||
"noVolumes": "No volumes configured yet. Add one above.",
|
||||
"volumeAdded": "Volume added",
|
||||
"volumeUpdated": "Volume updated",
|
||||
"volumeDeleted": "Volume deleted",
|
||||
"loadFailed": "Failed to load volumes",
|
||||
"addFailed": "Failed to add volume",
|
||||
"updateFailed": "Failed to update volume",
|
||||
"deleteFailed": "Failed to delete volume"
|
||||
},
|
||||
"volumeBrowser": {
|
||||
"title": "Volume Browser",
|
||||
"loadFailed": "Failed to load directory",
|
||||
"empty": "This directory is empty.",
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"modified": "Modified",
|
||||
"downloadAll": "Download volume as ZIP",
|
||||
"downloadFolder": "Download folder as ZIP",
|
||||
"upload": "Upload files",
|
||||
"uploaded": "Uploaded",
|
||||
"files": "file(s)",
|
||||
"uploadFailed": "Failed to upload files",
|
||||
"browse": "Browse",
|
||||
"download": "Download"
|
||||
},
|
||||
"quickDeploy": {
|
||||
"title": "Quick Deploy",
|
||||
"description": "Deploy a container image with zero configuration. Paste an image URL, review the defaults, and deploy.",
|
||||
"step1": "1. Enter Image URL",
|
||||
"imageUrl": "Image URL",
|
||||
"imageUrlHelp": "Full image URL including tag (e.g., git.example.com/user/app:dev-abc123)",
|
||||
"inspect": "Inspect",
|
||||
"inspecting": "Inspecting...",
|
||||
"step2": "2. Review Configuration",
|
||||
"reviewDesc": "These defaults were detected from the image. Adjust as needed before deploying.",
|
||||
"projectName": "Project Name",
|
||||
"port": "Port",
|
||||
"portHelp": "Container port to expose (1-65535)",
|
||||
"healthCheckPath": "Health Check Path",
|
||||
"healthCheckHelp": "Optional HTTP path for health verification",
|
||||
"stage": "Stage",
|
||||
"development": "Development",
|
||||
"release": "Release",
|
||||
"production": "Production",
|
||||
"stageHelp": "Deployment stage for this image",
|
||||
"subdomainOverride": "Subdomain Override",
|
||||
"subdomainHelp": "Leave empty to use the default subdomain pattern",
|
||||
"envVars": "Environment Variables",
|
||||
"envVarsHelp": "One per line, KEY=VALUE format",
|
||||
"step3": "3. Deploy",
|
||||
"deployDesc": "A new project will be created and the container will be deployed immediately.",
|
||||
"deployBtn": "Deploy",
|
||||
"inspectedSuccess": "Image inspected successfully",
|
||||
"deployedSuccess": "Deployed {name} successfully!",
|
||||
"inspectFailed": "Failed to inspect image",
|
||||
"deployFailed": "Deployment failed",
|
||||
"browseImages": "Browse",
|
||||
"selectImage": "Select an image from a registry",
|
||||
"noImages": "No images found",
|
||||
"loadingImages": "Loading...",
|
||||
"imageLoadFailed": "Failed to load images",
|
||||
"autoDeployLabel": "Deploy immediately",
|
||||
"lowercaseHint": "Lowercase with hyphens",
|
||||
"imageAlreadyExists": "Image already deployed",
|
||||
"conflictDescription": "A project using this image already exists. You can open the existing project to deploy a new version, or create a separate project.",
|
||||
"openProject": "Open project \u2192",
|
||||
"createNewAnyway": "Create New Project"
|
||||
"local": "Local"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -489,7 +247,7 @@
|
||||
"testing": "Testing...",
|
||||
"testSuccess": "NPM connection successful",
|
||||
"testFailed": "NPM connection failed",
|
||||
"saveFailedConnection": "Cannot save \u2014 connection test failed",
|
||||
"saveFailedConnection": "Cannot save — connection test failed",
|
||||
"remoteMode": "Remote NPM",
|
||||
"remoteModeHelp": "Enable when NPM runs on a different machine than Docker. Forwards to Server IP with published host ports.",
|
||||
"remoteModeWarning": "Requires Server IP in General settings. Ports are auto-mapped to random host ports.",
|
||||
@@ -613,117 +371,28 @@
|
||||
"networkError": "Network error"
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Proxy Manager",
|
||||
"create": "Create Proxy",
|
||||
"standalone": "Standalone Proxies",
|
||||
"managed": "Managed Proxies",
|
||||
"noProxies": "No proxies found",
|
||||
"noProxiesDesc": "Create a standalone proxy or deploy a project with proxy enabled.",
|
||||
"filter": {
|
||||
"search": "Search by domain or destination...",
|
||||
"health": "Health",
|
||||
"type": "Type",
|
||||
"all": "All",
|
||||
"clear": "Clear filters"
|
||||
},
|
||||
"health": {
|
||||
"healthy": "Healthy",
|
||||
"unhealthy": "Unhealthy",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"lastChecked": "Last checked"
|
||||
},
|
||||
"sites": {
|
||||
"webhookTitle": "Site webhook",
|
||||
"webhookDesc": "Point your Git provider's push webhook at this URL. Tinyforge will re-sync the site on matching refs (branch for push trigger, tag pattern for tag trigger). Send an empty body for an unconditional sync.",
|
||||
"outgoingUrlTitle": "Outgoing webhook URL (this site)",
|
||||
"outgoingUrlDesc": "Where Tinyforge posts site_sync_success / site_sync_failure events for this site. Empty falls through to global settings.",
|
||||
"outgoingWebhookTitle": "Outgoing webhook (site)",
|
||||
"outgoingWebhookDesc": "HMAC signing secret and test sender for the resolved outgoing URL.",
|
||||
"outgoingFallbackGlobal": "the global integrations setting",
|
||||
"title": "Static Sites",
|
||||
"addSite": "New Site",
|
||||
"newSite": "New Static Site",
|
||||
"createSite": "Create Site",
|
||||
"noSites": "No static sites",
|
||||
"noSitesDesc": "Deploy static content from a Git repository folder.",
|
||||
"searchPlaceholder": "Search sites by name, domain, or repo...",
|
||||
"noMatching": "No sites match your search.",
|
||||
"name": "Name",
|
||||
"title": "Proxy Routes",
|
||||
"description": "Active proxy routes from deployed containers and static sites.",
|
||||
"domain": "Domain",
|
||||
"mode": "Mode",
|
||||
"project": "Project / Site",
|
||||
"stage": "Stage / Mode",
|
||||
"tag": "Tag",
|
||||
"port": "Port",
|
||||
"status": "Status",
|
||||
"lastSync": "Last Sync",
|
||||
"deploy": "Deploy",
|
||||
"stop": "Stop",
|
||||
"start": "Start",
|
||||
"openSite": "Open Site",
|
||||
"confirmDelete": "Delete Site",
|
||||
"confirmDeleteMsg": "This will permanently delete the site and remove its container",
|
||||
"confirmDeleteSecret": "Delete Secret",
|
||||
"confirmDeleteSecretMsg": "Are you sure you want to delete secret",
|
||||
"siteInfo": "Site Information",
|
||||
"folder": "Folder",
|
||||
"syncTrigger": "Sync Trigger",
|
||||
"commitSha": "Commit SHA",
|
||||
"secrets": "Secrets",
|
||||
"addSecret": "Add Secret",
|
||||
"noSecrets": "No secrets configured. Add secrets if your site needs server-side API keys.",
|
||||
"secretKey": "Key",
|
||||
"secretValue": "Value",
|
||||
"encryptSecret": "Encrypt value",
|
||||
"saveSecret": "Add Secret",
|
||||
"step1Title": "1. Repository",
|
||||
"step2Title": "2. Select Branch",
|
||||
"step3Title": "3. Select Folder",
|
||||
"step4Title": "4. Configuration",
|
||||
"step5Title": "5. Review & Create",
|
||||
"fullRepoUrl": "Repository URL",
|
||||
"fullRepoUrlHelp": "Paste a full URL to auto-fill the fields below (e.g., https://git.example.com/owner/repo)",
|
||||
"serverUrl": "Server URL",
|
||||
"repoUrl": "Git Server URL",
|
||||
"repoUrlHelp": "Paste a full repo URL or enter the server base URL (Gitea, Forgejo, Gogs)",
|
||||
"repoOwner": "Owner",
|
||||
"repoName": "Repository",
|
||||
"accessToken": "Access Token",
|
||||
"accessTokenPlaceholder": "Optional — for private repos",
|
||||
"accessTokenHelp": "Personal access token with repo read permissions. Leave empty for public repos.",
|
||||
"noToken": "None (public repo)",
|
||||
"testConnection": "Test Connection",
|
||||
"connectionSuccess": "Repository is accessible",
|
||||
"loadingBranches": "Loading branches...",
|
||||
"selectBranch": "Select a branch",
|
||||
"chooseBranch": "Choose a branch...",
|
||||
"branch": "Branch",
|
||||
"loadingTree": "Loading repository tree...",
|
||||
"selectFolder": "Select the folder containing your site files",
|
||||
"selectedFolder": "Selected folder",
|
||||
"siteName": "Site Name",
|
||||
"domainHelp": "Public domain for the site. Proxy will be configured automatically.",
|
||||
"modeStaticDesc": "HTML, CSS, JS, images served via Nginx",
|
||||
"modeDenoDesc": "Static files + server-side API from api/ folder",
|
||||
"triggerManual": "Manual",
|
||||
"triggerPush": "On Push",
|
||||
"triggerTag": "On Tag",
|
||||
"tagPattern": "Tag Pattern",
|
||||
"tagPatternHelp": "Glob pattern for matching tags (e.g., v*, pages-*)",
|
||||
"renderMarkdown": "Render Markdown files to HTML",
|
||||
"provider": "Git Provider",
|
||||
"detectedProvider": "Auto-detected",
|
||||
"browseRepos": "Browse repositories",
|
||||
"selectRepo": "Select a repository",
|
||||
"storage": "Persistent Storage",
|
||||
"enableStorage": "Enable persistent storage",
|
||||
"storageHelp": "Mounts a Docker volume at /app/data for your Deno backend to read and write files that persist across deployments.",
|
||||
"storageLimitMB": "Storage Limit (MB)",
|
||||
"storageLimitHelp": "Maximum storage size in megabytes. 0 = unlimited.",
|
||||
"storageVolume": "Volume",
|
||||
"dataPath": "Data Path",
|
||||
"storageMountPath": "Mount Path",
|
||||
"storageLimit": "Limit",
|
||||
"storageUsed": "Used",
|
||||
"storageOfLimit": "of limit used",
|
||||
"unlimited": "Unlimited"
|
||||
"source": "Source",
|
||||
"sourceContainer": "Container",
|
||||
"sourceStatic": "Static Site",
|
||||
"sourceDeno": "Deno Site",
|
||||
"filterAll": "All",
|
||||
"filterContainers": "Containers",
|
||||
"filterSites": "Sites",
|
||||
"noRoutes": "No proxy routes",
|
||||
"noRoutesDesc": "Proxy routes are created automatically when you deploy a container with proxy enabled or publish a static site.",
|
||||
"searchPlaceholder": "Search by domain, project, or tag...",
|
||||
"noMatch": "No routes match your search.",
|
||||
"loadFailed": "Failed to load proxy routes",
|
||||
"route": "route",
|
||||
"routes": "routes"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
@@ -775,24 +444,9 @@
|
||||
"lastSeen": "Last seen"
|
||||
}
|
||||
},
|
||||
"instance": {
|
||||
"stopConfirm": "This will stop the running container. The instance can be started again later.",
|
||||
"restartConfirm": "This will restart the container, causing brief downtime.",
|
||||
"removeConfirm": "This will permanently remove the container and its proxy configuration. This cannot be undone.",
|
||||
"actionFailed": "Action failed"
|
||||
},
|
||||
"empty": {
|
||||
"noProjects": "No projects yet",
|
||||
"noProjectsDesc": "Get started by creating your first project or use Quick Deploy.",
|
||||
"createProject": "Create Project",
|
||||
"noInstances": "No instances",
|
||||
"noInstancesDesc": "Deploy a new version to see instances here.",
|
||||
"noDeploys": "No deploy history",
|
||||
"noDeploysDesc": "Deploy history will appear here after your first deployment.",
|
||||
"noRegistries": "No registries",
|
||||
"noRegistriesDesc": "Add a container registry to enable image detection.",
|
||||
"noVolumes": "No volumes",
|
||||
"noVolumesDesc": "Configure volume mounts for persistent data.",
|
||||
"noUsers": "No users",
|
||||
"noUsersDesc": "Add local users to manage access."
|
||||
},
|
||||
@@ -808,15 +462,6 @@
|
||||
"requiredWhenUpdating": "{field} is required when updating credentials",
|
||||
"requiredForNew": "{field} is required for new registries"
|
||||
},
|
||||
"confirm": {
|
||||
"stopInstance": "Stop Instance",
|
||||
"startInstance": "Start Instance",
|
||||
"restartInstance": "Restart Instance",
|
||||
"removeInstance": "Remove Instance",
|
||||
"stopAction": "Stop",
|
||||
"restartAction": "Restart",
|
||||
"removeAction": "Remove"
|
||||
},
|
||||
"theme": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
@@ -842,75 +487,6 @@
|
||||
"cleanupFailed": "Cleanup failed",
|
||||
"loadFailed": "Failed to load stale containers"
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Proxies",
|
||||
"create": "Create Proxy",
|
||||
"noProxies": "No proxies configured yet.",
|
||||
"noProxiesDesc": "Create a standalone proxy or deploy a project to see proxies here.",
|
||||
"standalone": "Standalone Proxies",
|
||||
"managed": "Managed",
|
||||
"lastChecked": "Last checked",
|
||||
"health": {
|
||||
"healthy": "Healthy",
|
||||
"unhealthy": "Unhealthy",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"filter": {
|
||||
"search": "Search proxies...",
|
||||
"health": "Health",
|
||||
"type": "Type",
|
||||
"all": "All",
|
||||
"clear": "Clear filters"
|
||||
},
|
||||
"form": {
|
||||
"title": "Create Proxy",
|
||||
"editTitle": "Edit Proxy",
|
||||
"destination": "Destination URL / IP",
|
||||
"port": "Port",
|
||||
"domain": "Domain",
|
||||
"domainHelp": "The public domain for this proxy.",
|
||||
"validate": "Validate",
|
||||
"validating": "Validating...",
|
||||
"create": "Create Proxy",
|
||||
"save": "Save Changes",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Delete this proxy? This cannot be undone."
|
||||
},
|
||||
"validation": {
|
||||
"title": "Destination Validation",
|
||||
"syntax": "URL syntax",
|
||||
"dns": "DNS resolution",
|
||||
"tcp": "TCP connection",
|
||||
"http": "HTTP response",
|
||||
"checking": "Checking...",
|
||||
"skipped": "Skipped"
|
||||
}
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Proxy Routes",
|
||||
"description": "Active proxy routes from deployed containers and static sites.",
|
||||
"domain": "Domain",
|
||||
"project": "Project / Site",
|
||||
"stage": "Stage / Mode",
|
||||
"tag": "Tag",
|
||||
"port": "Port",
|
||||
"status": "Status",
|
||||
"source": "Source",
|
||||
"sourceContainer": "Container",
|
||||
"sourceStatic": "Static Site",
|
||||
"sourceDeno": "Deno Site",
|
||||
"filterAll": "All",
|
||||
"filterContainers": "Containers",
|
||||
"filterSites": "Sites",
|
||||
"noRoutes": "No proxy routes",
|
||||
"noRoutesDesc": "Proxy routes are created automatically when you deploy a container with proxy enabled or publish a static site.",
|
||||
"searchPlaceholder": "Search by domain, project, or tag...",
|
||||
"noMatch": "No routes match your search.",
|
||||
"loadFailed": "Failed to load proxy routes",
|
||||
"route": "route",
|
||||
"routes": "routes"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Container Logs",
|
||||
"lines": "lines",
|
||||
@@ -1049,128 +625,6 @@
|
||||
"en": "English",
|
||||
"ru": "Russian"
|
||||
},
|
||||
"stacks": {
|
||||
"eyebrow": "THE FORGE",
|
||||
"title": "Stacks",
|
||||
"lede": "Compose blueprints, forged as <em>atomic units</em>. Spin up services, iterate on revisions, roll back without breaking a sweat.",
|
||||
"newStack": "New stack",
|
||||
"refresh": "Refresh",
|
||||
"total": "Total",
|
||||
"running": "Running",
|
||||
"deploying": "Forging",
|
||||
"failed": "Failed",
|
||||
"stopped": "Cold",
|
||||
"empty": {
|
||||
"title": "The anvil is cold.",
|
||||
"desc": "Upload a docker-compose.yml to forge your first stack."
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "No description",
|
||||
"updated": "Updated",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"delete": "Delete",
|
||||
"open": "Open"
|
||||
},
|
||||
"new": {
|
||||
"eyebrow": "NEW BLUEPRINT",
|
||||
"title": "Forge a new stack.",
|
||||
"lede": "Upload or paste a <code>docker-compose.yml</code>. All services in the blueprint deploy as a single atomic unit.",
|
||||
"back": "Stacks",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "my-app-stack",
|
||||
"nameHint": "Lowercase, hyphenated. Used as the compose project name.",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "What does this stack do?",
|
||||
"composeYaml": "Compose YAML",
|
||||
"required": "required",
|
||||
"optional": "optional",
|
||||
"loadSample": "Load sample",
|
||||
"uploadFile": "Upload file",
|
||||
"dropHere": "Drop a docker-compose.yml here",
|
||||
"dropSub": "or click to browse · or use <strong>Load sample</strong> above",
|
||||
"lines": "{n} lines",
|
||||
"bytes": "{n} bytes",
|
||||
"clear": "Clear",
|
||||
"deployImmediate": "Deploy immediately",
|
||||
"deployHint": "Strike while the iron's hot. If unchecked, the stack is saved cold.",
|
||||
"cancel": "Cancel",
|
||||
"forging": "Forging…",
|
||||
"forgeAndDeploy": "Forge & deploy",
|
||||
"saveBlueprint": "Save blueprint",
|
||||
"errorRequired": "Name and compose YAML are required.",
|
||||
"errorCreate": "Failed to create stack"
|
||||
},
|
||||
"detail": {
|
||||
"manifest": "MANIFEST",
|
||||
"loading": "Loading blueprint…",
|
||||
"composeProject": "COMPOSE PROJECT",
|
||||
"noDescription": "No description",
|
||||
"refresh": "Refresh",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"delete": "Delete",
|
||||
"fault": "FAULT",
|
||||
"err": "ERR",
|
||||
"stats": {
|
||||
"services": "Services",
|
||||
"servicesSub": "in blueprint",
|
||||
"running": "Running",
|
||||
"runningSub": "active containers",
|
||||
"revisions": "Revisions",
|
||||
"revisionsSub": "in history",
|
||||
"current": "Current",
|
||||
"currentSub": "deployed"
|
||||
},
|
||||
"services": {
|
||||
"title": "Services",
|
||||
"count": "{n} on the floor",
|
||||
"empty": "— no containers running —"
|
||||
},
|
||||
"tabs": {
|
||||
"blueprint": "Blueprint",
|
||||
"revisions": "Revisions",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"yaml": {
|
||||
"currentRevision": "Current revision",
|
||||
"edit": "Edit & redeploy",
|
||||
"cancel": "Cancel",
|
||||
"forging": "Forging…",
|
||||
"deployNew": "Deploy new revision"
|
||||
},
|
||||
"revisions": {
|
||||
"current": "CURRENT",
|
||||
"by": "by",
|
||||
"rollback": "← Rollback to this revision",
|
||||
"rollbackTitle": "Rollback to revision?",
|
||||
"rollbackMessage": "Create a new revision from rev {n} and redeploy the stack.",
|
||||
"rollbackConfirm": "Rollback"
|
||||
},
|
||||
"logs": {
|
||||
"service": "Service:",
|
||||
"allServices": "All services",
|
||||
"fetching": "Fetching…",
|
||||
"fetch": "Fetch logs",
|
||||
"empty": "— no logs loaded. tap fetch. —"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete stack?",
|
||||
"messageBase": "This runs 'docker compose down' and removes \"{name}\".",
|
||||
"messageVolumes": " Named volumes will also be removed.",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"errors": {
|
||||
"load": "Failed to load stack",
|
||||
"stop": "Stop failed",
|
||||
"start": "Start failed",
|
||||
"update": "Update failed",
|
||||
"rollback": "Rollback failed",
|
||||
"delete": "Delete failed",
|
||||
"fetchLogs": "Failed to load logs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"eyebrow": "The Forge // Chronograph",
|
||||
"title": "Display timezone",
|
||||
|
||||
+29
-575
@@ -18,38 +18,27 @@
|
||||
"eventTriggers": "Триггеры",
|
||||
"logScanRules": "Лог-правила",
|
||||
"triggers": "Триггеры",
|
||||
"projects": "Проекты",
|
||||
"deploy": "Деплой",
|
||||
"proxies": "Прокси",
|
||||
"events": "События",
|
||||
"settings": "Настройки",
|
||||
"logout": "Выйти",
|
||||
"dns": "DNS-записи",
|
||||
"sites": "Сайты",
|
||||
"stacks": "Стеки",
|
||||
"containers": "Контейнеры"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Панель управления",
|
||||
"quickDeploy": "Быстрый деплой",
|
||||
"totalProjects": "Всего проектов",
|
||||
"runningInstances": "Запущенных экземпляров",
|
||||
"failedInstances": "Сбойных экземпляров",
|
||||
"projects": "Проекты",
|
||||
"newApp": "Новое приложение",
|
||||
"totalWorkloads": "Всего нагрузок",
|
||||
"runningContainers": "Запущенных контейнеров",
|
||||
"failedContainers": "Сбойных контейнеров",
|
||||
"recentWorkloads": "Недавние нагрузки",
|
||||
"retry": "Повторить",
|
||||
"noProjects": "Проектов пока нет.",
|
||||
"addFirst": "Добавьте первый проект",
|
||||
"noWorkloads": "Нагрузок пока нет.",
|
||||
"noWorkloadsDesc": "Создайте приложение и выкуйте первую нагрузку, чтобы начать.",
|
||||
"loadFailed": "Не удалось загрузить панель",
|
||||
"staleContainers": "Устаревшие контейнеры",
|
||||
"unusedImagesWarning": "Неиспользуемые Docker-образы занимают дисковое пространство",
|
||||
"unusedImages": "неиспользуемых образов",
|
||||
"staticSites": "Статические сайты",
|
||||
"totalSites": "Всего сайтов",
|
||||
"deployedSites": "развёрнуто",
|
||||
"failedSites": "с ошибкой",
|
||||
"noSites": "Статических сайтов пока нет.",
|
||||
"addFirstSite": "Разверните первый сайт",
|
||||
"viewAllSites": "Все сайты",
|
||||
"systemHealth": "Состояние системы",
|
||||
"daemons": "Демоны",
|
||||
"systemResources": "Системные ресурсы",
|
||||
@@ -92,240 +81,9 @@
|
||||
"retentionLabel": "Хранение статистики (часы)",
|
||||
"retentionHelp": "Как долго хранятся замеры ресурсов. 0 отключает сбор. Диапазон: 0–24ч."
|
||||
},
|
||||
"projects": {
|
||||
"title": "Проекты",
|
||||
"addProject": "Добавить проект",
|
||||
"cancel": "Отмена",
|
||||
"newProject": "Новый проект",
|
||||
"name": "Название",
|
||||
"image": "Образ",
|
||||
"port": "Порт",
|
||||
"tagPicker": {
|
||||
"registry": "Реестр",
|
||||
"created": "Создан",
|
||||
"view": "Открыть",
|
||||
"noProjects": "Проекты ещё не настроены.",
|
||||
"getStarted": "Нажмите «Добавить проект» для начала.",
|
||||
"createProject": "Создать проект",
|
||||
"creating": "Создание...",
|
||||
"healthcheck": "Путь проверки здоровья",
|
||||
"nameRequired": "Название и образ обязательны.",
|
||||
"loadFailed": "Не удалось загрузить проекты",
|
||||
"createFailed": "Не удалось создать проект",
|
||||
"browseImages": "Обзор образов",
|
||||
"selectImage": "Выберите образ",
|
||||
"noImages": "Образы не найдены",
|
||||
"loadingImages": "Загрузка образов...",
|
||||
"imageLoadFailed": "Не удалось загрузить образы",
|
||||
"alreadyAdded": "Уже добавлен",
|
||||
"portHelpText": "Автоопределение из EXPOSE, если пусто",
|
||||
"healthcheckHelpText": "Автоопределение из образа, если пусто",
|
||||
"searchPlaceholder": "Поиск по имени, образу или реестру...",
|
||||
"noMatchingProjects": "Проекты не найдены."
|
||||
},
|
||||
"projectDetail": {
|
||||
"webhookTitle": "Webhook проекта",
|
||||
"webhookDesc": "Отправьте POST с image-ссылкой на этот URL из CI — и Tinyforge запустит деплой. Стейдж выбирается по tag_pattern.",
|
||||
"outgoingWebhookTitle": "Исходящий webhook (проект)",
|
||||
"outgoingWebhookDesc": "Куда Tinyforge отправляет события деплоя для этого проекта. Стейджи могут переопределить; если нигде не задано — используется глобальная настройка.",
|
||||
"outgoingFallbackGlobal": "глобальной настройки интеграций",
|
||||
"notificationUrlLabel": "URL исходящего webhook",
|
||||
"notificationUrlHelp": "Оставьте пустым для наследования из глобальных настроек. Стейджи могут переопределить.",
|
||||
"stageNotificationUrlLabel": "URL исходящего webhook (этот стейдж)",
|
||||
"stageNotificationUrlHelp": "Оставьте пустым для наследования от проекта, затем — из глобальных настроек.",
|
||||
"stageOutgoingTitle": "Исходящий webhook (стейдж)",
|
||||
"stageOutgoingDesc": "Куда Tinyforge отправляет события деплоя этого стейджа. Побеждает самый конкретный уровень.",
|
||||
"stageFallbackLabel": "проектной или глобальной настройки",
|
||||
"deleteProject": "Удалить проект",
|
||||
"envVars": "Переменные окружения",
|
||||
"volumes": "Тома",
|
||||
"stages": "Стадии",
|
||||
"noStages": "Для этого проекта не настроены стадии.",
|
||||
"pattern": "Шаблон",
|
||||
"autoDeploy": "авто-деплой",
|
||||
"requiresConfirm": "нужно подтверждение",
|
||||
"instances": "экземпляров",
|
||||
"deployNewVersion": "Развернуть новую версию",
|
||||
"selectTag": "Выберите тег для деплоя",
|
||||
"loadingTags": "Загрузка тегов...",
|
||||
"chooseTag": "Выберите тег...",
|
||||
"enterTag": "Введите тег образа (напр., dev-abc123)",
|
||||
"registryTag": "Реестр",
|
||||
"localTag": "Локальный",
|
||||
"alsoLocal": "Также доступен локально",
|
||||
"searchTags": "Поиск тегов...",
|
||||
"deployTag": "Тег",
|
||||
"deploy": "Развернуть",
|
||||
"deploying": "Развёртывание...",
|
||||
"recentDeploys": "Последние деплои",
|
||||
"noDeployHistory": "Нет истории деплоев для этого проекта.",
|
||||
"tag": "Тег",
|
||||
"status": "Статус",
|
||||
"started": "Начат",
|
||||
"finished": "Завершён",
|
||||
"error": "Ошибка",
|
||||
"noInstancesRunning": "Нет запущенных экземпляров",
|
||||
"deleteConfirmTitle": "Удалить проект",
|
||||
"deleteConfirmMessage": "Это безвозвратно удалит проект '{name}' и все его стадии, экземпляры и историю деплоев.",
|
||||
"loadFailed": "Не удалось загрузить проект",
|
||||
"deleteFailed": "Не удалось удалить проект",
|
||||
"deployFailed": "Деплой не удался",
|
||||
"nameLabel": "Название *",
|
||||
"imageLabel": "Образ *",
|
||||
"portLabel": "Порт",
|
||||
"healthcheckLabel": "Путь проверки",
|
||||
"saving": "Сохранение...",
|
||||
"addStage": "Добавить стадию",
|
||||
"tagPattern": "Шаблон тега",
|
||||
"tagPatternHelp": "Glob-шаблон (напр., dev-*, v*)",
|
||||
"maxInstances": "Макс. экземпляров",
|
||||
"autoDeployLabel": "Авто-деплой",
|
||||
"enableProxy": "Включить прокси",
|
||||
"accessListId": "ID списка доступа NPM",
|
||||
"accessListIdHelp": "Переопределить глобальный список доступа для этого проекта. Очистите, чтобы наследовать из настроек NPM.",
|
||||
"localImages": "Локальные Docker-образы",
|
||||
"imageTag": "Тег",
|
||||
"imageId": "ID образа",
|
||||
"imageSize": "Размер",
|
||||
"imageCreated": "Создан",
|
||||
"cpuLimit": "Лимит CPU (ядра)",
|
||||
"cpuLimitHelp": "напр., 0.5, 1, 2. Оставьте 0 для без ограничений",
|
||||
"memoryLimit": "Лимит памяти (МБ)",
|
||||
"memoryLimitHelp": "напр., 256, 512, 1024. Оставьте 0 для без ограничений",
|
||||
"npmProxy": "NPM прокси",
|
||||
"creating": "Создание...",
|
||||
"createStage": "Создать стадию",
|
||||
"noProxy": "Без прокси",
|
||||
"deleteStage": "Удалить стадию",
|
||||
"deleteStageConfirm": "Удалить стадию \"{name}\"?",
|
||||
"stageCreated": "Стадия \"{name}\" создана",
|
||||
"stageUpdated": "Стадия обновлена",
|
||||
"stageUpdateFailed": "Не удалось обновить стадию",
|
||||
"stageDeleted": "Стадия \"{name}\" удалена",
|
||||
"projectUpdated": "Проект обновлён",
|
||||
"updateFailed": "Не удалось обновить проект",
|
||||
"stageCreateFailed": "Не удалось создать стадию",
|
||||
"stageDeleteFailed": "Не удалось удалить стадию"
|
||||
},
|
||||
"envEditor": {
|
||||
"title": "Переменные окружения",
|
||||
"description": "Управление переопределениями переменных окружения на уровне стадий. Значения стадий переопределяют значения проекта.",
|
||||
"stage": "Стадия",
|
||||
"projectDefaults": "Значения проекта по умолчанию",
|
||||
"noProjectEnv": "Переменные окружения на уровне проекта ещё не определены.",
|
||||
"stageOverrides": "Переопределения стадии",
|
||||
"key": "Ключ",
|
||||
"value": "Значение",
|
||||
"secret": "Секрет",
|
||||
"source": "Источник",
|
||||
"actions": "Действия",
|
||||
"overridden": "переопределено",
|
||||
"inherited": "наследуется",
|
||||
"overridesProject": "переопределяет проект",
|
||||
"stageOnly": "только стадия",
|
||||
"edit": "Изменить",
|
||||
"change": "Изменить",
|
||||
"delete": "Удалить",
|
||||
"save": "Сохранить",
|
||||
"add": "Добавить",
|
||||
"adding": "Добавление...",
|
||||
"noStages": "Стадии не настроены. Сначала добавьте стадии к проекту.",
|
||||
"loadFailed": "Не удалось загрузить проект",
|
||||
"envAdded": "Переменная окружения добавлена",
|
||||
"envUpdated": "Переменная окружения обновлена",
|
||||
"envDeleted": "Переменная окружения удалена",
|
||||
"addFailed": "Не удалось добавить переменную",
|
||||
"updateFailed": "Не удалось обновить переменную",
|
||||
"deleteFailed": "Не удалось удалить переменную",
|
||||
"loadEnvFailed": "Не удалось загрузить переменные",
|
||||
"leaveEmptyToKeep": "Оставьте пустым, чтобы сохранить текущее",
|
||||
"deleteTitle": "Удалить переменную окружения",
|
||||
"deleteMessage": "Вы уверены, что хотите удалить эту переменную окружения? Это действие нельзя отменить."
|
||||
},
|
||||
"volumeEditor": {
|
||||
"title": "Тома",
|
||||
"description": "Настройка монтирования томов для контейнеров. Выберите область видимости для управления общим доступом между развёртываниями.",
|
||||
"sourceHost": "Источник (хост)",
|
||||
"targetContainer": "Цель (контейнер)",
|
||||
"scope": "Область",
|
||||
"nameColumn": "Имя",
|
||||
"namePlaceholder": "напр. shared-db",
|
||||
"requiresName": "требуется имя",
|
||||
"noHostPath": "нет пути на хосте",
|
||||
"tmpfs": "tmpfs (в памяти)",
|
||||
"actions": "Действия",
|
||||
"edit": "Изменить",
|
||||
"delete": "Удалить",
|
||||
"save": "Сохранить",
|
||||
"add": "Добавить",
|
||||
"adding": "Добавление...",
|
||||
"scopeGuide": "Области видимости томов",
|
||||
"noVolumes": "Тома ещё не настроены. Добавьте один выше.",
|
||||
"volumeAdded": "Том добавлен",
|
||||
"volumeUpdated": "Том обновлён",
|
||||
"volumeDeleted": "Том удалён",
|
||||
"loadFailed": "Не удалось загрузить тома",
|
||||
"addFailed": "Не удалось добавить том",
|
||||
"updateFailed": "Не удалось обновить том",
|
||||
"deleteFailed": "Не удалось удалить том"
|
||||
},
|
||||
"volumeBrowser": {
|
||||
"title": "Обзор тома",
|
||||
"loadFailed": "Не удалось загрузить каталог",
|
||||
"empty": "Этот каталог пуст.",
|
||||
"name": "Имя",
|
||||
"size": "Размер",
|
||||
"modified": "Изменён",
|
||||
"downloadAll": "Скачать том как ZIP",
|
||||
"downloadFolder": "Скачать папку как ZIP",
|
||||
"upload": "Загрузить файлы",
|
||||
"uploaded": "Загружено",
|
||||
"files": "файл(ов)",
|
||||
"uploadFailed": "Не удалось загрузить файлы",
|
||||
"browse": "Обзор",
|
||||
"download": "Скачать"
|
||||
},
|
||||
"quickDeploy": {
|
||||
"title": "Быстрый деплой",
|
||||
"description": "Разверните образ контейнера без настройки. Вставьте URL образа, проверьте параметры и разверните.",
|
||||
"step1": "1. Введите URL образа",
|
||||
"imageUrl": "URL образа",
|
||||
"imageUrlHelp": "Полный URL образа с тегом (напр., git.example.com/user/app:dev-abc123)",
|
||||
"inspect": "Проверить",
|
||||
"inspecting": "Проверка...",
|
||||
"step2": "2. Проверка конфигурации",
|
||||
"reviewDesc": "Эти параметры были обнаружены из образа. Измените при необходимости перед деплоем.",
|
||||
"projectName": "Имя проекта",
|
||||
"port": "Порт",
|
||||
"portHelp": "Порт контейнера (1-65535)",
|
||||
"healthCheckPath": "Путь проверки здоровья",
|
||||
"healthCheckHelp": "Необязательный HTTP-путь для проверки работоспособности",
|
||||
"stage": "Стадия",
|
||||
"development": "Разработка",
|
||||
"release": "Релиз",
|
||||
"production": "Продакшн",
|
||||
"stageHelp": "Стадия развёртывания для этого образа",
|
||||
"subdomainOverride": "Переопределение поддомена",
|
||||
"subdomainHelp": "Оставьте пустым для использования шаблона по умолчанию",
|
||||
"envVars": "Переменные окружения",
|
||||
"envVarsHelp": "По одной на строку, формат KEY=VALUE",
|
||||
"step3": "3. Развёртывание",
|
||||
"deployDesc": "Будет создан новый проект и контейнер будет развёрнут немедленно.",
|
||||
"deployBtn": "Развернуть",
|
||||
"inspectedSuccess": "Образ успешно проверен",
|
||||
"deployedSuccess": "{name} успешно развёрнут!",
|
||||
"inspectFailed": "Не удалось проверить образ",
|
||||
"deployFailed": "Развёртывание не удалось",
|
||||
"browseImages": "Обзор",
|
||||
"selectImage": "Выберите образ из реестра",
|
||||
"noImages": "Образы не найдены",
|
||||
"loadingImages": "Загрузка...",
|
||||
"imageLoadFailed": "Не удалось загрузить образы",
|
||||
"autoDeployLabel": "Развернуть сразу",
|
||||
"lowercaseHint": "Строчные буквы и дефисы",
|
||||
"imageAlreadyExists": "Образ уже развёрнут",
|
||||
"conflictDescription": "Проект с этим образом уже существует. Откройте существующий проект для развёртывания новой версии или создайте отдельный проект.",
|
||||
"openProject": "Открыть проект \u2192",
|
||||
"createNewAnyway": "Создать новый проект"
|
||||
"local": "Локальный"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
@@ -613,117 +371,28 @@
|
||||
"networkError": "Ошибка сети"
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Менеджер прокси",
|
||||
"create": "Создать прокси",
|
||||
"standalone": "Автономные прокси",
|
||||
"managed": "Управляемые прокси",
|
||||
"noProxies": "Прокси не найдены",
|
||||
"noProxiesDesc": "Создайте автономный прокси или разверните проект с включённым прокси.",
|
||||
"filter": {
|
||||
"search": "Поиск по домену или назначению...",
|
||||
"health": "Здоровье",
|
||||
"type": "Тип",
|
||||
"all": "Все",
|
||||
"clear": "Сбросить фильтры"
|
||||
},
|
||||
"health": {
|
||||
"healthy": "Здоров",
|
||||
"unhealthy": "Нездоров",
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"lastChecked": "Последняя проверка"
|
||||
},
|
||||
"sites": {
|
||||
"webhookTitle": "Webhook сайта",
|
||||
"webhookDesc": "Укажите этот URL в push-вебхуке Git-провайдера. Tinyforge пересинхронизирует сайт при подходящей ref-ссылке (ветка для push, шаблон тега для tag). Пустое тело запускает синхронизацию безусловно.",
|
||||
"outgoingUrlTitle": "URL исходящего webhook (этот сайт)",
|
||||
"outgoingUrlDesc": "Куда Tinyforge отправляет события site_sync_success / site_sync_failure. Пусто — наследовать из глобальных настроек.",
|
||||
"outgoingWebhookTitle": "Исходящий webhook (сайт)",
|
||||
"outgoingWebhookDesc": "HMAC-секрет и тестовая отправка для разрешённого исходящего URL.",
|
||||
"outgoingFallbackGlobal": "глобальной настройки интеграций",
|
||||
"title": "Статические сайты",
|
||||
"addSite": "Новый сайт",
|
||||
"newSite": "Новый статический сайт",
|
||||
"createSite": "Создать сайт",
|
||||
"noSites": "Нет статических сайтов",
|
||||
"noSitesDesc": "Разверните статический контент из папки Git-репозитория.",
|
||||
"searchPlaceholder": "Поиск по имени, домену или репозиторию...",
|
||||
"noMatching": "Нет сайтов, соответствующих поиску.",
|
||||
"name": "Имя",
|
||||
"title": "Прокси-маршруты",
|
||||
"description": "Активные прокси-маршруты от контейнеров и статических сайтов.",
|
||||
"domain": "Домен",
|
||||
"mode": "Режим",
|
||||
"project": "Проект / Сайт",
|
||||
"stage": "Этап / Режим",
|
||||
"tag": "Тег",
|
||||
"port": "Порт",
|
||||
"status": "Статус",
|
||||
"lastSync": "Последняя синхр.",
|
||||
"deploy": "Развернуть",
|
||||
"stop": "Остановить",
|
||||
"start": "Запустить",
|
||||
"openSite": "Открыть сайт",
|
||||
"confirmDelete": "Удалить сайт",
|
||||
"confirmDeleteMsg": "Это удалит сайт и его контейнер",
|
||||
"confirmDeleteSecret": "Удалить секрет",
|
||||
"confirmDeleteSecretMsg": "Вы уверены, что хотите удалить секрет",
|
||||
"siteInfo": "Информация о сайте",
|
||||
"folder": "Папка",
|
||||
"syncTrigger": "Триггер синхр.",
|
||||
"commitSha": "Коммит SHA",
|
||||
"secrets": "Секреты",
|
||||
"addSecret": "Добавить секрет",
|
||||
"noSecrets": "Секреты не настроены. Добавьте их, если сайту нужны серверные API-ключи.",
|
||||
"secretKey": "Ключ",
|
||||
"secretValue": "Значение",
|
||||
"encryptSecret": "Шифровать значение",
|
||||
"saveSecret": "Добавить секрет",
|
||||
"step1Title": "1. Репозиторий",
|
||||
"step2Title": "2. Выбор ветки",
|
||||
"step3Title": "3. Выбор папки",
|
||||
"step4Title": "4. Настройки",
|
||||
"step5Title": "5. Проверка и создание",
|
||||
"fullRepoUrl": "URL репозитория",
|
||||
"fullRepoUrlHelp": "Вставьте полный URL для автозаполнения полей ниже (напр., https://git.example.com/owner/repo)",
|
||||
"serverUrl": "URL сервера",
|
||||
"repoUrl": "URL Git-сервера",
|
||||
"repoUrlHelp": "Вставьте полный URL репозитория или базовый URL сервера (Gitea, Forgejo, Gogs)",
|
||||
"repoOwner": "Владелец",
|
||||
"repoName": "Репозиторий",
|
||||
"accessToken": "Токен доступа",
|
||||
"accessTokenPlaceholder": "Необязательно — для приватных репозиториев",
|
||||
"accessTokenHelp": "Персональный токен с правами на чтение репозитория. Оставьте пустым для публичных.",
|
||||
"noToken": "Нет (публичный репо)",
|
||||
"testConnection": "Проверить соединение",
|
||||
"connectionSuccess": "Репозиторий доступен",
|
||||
"loadingBranches": "Загрузка веток...",
|
||||
"selectBranch": "Выберите ветку",
|
||||
"chooseBranch": "Выберите ветку...",
|
||||
"branch": "Ветка",
|
||||
"loadingTree": "Загрузка дерева репозитория...",
|
||||
"selectFolder": "Выберите папку с файлами сайта",
|
||||
"selectedFolder": "Выбранная папка",
|
||||
"siteName": "Имя сайта",
|
||||
"domainHelp": "Публичный домен сайта. Прокси будет настроен автоматически.",
|
||||
"modeStaticDesc": "HTML, CSS, JS, изображения через Nginx",
|
||||
"modeDenoDesc": "Статические файлы + серверный API из папки api/",
|
||||
"triggerManual": "Вручную",
|
||||
"triggerPush": "При пуше",
|
||||
"triggerTag": "По тегу",
|
||||
"tagPattern": "Паттерн тега",
|
||||
"tagPatternHelp": "Glob-паттерн для тегов (напр., v*, pages-*)",
|
||||
"renderMarkdown": "Рендерить Markdown-файлы в HTML",
|
||||
"provider": "Git-провайдер",
|
||||
"detectedProvider": "Автоопределён",
|
||||
"browseRepos": "Обзор репозиториев",
|
||||
"selectRepo": "Выберите репозиторий",
|
||||
"storage": "Хранилище данных",
|
||||
"enableStorage": "Включить хранилище данных",
|
||||
"storageHelp": "Подключает Docker-том в /app/data, чтобы Deno-бэкенд мог читать и записывать файлы, сохраняющиеся между деплоями.",
|
||||
"storageLimitMB": "Лимит хранилища (МБ)",
|
||||
"storageLimitHelp": "Максимальный размер хранилища в мегабайтах. 0 = без ограничений.",
|
||||
"storageVolume": "Том",
|
||||
"dataPath": "Путь к данным",
|
||||
"storageMountPath": "Путь монтирования",
|
||||
"storageLimit": "Лимит",
|
||||
"storageUsed": "Использовано",
|
||||
"storageOfLimit": "от лимита использовано",
|
||||
"unlimited": "Без ограничений"
|
||||
"source": "Источник",
|
||||
"sourceContainer": "Контейнер",
|
||||
"sourceStatic": "Статический сайт",
|
||||
"sourceDeno": "Deno-сайт",
|
||||
"filterAll": "Все",
|
||||
"filterContainers": "Контейнеры",
|
||||
"filterSites": "Сайты",
|
||||
"noRoutes": "Нет прокси-маршрутов",
|
||||
"noRoutesDesc": "Прокси-маршруты создаются автоматически при развёртывании контейнера с прокси или публикации статического сайта.",
|
||||
"searchPlaceholder": "Поиск по домену, проекту или тегу...",
|
||||
"noMatch": "Нет маршрутов, соответствующих поиску.",
|
||||
"loadFailed": "Не удалось загрузить прокси-маршруты",
|
||||
"route": "маршрут",
|
||||
"routes": "маршрутов"
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Отмена",
|
||||
@@ -775,24 +444,9 @@
|
||||
"lastSeen": "Замечен"
|
||||
}
|
||||
},
|
||||
"instance": {
|
||||
"stopConfirm": "Контейнер будет остановлен. Экземпляр можно будет запустить снова позже.",
|
||||
"restartConfirm": "Контейнер будет перезапущен с кратковременным простоем.",
|
||||
"removeConfirm": "Контейнер и его прокси-конфигурация будут безвозвратно удалены.",
|
||||
"actionFailed": "Действие не удалось"
|
||||
},
|
||||
"empty": {
|
||||
"noProjects": "Проектов пока нет",
|
||||
"noProjectsDesc": "Начните с создания первого проекта или используйте быстрый деплой.",
|
||||
"createProject": "Создать проект",
|
||||
"noInstances": "Нет экземпляров",
|
||||
"noInstancesDesc": "Разверните новую версию, чтобы увидеть экземпляры здесь.",
|
||||
"noDeploys": "Нет истории деплоев",
|
||||
"noDeploysDesc": "История деплоев появится здесь после первого развёртывания.",
|
||||
"noRegistries": "Нет реестров",
|
||||
"noRegistriesDesc": "Добавьте реестр контейнеров для обнаружения образов.",
|
||||
"noVolumes": "Нет томов",
|
||||
"noVolumesDesc": "Настройте монтирование томов для постоянных данных.",
|
||||
"noUsers": "Нет пользователей",
|
||||
"noUsersDesc": "Добавьте локальных пользователей для управления доступом."
|
||||
},
|
||||
@@ -808,15 +462,6 @@
|
||||
"requiredWhenUpdating": "Поле {field} обязательно при обновлении учётных данных",
|
||||
"requiredForNew": "Поле {field} обязательно для новых реестров"
|
||||
},
|
||||
"confirm": {
|
||||
"stopInstance": "Остановить экземпляр",
|
||||
"startInstance": "Запустить экземпляр",
|
||||
"restartInstance": "Перезапустить экземпляр",
|
||||
"removeInstance": "Удалить экземпляр",
|
||||
"stopAction": "Остановить",
|
||||
"restartAction": "Перезапустить",
|
||||
"removeAction": "Удалить"
|
||||
},
|
||||
"theme": {
|
||||
"light": "Светлая",
|
||||
"dark": "Тёмная",
|
||||
@@ -842,75 +487,6 @@
|
||||
"cleanupFailed": "Не удалось очистить",
|
||||
"loadFailed": "Не удалось загрузить устаревшие контейнеры"
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Прокси",
|
||||
"create": "Создать прокси",
|
||||
"noProxies": "Прокси ещё не настроены.",
|
||||
"noProxiesDesc": "Создайте автономный прокси или разверните проект, чтобы увидеть прокси здесь.",
|
||||
"standalone": "Автономные прокси",
|
||||
"managed": "Управляемые",
|
||||
"lastChecked": "Последняя проверка",
|
||||
"health": {
|
||||
"healthy": "Работает",
|
||||
"unhealthy": "Недоступен",
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"filter": {
|
||||
"search": "Поиск прокси...",
|
||||
"health": "Здоровье",
|
||||
"type": "Тип",
|
||||
"all": "Все",
|
||||
"clear": "Сбросить фильтры"
|
||||
},
|
||||
"form": {
|
||||
"title": "Создать прокси",
|
||||
"editTitle": "Редактировать прокси",
|
||||
"destination": "URL / IP назначения",
|
||||
"port": "Порт",
|
||||
"domain": "Домен",
|
||||
"domainHelp": "Публичный домен для этого прокси.",
|
||||
"validate": "Проверить",
|
||||
"validating": "Проверка...",
|
||||
"create": "Создать прокси",
|
||||
"save": "Сохранить изменения",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"deleteConfirm": "Удалить этот прокси? Это действие необратимо."
|
||||
},
|
||||
"validation": {
|
||||
"title": "Проверка назначения",
|
||||
"syntax": "Синтаксис URL",
|
||||
"dns": "DNS разрешение",
|
||||
"tcp": "TCP подключение",
|
||||
"http": "HTTP ответ",
|
||||
"checking": "Проверка...",
|
||||
"skipped": "Пропущено"
|
||||
}
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Прокси-маршруты",
|
||||
"description": "Активные прокси-маршруты от контейнеров и статических сайтов.",
|
||||
"domain": "Домен",
|
||||
"project": "Проект / Сайт",
|
||||
"stage": "Этап / Режим",
|
||||
"tag": "Тег",
|
||||
"port": "Порт",
|
||||
"status": "Статус",
|
||||
"source": "Источник",
|
||||
"sourceContainer": "Контейнер",
|
||||
"sourceStatic": "Статический сайт",
|
||||
"sourceDeno": "Deno-сайт",
|
||||
"filterAll": "Все",
|
||||
"filterContainers": "Контейнеры",
|
||||
"filterSites": "Сайты",
|
||||
"noRoutes": "Нет прокси-маршрутов",
|
||||
"noRoutesDesc": "Прокси-маршруты создаются автоматически при развёртывании контейнера с прокси или публикации статического сайта.",
|
||||
"searchPlaceholder": "Поиск по домену, проекту или тегу...",
|
||||
"noMatch": "Нет маршрутов, соответствующих поиску.",
|
||||
"loadFailed": "Не удалось загрузить прокси-маршруты",
|
||||
"route": "маршрут",
|
||||
"routes": "маршрутов"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Логи контейнера",
|
||||
"lines": "строк",
|
||||
@@ -1049,128 +625,6 @@
|
||||
"en": "Английский",
|
||||
"ru": "Русский"
|
||||
},
|
||||
"stacks": {
|
||||
"eyebrow": "КУЗНИЦА",
|
||||
"title": "Стеки",
|
||||
"lede": "Compose-чертежи, выкованные как <em>атомарные единицы</em>. Запускайте сервисы, меняйте ревизии и откатывайтесь без нервов.",
|
||||
"newStack": "Новый стек",
|
||||
"refresh": "Обновить",
|
||||
"total": "Всего",
|
||||
"running": "Работают",
|
||||
"deploying": "Куются",
|
||||
"failed": "Сбой",
|
||||
"stopped": "Холодные",
|
||||
"empty": {
|
||||
"title": "Наковальня остыла.",
|
||||
"desc": "Загрузите docker-compose.yml, чтобы выковать первый стек."
|
||||
},
|
||||
"card": {
|
||||
"noDescription": "Без описания",
|
||||
"updated": "Обновлён",
|
||||
"start": "Запустить",
|
||||
"stop": "Остановить",
|
||||
"delete": "Удалить",
|
||||
"open": "Открыть"
|
||||
},
|
||||
"new": {
|
||||
"eyebrow": "НОВЫЙ ЧЕРТЁЖ",
|
||||
"title": "Выковать новый стек.",
|
||||
"lede": "Загрузите или вставьте <code>docker-compose.yml</code>. Все сервисы чертежа разворачиваются как одна атомарная единица.",
|
||||
"back": "Стеки",
|
||||
"name": "Имя",
|
||||
"namePlaceholder": "мой-стек",
|
||||
"nameHint": "Строчные буквы, через дефис. Используется как имя compose-проекта.",
|
||||
"description": "Описание",
|
||||
"descriptionPlaceholder": "Что делает этот стек?",
|
||||
"composeYaml": "Compose YAML",
|
||||
"required": "обязательно",
|
||||
"optional": "необязательно",
|
||||
"loadSample": "Загрузить пример",
|
||||
"uploadFile": "Загрузить файл",
|
||||
"dropHere": "Перетащите сюда docker-compose.yml",
|
||||
"dropSub": "или нажмите для выбора · или используйте <strong>Загрузить пример</strong> выше",
|
||||
"lines": "{n} строк",
|
||||
"bytes": "{n} байт",
|
||||
"clear": "Очистить",
|
||||
"deployImmediate": "Развернуть сразу",
|
||||
"deployHint": "Куй железо, пока горячо. Без галочки стек сохраняется холодным.",
|
||||
"cancel": "Отмена",
|
||||
"forging": "Куём…",
|
||||
"forgeAndDeploy": "Выковать и развернуть",
|
||||
"saveBlueprint": "Сохранить чертёж",
|
||||
"errorRequired": "Имя и compose YAML обязательны.",
|
||||
"errorCreate": "Не удалось создать стек"
|
||||
},
|
||||
"detail": {
|
||||
"manifest": "МАНИФЕСТ",
|
||||
"loading": "Загрузка чертежа…",
|
||||
"composeProject": "COMPOSE-ПРОЕКТ",
|
||||
"noDescription": "Без описания",
|
||||
"refresh": "Обновить",
|
||||
"start": "Запустить",
|
||||
"stop": "Остановить",
|
||||
"delete": "Удалить",
|
||||
"fault": "СБОЙ",
|
||||
"err": "ОШБ",
|
||||
"stats": {
|
||||
"services": "Сервисы",
|
||||
"servicesSub": "в чертеже",
|
||||
"running": "Работают",
|
||||
"runningSub": "активных контейнеров",
|
||||
"revisions": "Ревизии",
|
||||
"revisionsSub": "в истории",
|
||||
"current": "Текущая",
|
||||
"currentSub": "развёрнута"
|
||||
},
|
||||
"services": {
|
||||
"title": "Сервисы",
|
||||
"count": "{n} в работе",
|
||||
"empty": "— нет запущенных контейнеров —"
|
||||
},
|
||||
"tabs": {
|
||||
"blueprint": "Чертёж",
|
||||
"revisions": "Ревизии",
|
||||
"logs": "Логи"
|
||||
},
|
||||
"yaml": {
|
||||
"currentRevision": "Текущая ревизия",
|
||||
"edit": "Править и развернуть",
|
||||
"cancel": "Отмена",
|
||||
"forging": "Куём…",
|
||||
"deployNew": "Развернуть новую ревизию"
|
||||
},
|
||||
"revisions": {
|
||||
"current": "ТЕКУЩАЯ",
|
||||
"by": "автор",
|
||||
"rollback": "← Откатиться к этой ревизии",
|
||||
"rollbackTitle": "Откатить ревизию?",
|
||||
"rollbackMessage": "Создать новую ревизию из rev {n} и развернуть стек заново.",
|
||||
"rollbackConfirm": "Откатить"
|
||||
},
|
||||
"logs": {
|
||||
"service": "Сервис:",
|
||||
"allServices": "Все сервисы",
|
||||
"fetching": "Загрузка…",
|
||||
"fetch": "Получить логи",
|
||||
"empty": "— логи не загружены. нажмите получить. —"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Удалить стек?",
|
||||
"messageBase": "Будет выполнен 'docker compose down' и удалён \"{name}\".",
|
||||
"messageVolumes": " Именованные тома также будут удалены.",
|
||||
"confirm": "Удалить"
|
||||
},
|
||||
"errors": {
|
||||
"load": "Не удалось загрузить стек",
|
||||
"stop": "Остановка не удалась",
|
||||
"start": "Запуск не удался",
|
||||
"update": "Обновление не удалось",
|
||||
"rollback": "Откат не удался",
|
||||
"delete": "Удаление не удалось",
|
||||
"fetchLogs": "Не удалось загрузить логи"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"eyebrow": "The Forge // Хронограф",
|
||||
"title": "Часовой пояс отображения",
|
||||
|
||||
+3
-26
@@ -156,32 +156,9 @@ export function connectSSE(url: string, options: SSEOptions): SSEConnection {
|
||||
}
|
||||
|
||||
// ── Convenience Factories ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Connect to deploy log SSE stream for a specific deploy.
|
||||
* Streams existing logs first, then real-time updates.
|
||||
*/
|
||||
export function connectDeployLogs(
|
||||
deployId: string,
|
||||
callbacks: {
|
||||
onLog: (log: DeployLogPayload) => void;
|
||||
onStatus?: (status: DeployStatusPayload) => void;
|
||||
onOpen?: () => void;
|
||||
onError?: (attempt: number) => void;
|
||||
}
|
||||
): SSEConnection {
|
||||
return connectSSE(`/api/deploys/${deployId}/logs`, {
|
||||
onEvent(event) {
|
||||
if (event.type === 'deploy_log') {
|
||||
callbacks.onLog(event.payload as DeployLogPayload);
|
||||
} else if (event.type === 'deploy_status') {
|
||||
callbacks.onStatus?.(event.payload as DeployStatusPayload);
|
||||
}
|
||||
},
|
||||
onOpen: callbacks.onOpen,
|
||||
onError: callbacks.onError
|
||||
});
|
||||
}
|
||||
// connectDeployLogs (legacy /api/deploys/{id}/logs SSE) was retired
|
||||
// with the hard cutover; deploy progress now surfaces through the
|
||||
// global /api/events stream below.
|
||||
|
||||
/**
|
||||
* Connect to the global events SSE stream.
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
* so the UI can dim the badge if desired. The poller is intentionally
|
||||
* forgiving: if the user is unauthenticated or the backend isn't ready,
|
||||
* it silently retries on the next tick.
|
||||
*
|
||||
* Workload-first: legacy project / site / stack counts were retired with
|
||||
* the hard cutover. `workloads` is now the single "things you have"
|
||||
* counter, supplementing `apps` (the grouping primitive) and
|
||||
* `containers` (the running instances).
|
||||
*/
|
||||
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
@@ -13,9 +18,8 @@ import * as api from '$lib/api';
|
||||
import { isAuthenticated } from '$lib/auth';
|
||||
|
||||
export interface NavCounts {
|
||||
projects: number | null;
|
||||
sites: number | null;
|
||||
stacks: number | null;
|
||||
apps: number | null;
|
||||
workloads: number | null;
|
||||
proxies: number | null;
|
||||
containers: number | null;
|
||||
/** Error-severity events only; dashboard surfaces total separately. */
|
||||
@@ -23,9 +27,8 @@ export interface NavCounts {
|
||||
}
|
||||
|
||||
const EMPTY: NavCounts = {
|
||||
projects: null,
|
||||
sites: null,
|
||||
stacks: null,
|
||||
apps: null,
|
||||
workloads: null,
|
||||
proxies: null,
|
||||
containers: null,
|
||||
eventsErrors: null
|
||||
@@ -36,31 +39,31 @@ const store = writable<NavCounts>(EMPTY);
|
||||
export const navCounts: Readable<NavCounts> = { subscribe: store.subscribe };
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let inFlight = false;
|
||||
let inFlight: AbortController | null = null;
|
||||
|
||||
async function refreshOnce(): Promise<void> {
|
||||
if (inFlight || !isAuthenticated()) return;
|
||||
inFlight = true;
|
||||
const ac = new AbortController();
|
||||
inFlight = ac;
|
||||
try {
|
||||
const [projects, sites, stacks, proxies, containers, eventStats] = await Promise.allSettled([
|
||||
api.listProjects(),
|
||||
api.listStaticSites(),
|
||||
api.listStacks(),
|
||||
api.listProxyRoutes(),
|
||||
api.listContainers({}),
|
||||
api.fetchEventLogStats()
|
||||
const [apps, workloads, proxies, containers, eventStats] = await Promise.allSettled([
|
||||
api.listApps(ac.signal),
|
||||
api.listWorkloads(undefined, ac.signal),
|
||||
api.listProxyRoutes(ac.signal),
|
||||
api.listContainers({}, ac.signal),
|
||||
api.fetchEventLogStats(ac.signal)
|
||||
]);
|
||||
|
||||
if (ac.signal.aborted) return;
|
||||
store.update((prev) => ({
|
||||
projects: projects.status === 'fulfilled' ? projects.value.length : prev.projects,
|
||||
sites: sites.status === 'fulfilled' ? sites.value.length : prev.sites,
|
||||
stacks: stacks.status === 'fulfilled' ? stacks.value.length : prev.stacks,
|
||||
apps: apps.status === 'fulfilled' ? apps.value.length : prev.apps,
|
||||
workloads: workloads.status === 'fulfilled' ? workloads.value.length : prev.workloads,
|
||||
proxies: proxies.status === 'fulfilled' ? proxies.value.length : prev.proxies,
|
||||
containers: containers.status === 'fulfilled' ? containers.value.length : prev.containers,
|
||||
eventsErrors: eventStats.status === 'fulfilled' ? eventStats.value.error : prev.eventsErrors
|
||||
}));
|
||||
} finally {
|
||||
inFlight = false;
|
||||
if (inFlight === ac) inFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +82,10 @@ export function stopNavCountsPolling(): void {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (inFlight) {
|
||||
inFlight.abort();
|
||||
inFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Trigger an out-of-band refresh (e.g. after a mutation). */
|
||||
|
||||
+21
-216
@@ -1,83 +1,13 @@
|
||||
// Types matching the Go backend store models (internal/store/models.go).
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
registry: string;
|
||||
image: string;
|
||||
port: number;
|
||||
healthcheck: string;
|
||||
env: string;
|
||||
volumes: string;
|
||||
npm_access_list_id: number;
|
||||
notification_url: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Stage {
|
||||
id: string;
|
||||
project_id: string;
|
||||
name: string;
|
||||
tag_pattern: string;
|
||||
auto_deploy: boolean;
|
||||
max_instances: number;
|
||||
confirm: boolean;
|
||||
enable_proxy: boolean;
|
||||
promote_from: string;
|
||||
subdomain: string;
|
||||
notification_url: string;
|
||||
cpu_limit: number;
|
||||
memory_limit: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance is a back-compat alias: project deploys used to live in a
|
||||
* dedicated `instances` table, but after the workload refactor the canonical
|
||||
* row is a Container. New code should use Container directly. The fields the
|
||||
* deployer always populates for project containers (workload_id, role,
|
||||
* stage_id, container_id, etc.) are required on Container; the alias is a
|
||||
* straight rename, not a relaxation of the type contract.
|
||||
*/
|
||||
export type Instance = Container;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ContainerState} for new code. Kept around for older
|
||||
* components that still narrow on the legacy four-state union.
|
||||
*/
|
||||
export type InstanceStatus = 'running' | 'stopped' | 'failed' | 'removing';
|
||||
|
||||
export interface Deploy {
|
||||
id: string;
|
||||
project_id: string;
|
||||
stage_id: string;
|
||||
instance_id: string;
|
||||
image_tag: string;
|
||||
status: DeployStatus;
|
||||
started_at: string;
|
||||
finished_at: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type DeployStatus =
|
||||
| 'pending'
|
||||
| 'pulling'
|
||||
| 'starting'
|
||||
| 'configuring_proxy'
|
||||
| 'health_checking'
|
||||
| 'success'
|
||||
| 'failed'
|
||||
| 'rolled_back';
|
||||
|
||||
export interface DeployLog {
|
||||
id: number;
|
||||
deploy_id: string;
|
||||
message: string;
|
||||
level: 'info' | 'warn' | 'error';
|
||||
created_at: string;
|
||||
}
|
||||
//
|
||||
// Workload-first refactor: the canonical primitive is a Workload (with
|
||||
// a Container row per instance). Legacy Project / Stage / Stack /
|
||||
// StaticSite / Deploy / Instance / Volume types were retired with the
|
||||
// hard cutover — the WebUI talks to /api/workloads, /api/containers,
|
||||
// /api/triggers, and friends only. The few "envelope" types still
|
||||
// useful across the surviving endpoints (ApiEnvelope, BrowseResult,
|
||||
// FileEntry, registries, settings, DNS, backups, NPM, system stats,
|
||||
// container stats, etc.) live below.
|
||||
|
||||
export interface Registry {
|
||||
id: string;
|
||||
@@ -181,12 +111,6 @@ export interface ApiEnvelope<T> {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Response shape for GET /api/projects/:id */
|
||||
export interface ProjectDetail {
|
||||
project: Project;
|
||||
stages: Stage[];
|
||||
}
|
||||
|
||||
/** Response shape for POST /api/deploy/inspect */
|
||||
export interface InspectResult {
|
||||
image: string;
|
||||
@@ -194,17 +118,6 @@ export interface InspectResult {
|
||||
healthcheck: string;
|
||||
}
|
||||
|
||||
/** Stage environment variable override. */
|
||||
export interface StageEnv {
|
||||
id: string;
|
||||
stage_id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
encrypted: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Item for the EntityPicker command-palette component. */
|
||||
export interface EntityPickerItem {
|
||||
value: string;
|
||||
@@ -217,20 +130,14 @@ export interface EntityPickerItem {
|
||||
}
|
||||
|
||||
/** Volume scope determines the sharing level. */
|
||||
export type VolumeScope = 'instance' | 'stage' | 'project' | 'project_named' | 'named' | 'ephemeral' | 'absolute';
|
||||
|
||||
/** Volume mount configuration for a project. */
|
||||
export interface Volume {
|
||||
id: string;
|
||||
project_id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
mode?: string;
|
||||
scope: VolumeScope;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
export type VolumeScope =
|
||||
| 'instance'
|
||||
| 'stage'
|
||||
| 'project'
|
||||
| 'project_named'
|
||||
| 'named'
|
||||
| 'ephemeral'
|
||||
| 'absolute';
|
||||
|
||||
/** Scope metadata returned by GET /api/volumes/scopes. */
|
||||
export interface VolumeScopeInfo {
|
||||
@@ -353,9 +260,7 @@ export interface EventLogStats {
|
||||
*
|
||||
* Shape matches the Go side after the workload refactor:
|
||||
* the embedded Container row is the canonical state and the workload/role
|
||||
* fields decorate it for display. The legacy `instance` / `project_name` /
|
||||
* `stage_name` aliases are exposed as optional getters via the StaleContainerCard
|
||||
* adapter so we don't have to update every consumer at once.
|
||||
* fields decorate it for display.
|
||||
*/
|
||||
export interface StaleContainer {
|
||||
container: Container;
|
||||
@@ -365,106 +270,6 @@ export interface StaleContainer {
|
||||
days_stale: number;
|
||||
}
|
||||
|
||||
/** A static site deployed from a Git repository folder. */
|
||||
export interface StaticSite {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: GitProvider;
|
||||
gitea_url: string;
|
||||
repo_owner: string;
|
||||
repo_name: string;
|
||||
branch: string;
|
||||
folder_path: string;
|
||||
access_token: string;
|
||||
domain: string;
|
||||
mode: 'static' | 'deno';
|
||||
render_markdown: boolean;
|
||||
sync_trigger: 'push' | 'tag' | 'manual';
|
||||
tag_pattern: string;
|
||||
container_id: string;
|
||||
proxy_route_id: string;
|
||||
status: StaticSiteStatus;
|
||||
last_sync_at: string;
|
||||
last_commit_sha: string;
|
||||
error: string;
|
||||
storage_enabled: boolean;
|
||||
storage_limit_mb: number;
|
||||
notification_url: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface StaticSiteStorageUsage {
|
||||
enabled: boolean;
|
||||
used_bytes: number;
|
||||
limit_mb: number;
|
||||
}
|
||||
|
||||
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. */
|
||||
export interface StaticSiteSecret {
|
||||
id: string;
|
||||
site_id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
encrypted: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** A repository from the Git provider's API. */
|
||||
export interface RepoInfo {
|
||||
owner: string;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
private: boolean;
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
/** A folder entry from the Gitea repo tree. */
|
||||
export interface FolderEntry {
|
||||
path: string;
|
||||
is_dir: boolean;
|
||||
}
|
||||
|
||||
/** Container CPU, memory, network, and block I/O stats from the Docker stats API. */
|
||||
export interface ContainerStats {
|
||||
timestamp?: string;
|
||||
@@ -539,8 +344,9 @@ export interface SystemStatsSample {
|
||||
export type WorkloadKind = 'project' | 'stack' | 'site' | 'plugin' | (string & {});
|
||||
|
||||
/**
|
||||
* Workload is the unifying primitive over Project / Stack / StaticSite,
|
||||
* plus plugin-native rows whose source/trigger kinds are populated.
|
||||
* Workload is the unifying primitive after the legacy Project / Stack /
|
||||
* StaticSite types were retired. Plugin-native rows whose source/trigger
|
||||
* kinds are populated are the only rows the WebUI creates now.
|
||||
*/
|
||||
export interface Workload {
|
||||
id: string;
|
||||
@@ -647,4 +453,3 @@ export interface App {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user