chore(workload): close the workload-first arc — apps i18n + codemap + tests
Build / build (push) Successful in 10m36s
Build / build (push) Successful in 10m36s
Closes the workload-first refactor by landing the Priority 3 polish items and the Priority 4 test gap. Net: ~2,400 lines added, ~350 lines modified across 13 files. Priority 3 — polish - apps.* i18n namespace: 276 new keys across apps.list.* (27), apps.new.* (91, sibling of existing apps.new.triggers.*), and apps.detail.* (158, sibling of existing apps.detail.bindings.*). EN+RU at 1314 keys each, perfectly in sync. /apps, /apps/new, /apps/[id] now render entirely from i18n. - New codemap docs/CODEMAPS/workload-plugin.md (238 lines): Source × Trigger contract, dispatch seam, webhook fan-out path, recipes for adding a new Source or Trigger kind. Plus docs/CODEMAPS/INDEX.md gateway. Priority 4 — tests - internal/api/workloads_test.go (new, ~30 subtests): /api/workloads CRUD + deploy + delete + env + volumes + chain + promote-from + triggers list/inline-bind + auth gating + standalone /api/triggers CRUD (create / dup-409 / kind filter / delete). Uses real POST handlers via httptest.NewServer + a fake plugin source registered under "testfakesource". - internal/deployer/dispatch_test.go (new, 11 tests): DispatchPlugin / DispatchTeardown / DispatchReconcile happy + unknown-kind + propagated-error each; PluginDeps wiring; a real 2s-bounded RWMutex deadlock probe on PluginDeps vs SetDNSProvider. - internal/workload/plugin/source/compose/compose_test.go (new, ~26 subtests): composeProjectName sanitization, writeYAML / writeYAMLIfChanged hash short-circuit, Validate happy + bad inputs, Kind / SchemaSample. Coverage delta on the workload-plugin path: - internal/api: 1.1% → 16.0% - internal/deployer: 0% → 54.1% - internal/workload/plugin/source/compose: 0% → 38.5% - Trigger plugins already at 87-95% from the trigger-split work. Production fix surfaced by the tests - store.CreateWorkload now self-references RefID = ID when caller leaves RefID empty (the typical plugin-native path). The api layer's broken backfill loop (called UpdateWorkload, which deliberately omits ref_id) is gone. Multiple sibling plugin workloads can now coexist under the UNIQUE(kind, ref_id) constraint. Review fixes addressed before commit - CRITICAL: deadlock-detect test gained a real 2s time.After (was selecting on context.Background().Done() which never fires). - HIGH: happy-path test now hard-asserts RefID = ID (was a t.Logf that would silently pass after a production fix). - HIGH: standalone /api/triggers CRUD coverage added (was bypassed by the workload-side bind flow). - HIGH: seedWorkload bypass deleted; tests now go through the real POST /api/workloads handler. - MEDIUM: withTempDir restore is a no-op (t.Setenv auto-restores); dead `old := os.Getenv(...)` capture removed. - MEDIUM: list-workloads test now asserts ID membership, not just count. Doc - WORKLOAD_REFACTOR_TODO: all three Priority 1 items, Priority 3 polish, and Priority 4 tests marked DONE. The workload-first arc is closed.
This commit is contained in:
@@ -1128,7 +1128,127 @@
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"list": {
|
||||
"eyebrowSuffix": "APPS",
|
||||
"title": "Apps",
|
||||
"ledePrefix": "Plugin-native deployables —",
|
||||
"ledeKindImage": "image",
|
||||
"ledeKindCompose": "compose",
|
||||
"ledeKindStatic": "static",
|
||||
"ledeMiddle": ", or",
|
||||
"ledeSuffix": ", with pluggable redeploy triggers. Legacy projects, stacks, and sites continue to live under their own sections during the cutover.",
|
||||
"statTotal": "TOTAL",
|
||||
"statImage": "IMAGE",
|
||||
"statCompose": "COMPOSE",
|
||||
"statStatic": "STATIC",
|
||||
"refresh": "Refresh",
|
||||
"newApp": "New App",
|
||||
"filterAriaLabel": "Filter by source plugin",
|
||||
"filterAll": "ALL",
|
||||
"loadError": "Failed to load apps",
|
||||
"alertTag": "ERR",
|
||||
"emptyTitle": "No apps yet",
|
||||
"emptyBody": "Apps unify image, compose, and static deployables behind a single plugin-driven surface. Forge your first one to see it light up here.",
|
||||
"emptyCta": "Forge the first app",
|
||||
"colName": "Name",
|
||||
"colSource": "Source",
|
||||
"colTrigger": "Trigger",
|
||||
"colCreated": "Created",
|
||||
"colActions": "Actions",
|
||||
"rowOpen": "Open"
|
||||
},
|
||||
"new": {
|
||||
"pageTitle": "New App · Tinyforge",
|
||||
"backLabel": "Back to apps",
|
||||
"eyebrowSuffix": "NEW APP",
|
||||
"title": "Forge a new app",
|
||||
"ledePrefix": "Create a plugin-native workload.",
|
||||
"ledeSourceLabel": "Source",
|
||||
"ledeSourceMid": "= how it deploys (image, compose, static). Pick or create a",
|
||||
"ledeTriggerLabel": "trigger",
|
||||
"ledeSuffix": "below — when one fires, the source plugin redeploys.",
|
||||
"loadingKinds": "Loading available plugin kinds…",
|
||||
"alertTag": "ERR",
|
||||
"fieldName": "Name",
|
||||
"fieldNameRequired": "REQUIRED",
|
||||
"fieldNamePlaceholder": "my-app",
|
||||
"fieldNameHint": "Lowercase, no spaces. Becomes part of container names and subdomains.",
|
||||
"fieldSourcePlugin": "Source plugin",
|
||||
"fieldSourceLabel": "Source",
|
||||
"fieldSourceHint": "Populated from the running daemon — only plugins compiled in show up. Triggers (registry / git / manual) are configured below as standalone records.",
|
||||
"fieldSourceConfig": "Source config",
|
||||
"fieldConfigYaml": "YAML",
|
||||
"fieldConfigForm": "FORM",
|
||||
"fieldConfigJson": "JSON",
|
||||
"advancedJson": "Advanced JSON",
|
||||
"backToForm": "Back to form",
|
||||
"resetSample": "Reset sample",
|
||||
"switchToJsonTitle": "Switch to the raw JSON editor",
|
||||
"switchToFormTitle": "Switch back to the form",
|
||||
"jsonOk": "JSON OK",
|
||||
"jsonInvalid": "JSON INVALID",
|
||||
"linesUnit": "lines",
|
||||
"composeHeader": "compose.yaml · compose",
|
||||
"composeAriaLabel": "Compose YAML",
|
||||
"composeProjectLabel": "Compose project name (optional)",
|
||||
"composeProjectPlaceholder": "(defaults to sanitized workload name)",
|
||||
"composePlaceholder": "services:\n web:\n image: nginx:alpine\n ports:\n - \"80\"",
|
||||
"imageHeader": "image source · runtime knobs",
|
||||
"imageRefLabel": "Image (registry path)",
|
||||
"imageRefPlaceholder": "registry.example.com/owner/app",
|
||||
"imageRefHint": "Fully-qualified reference; the tag is set per-deploy via the trigger or the Default tag field below.",
|
||||
"imagePort": "Port",
|
||||
"imageHealthcheck": "Healthcheck path",
|
||||
"imageDefaultTag": "Default tag",
|
||||
"imageRegistryLabel": "Registry (for private pulls)",
|
||||
"imageRegistryPublic": "(public — no auth)",
|
||||
"imageRegistryHint": "Match the name from the Registries settings page. Leave empty for public images.",
|
||||
"imageCpu": "CPU limit (cores, 0 = ∞)",
|
||||
"imageMemory": "Memory limit (MB, 0 = ∞)",
|
||||
"imageMax": "Max instances",
|
||||
"imageMaxHint": "1 = strict blue-green.",
|
||||
"imageFoot": "Env vars and volume mounts live in their own panels on the workload detail page after creation.",
|
||||
"staticHeader": "static source · pages from a repo",
|
||||
"staticProvider": "Provider",
|
||||
"staticBaseUrl": "Base URL",
|
||||
"staticBaseUrlPlaceholder": "https://git.example.com",
|
||||
"staticRepoOwner": "Repo owner",
|
||||
"staticRepoOwnerPlaceholder": "owner",
|
||||
"staticRepoName": "Repo name",
|
||||
"staticRepoNamePlaceholder": "pages",
|
||||
"staticBranch": "Branch",
|
||||
"staticBranchPlaceholder": "main",
|
||||
"staticFolder": "Folder path (optional)",
|
||||
"staticFolderPlaceholder": "(repo root)",
|
||||
"staticToken": "Access token (private repos)",
|
||||
"staticTokenPlaceholder": "(leave blank for public repos)",
|
||||
"staticTokenHint": "Encrypted at rest. Required only when the repo is private.",
|
||||
"staticMode": "Mode",
|
||||
"staticModeStaticDesc": "— serve files via nginx; zero runtime overhead.",
|
||||
"staticModeDenoDesc": "— Deno runtime container with optional dynamic routing.",
|
||||
"staticRenderMarkdown": "Render markdown",
|
||||
"staticRenderMarkdownDesc": "— auto-render <code>.md</code> files as HTML pages.",
|
||||
"staticFoot": "The webhook secret for git push triggers lives on the workload's Webhook panel after creation.",
|
||||
"sourceConfigJsonTitle": "source_config.json · {kind}",
|
||||
"sourceConfigJsonAria": "Source plugin configuration (JSON)",
|
||||
"triggerNumLabel": "Trigger",
|
||||
"triggerNumOptional": "OPTIONAL",
|
||||
"triggerNewTag": "NEW",
|
||||
"triggerPickTag": "PICK",
|
||||
"triggerSkipTag": "SKIP",
|
||||
"noteSkipTag": "SKIP",
|
||||
"noteEmptyTag": "∅",
|
||||
"faceLabel": "Public face",
|
||||
"faceOptional": "OPTIONAL",
|
||||
"faceSubdomain": "Subdomain",
|
||||
"faceSubdomainPlaceholder": "myapp",
|
||||
"faceDomain": "Domain",
|
||||
"faceDomainPlaceholder": "(inherit from settings)",
|
||||
"facePort": "Target port",
|
||||
"faceHint": "Leave blank to skip provisioning a proxy route. Filling any field creates a single face row attached to this workload.",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Forge app",
|
||||
"submitting": "Forging…",
|
||||
"triggers": {
|
||||
"section": "Trigger",
|
||||
"sectionSub": "Optional. Pick how this app gets a redeploy signal — registry watcher, git event, or manual button.",
|
||||
@@ -1151,6 +1271,160 @@
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"pageTitleFallback": "App",
|
||||
"backLabel": "Back to apps",
|
||||
"eyebrowSuffix": "APP",
|
||||
"kickerId": "id: {id}",
|
||||
"loading": "Loading workload…",
|
||||
"loadError": "Failed to load app",
|
||||
"deployError": "Deploy failed",
|
||||
"saveError": "Save failed",
|
||||
"deleteError": "Delete failed",
|
||||
"alertTag": "ERR",
|
||||
"createdAt": "created",
|
||||
"refreshLabel": "Refresh",
|
||||
"editButton": "Edit",
|
||||
"deleteButton": "Delete",
|
||||
"editTitle": "Edit configuration",
|
||||
"editSubPrefix": "Source",
|
||||
"editSubSuffix": "· triggers managed in the Triggers panel below",
|
||||
"editFieldName": "Name",
|
||||
"editFieldParent": "Parent workload",
|
||||
"editFieldOptional": "OPTIONAL",
|
||||
"editFieldParentPlaceholder": "workload UUID (blank for root)",
|
||||
"editSourceConfig": "Source config",
|
||||
"editConfigYaml": "YAML",
|
||||
"editConfigForm": "FORM",
|
||||
"editConfigJson": "JSON",
|
||||
"advancedJson": "Advanced JSON",
|
||||
"backToForm": "Back to form",
|
||||
"switchToJsonTitle": "Switch to the raw JSON editor",
|
||||
"switchToFormTitle": "Switch back to the form",
|
||||
"jsonOk": "JSON OK",
|
||||
"jsonInvalid": "JSON INVALID",
|
||||
"editComposeProject": "Compose project name (optional)",
|
||||
"editComposeProjectPlaceholder": "(defaults to sanitized workload name)",
|
||||
"editComposePlaceholder": "services:\n web:\n image: nginx:alpine",
|
||||
"editComposeAria": "Compose YAML",
|
||||
"editComposeHeader": "compose.yaml",
|
||||
"editImageHeader": "image source · runtime knobs",
|
||||
"editImageRef": "Image (registry path)",
|
||||
"editImageRefPlaceholder": "registry.example.com/owner/app",
|
||||
"editImagePort": "Port",
|
||||
"editImageHealthcheck": "Healthcheck path",
|
||||
"editImageDefaultTag": "Default tag",
|
||||
"editImageRegistry": "Registry (for private pulls)",
|
||||
"editImageRegistryPublic": "(public — no auth)",
|
||||
"editImageCpu": "CPU limit (cores, 0 = ∞)",
|
||||
"editImageMemory": "Memory limit (MB, 0 = ∞)",
|
||||
"editImageMax": "Max instances",
|
||||
"editImageFoot": "Env vars and volume mounts use their own panels below — saving here preserves them.",
|
||||
"editStaticHeader": "static source · pages from a repo",
|
||||
"editStaticProvider": "Provider",
|
||||
"editStaticBaseUrl": "Base URL",
|
||||
"editStaticBaseUrlPlaceholder": "https://git.example.com",
|
||||
"editStaticRepoOwner": "Repo owner",
|
||||
"editStaticRepoName": "Repo name",
|
||||
"editStaticBranch": "Branch",
|
||||
"editStaticFolder": "Folder path (optional)",
|
||||
"editStaticFolderPlaceholder": "(repo root)",
|
||||
"editStaticToken": "Access token (private repos)",
|
||||
"editStaticTokenPlaceholder": "(leave blank for public repos)",
|
||||
"editStaticMode": "Mode",
|
||||
"editStaticModeStaticDesc": "— serve files via nginx.",
|
||||
"editStaticModeDenoDesc": "— Deno runtime with dynamic routing.",
|
||||
"editStaticRenderMarkdown": "Render markdown",
|
||||
"editStaticRenderMarkdownDesc": "— auto-render <code>.md</code> as HTML.",
|
||||
"editSourceJsonHeader": "source_config.json",
|
||||
"editSourceJsonAria": "Source plugin configuration (JSON)",
|
||||
"editPublicFaces": "Public faces",
|
||||
"editPublicFacesTag": "JSON ARRAY",
|
||||
"editPublicFacesHeader": "public_faces.json",
|
||||
"editPublicFacesAria": "Public faces configuration (JSON array)",
|
||||
"editCancel": "Cancel",
|
||||
"editSave": "Save changes",
|
||||
"editSaving": "Saving…",
|
||||
"manualDeployTitle": "Manual deploy",
|
||||
"manualDeployOk": "OK",
|
||||
"manualDeployDispatched": "Dispatched {reference} as {by}",
|
||||
"manualDeployRefAria": "Deploy reference",
|
||||
"manualDeployRefPlaceholder": "reference (image tag, git sha, blank for default)",
|
||||
"manualDeployButton": "Deploy",
|
||||
"manualDeployDispatching": "Dispatching…",
|
||||
"manualDeployHint": "Use a specific image tag, git sha, or branch to force a deploy. Leave blank to use the default reference resolved by the source plugin.",
|
||||
"containersTitle": "Containers",
|
||||
"containersEmpty": "No containers yet",
|
||||
"containersCount": "{count} reconciled",
|
||||
"containersEmptyInline": "No containers yet — deploy to spin one up.",
|
||||
"containersColRole": "Role",
|
||||
"containersColState": "State",
|
||||
"containersColImage": "Image",
|
||||
"containersColSubdomain": "Subdomain",
|
||||
"containersColLastSeen": "Last seen",
|
||||
"containersColActions": "Actions",
|
||||
"containersLogsAction": "Logs",
|
||||
"chainTitle": "Chain",
|
||||
"chainSubFromParent": "promotes from a parent",
|
||||
"chainSubParentOf": "parent of",
|
||||
"chainChildSingular": "child",
|
||||
"chainChildPlural": "children",
|
||||
"chainParentLabel": "Parent",
|
||||
"chainSelfLabel": "This",
|
||||
"chainChildrenLabel": "Children",
|
||||
"chainPromoteButton": "Promote from parent",
|
||||
"chainPromoting": "Promoting…",
|
||||
"chainHint": "Set <code>parent_workload_id</code> on a workload to build a chain. Image-source children can promote the parent's currently-running tag with one click.",
|
||||
"volumesTitle": "Volumes",
|
||||
"volumesEmpty": "No mounts",
|
||||
"volumesCountSingular": "{count} mount",
|
||||
"volumesCountPlural": "{count} mounts",
|
||||
"volumesColTarget": "Target",
|
||||
"volumesColSource": "Source",
|
||||
"volumesColScope": "Scope",
|
||||
"volumesColUpdated": "Updated",
|
||||
"volumesColActions": "Actions",
|
||||
"volumeSource": "Source (host)",
|
||||
"volumeSourcePlaceholder": "/srv/data/myapp",
|
||||
"volumeTarget": "Target (container)",
|
||||
"volumeTargetPlaceholder": "/data",
|
||||
"volumeScope": "Scope",
|
||||
"volumeAddButton": "Add / Replace",
|
||||
"volumeSaving": "Saving…",
|
||||
"volumeHint": "Absolute mounts bind a host path into the container. Non-absolute scopes are accepted for future use; only absolute is honoured at deploy time today.",
|
||||
"volumeTargetError": "Target must be an absolute container path (e.g. /data)",
|
||||
"volumeSetFailed": "Failed to set volume",
|
||||
"volumeDeleteFailed": "Failed to delete volume",
|
||||
"envTitle": "Env",
|
||||
"envEmpty": "No overrides",
|
||||
"envCountSingular": "{count} override",
|
||||
"envCountPlural": "{count} overrides",
|
||||
"envColKey": "Key",
|
||||
"envColValue": "Value",
|
||||
"envColUpdated": "Updated",
|
||||
"envColActions": "Actions",
|
||||
"envEncrypted": "ENCRYPTED",
|
||||
"envKey": "Key",
|
||||
"envKeyPlaceholder": "DATABASE_URL",
|
||||
"envValue": "Value",
|
||||
"envValuePlaceholder": "(empty to unset)",
|
||||
"envEncryptLabel": "Encrypt at rest",
|
||||
"envAddButton": "Add / Replace",
|
||||
"envSaving": "Saving…",
|
||||
"envHint": "Encrypted values are write-only after store — the API redacts them on read. Rotate by setting a new value.",
|
||||
"envKeyRequired": "Key is required",
|
||||
"envSetFailed": "Failed to set env",
|
||||
"envDeleteFailed": "Failed to delete env",
|
||||
"sourceConfigTitle": "Source config",
|
||||
"sourceConfigCopy": "Copy",
|
||||
"sourceConfigCopied": "Copied",
|
||||
"sourceConfigCopyAria": "Copy source config",
|
||||
"publicFacesTitle": "Public faces",
|
||||
"publicFacesCopyAria": "Copy public faces",
|
||||
"deleteConfirmTitle": "Delete this app?",
|
||||
"deleteConfirmMessage": "Tears down all containers and proxy routes owned by \"{name}\", then removes the row. This cannot be undone.",
|
||||
"deleteConfirmFallbackName": "this workload",
|
||||
"deleteConfirmYes": "Yes, delete",
|
||||
"deleteConfirmDeleting": "Deleting…",
|
||||
"manualDeploySub": "Bypasses configured triggers and dispatches through the source plugin directly.",
|
||||
"chainTriggersZero": "no triggers",
|
||||
"chainTriggersOne": "1 trigger",
|
||||
|
||||
Reference in New Issue
Block a user