feat(apps): stepped creation wizard, branch previews, and app-creation fixes
This session (frontend focus):
- Rebuild /apps/new as a 4-step wizard (Basics → Configure → Trigger → Review):
WizardRail, SourceKindPicker card grid, AppManifest review, per-step validation,
ConfirmDialog-based unsaved-changes guard.
- Extract lib/workload/sourceForms.ts (single source of truth for source_config)
+ {Image,Compose,Static,Dockerfile}SourceForm + StaticDiscoveryWizard; fold the
/apps/[id] edit form onto the same components (removes the duplication). Add
vitest + sourceForms unit tests.
- Branch preview environments UI: /chain is_preview/preview_branch + a Preview
environments panel on /apps/[id] (per-branch URLs, ConfirmDialog teardown, armed
state); RegistryImagePicker on the registry trigger and the image source.
- Fixes: image-inspect 404 -> admin-gated POST /api/discovery/image/inspect;
conflict-panel blur flicker; friendly localized discovery errors; CPU/Memory
label hints; dashboard + /apps "Total workloads" count only source_kind workloads
(drop stale trigger_kind gate); NPM cert/access-list name cache; EntityPicker
empty-list guard.
- Update CLAUDE.md frontend conventions + add a Build & Test section.
Also captures pre-existing in-progress platform work (not from this session):
workload notifications, Prometheus metrics export, store lockfile, health probes,
backup hardening, and related store/webhook/scheduler changes.
This commit is contained in:
+143
-15
@@ -15,7 +15,7 @@
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"apps": "Apps",
|
||||
"eventTriggers": "Triggers",
|
||||
"eventTriggers": "Event Triggers",
|
||||
"logScanRules": "Log Rules",
|
||||
"triggers": "Triggers",
|
||||
"proxies": "Proxies",
|
||||
@@ -23,7 +23,13 @@
|
||||
"settings": "Settings",
|
||||
"logout": "Log out",
|
||||
"dns": "DNS Records",
|
||||
"containers": "Containers"
|
||||
"containers": "Containers",
|
||||
"sectionObserve": "Observe",
|
||||
"sectionSystem": "System",
|
||||
"closeSidebar": "Close sidebar",
|
||||
"openSidebar": "Open sidebar",
|
||||
"quickNavTitle": "Press 'g' then a letter to jump between sections",
|
||||
"quickNavLabel": "quick-nav"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -42,7 +48,11 @@
|
||||
"systemHealth": "System health",
|
||||
"daemons": "Daemons",
|
||||
"systemResources": "System resources",
|
||||
"systemResourcesSubtitle": "CPU, memory, disk, and top consumers"
|
||||
"systemResourcesSubtitle": "CPU, memory, disk, and top consumers",
|
||||
"statSubWorkloads": "workloads →",
|
||||
"statSubRunning": "running",
|
||||
"statSubNeedAttention": "need attention",
|
||||
"statSubStale": "stale →"
|
||||
},
|
||||
"resources": {
|
||||
"cpuCores": "CPU Cores",
|
||||
@@ -237,6 +247,7 @@
|
||||
"deleteFailed": "Failed to delete registry",
|
||||
"testFailed": "Connection test failed",
|
||||
"loadFailed": "Failed to load registries",
|
||||
"deleteTitle": "Delete registry?",
|
||||
"deleteConfirm": "Delete registry \"{name}\"? This cannot be undone.",
|
||||
"healthChecking": "Checking...",
|
||||
"healthConnected": "Connected",
|
||||
@@ -354,6 +365,7 @@
|
||||
"createFailed": "Failed to create user",
|
||||
"deleteFailed": "Failed to delete user",
|
||||
"deleteConfirm": "Are you sure you want to delete this user?",
|
||||
"deleteConfirmMessage": "Delete user \"{username}\"? This cannot be undone.",
|
||||
"usernameRequired": "Username and password are required",
|
||||
"networkError": "Network error",
|
||||
"password": "Password"
|
||||
@@ -400,6 +412,9 @@
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"close": "Close",
|
||||
"toggle": "Toggle",
|
||||
"dismissNotification": "Dismiss notification",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"change": "Change",
|
||||
@@ -429,6 +444,7 @@
|
||||
"missing": "Missing"
|
||||
},
|
||||
"containers": {
|
||||
"eyebrowSuffix": "GLOBAL",
|
||||
"errLoad": "Failed to load containers",
|
||||
"searchPlaceholder": "Search workload, role, image, subdomain…",
|
||||
"kindFilterLabel": "Workload kind",
|
||||
@@ -476,6 +492,7 @@
|
||||
},
|
||||
"stale": {
|
||||
"title": "Stale Containers",
|
||||
"eyebrowSuffix": "STALE",
|
||||
"noStale": "No stale containers",
|
||||
"noStaleDesc": "All containers are healthy and running.",
|
||||
"cleanup": "Clean up",
|
||||
@@ -541,13 +558,13 @@
|
||||
"unavailable": "Stats unavailable"
|
||||
},
|
||||
"systemHealth": {
|
||||
"title": "System Health",
|
||||
"containers": "Containers",
|
||||
"proxies": "Proxies",
|
||||
"recentErrors": "Recent Errors"
|
||||
},
|
||||
"daemons": {
|
||||
"title": "Daemons",
|
||||
"notReachable": "{provider} is not reachable.",
|
||||
"refresh": "Refresh",
|
||||
"refreshing": "Refreshing",
|
||||
"docker": "Docker Engine",
|
||||
@@ -1110,6 +1127,10 @@
|
||||
"image": "Image reference",
|
||||
"imagePlaceholder": "registry.example.com/owner/app",
|
||||
"imageHint": "Full image reference without the tag — Tinyforge matches new tags pushed under it.",
|
||||
"browseImages": "Browse",
|
||||
"browseImagesHint": "Pick an image from a configured registry instead of typing the reference.",
|
||||
"browseImagesTitle": "Select an image",
|
||||
"browseImagesSearch": "Search images…",
|
||||
"tagPattern": "Tag pattern",
|
||||
"tagPatternPlaceholder": "*",
|
||||
"tagPatternHint": "path.Match glob (e.g. v*, release-*). Use * to match every tag.",
|
||||
@@ -1122,6 +1143,9 @@
|
||||
"branch": "Branch",
|
||||
"branchPlaceholder": "main",
|
||||
"branchHint": "Only push events advancing this branch fire the trigger.",
|
||||
"branchPattern": "Branch pattern (preview deploys)",
|
||||
"branchPatternPlaceholder": "feat/* or * for any branch",
|
||||
"branchPatternHint": "When set, any push to a matching branch spawns a per-branch preview deploy. Leave empty to disable previews.",
|
||||
"manualNote": "Manual triggers carry no config. They fire only via the workload's Deploy button or POST /workloads/{id}/deploy.",
|
||||
"scheduleNote": "Fires on a fixed interval driven by Tinyforge's internal scheduler. No external webhook is required — enable the webhook ingress below only if a CI also needs to fire it on demand.",
|
||||
"intervalPresets": "Quick presets",
|
||||
@@ -1186,6 +1210,14 @@
|
||||
},
|
||||
"new": {
|
||||
"pageTitle": "New App · Tinyforge",
|
||||
"wizard": {
|
||||
"stepBasics": "Basics",
|
||||
"stepConfigure": "Configure",
|
||||
"stepTrigger": "Trigger",
|
||||
"stepReview": "Review",
|
||||
"next": "Next",
|
||||
"back": "Back"
|
||||
},
|
||||
"backLabel": "Back to apps",
|
||||
"eyebrowSuffix": "NEW APP",
|
||||
"title": "Forge a new app",
|
||||
@@ -1198,6 +1230,7 @@
|
||||
"alertTag": "ERR",
|
||||
"fieldName": "Name",
|
||||
"fieldNameRequired": "REQUIRED",
|
||||
"fieldRequired": "Required",
|
||||
"fieldNamePlaceholder": "my-app",
|
||||
"fieldNameHint": "Lowercase, no spaces. Becomes part of container names and subdomains.",
|
||||
"fieldSourcePlugin": "Source plugin",
|
||||
@@ -1207,7 +1240,7 @@
|
||||
"fieldConfigYaml": "YAML",
|
||||
"fieldConfigForm": "FORM",
|
||||
"fieldConfigJson": "JSON",
|
||||
"advancedJson": "Advanced JSON",
|
||||
"advancedJson": "Edit as JSON",
|
||||
"backToForm": "Back to form",
|
||||
"resetSample": "Reset sample",
|
||||
"switchToJsonTitle": "Switch to the raw JSON editor",
|
||||
@@ -1230,11 +1263,21 @@
|
||||
"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 = ∞)",
|
||||
"imageCpu": "CPU limit",
|
||||
"imageCpuHint": "Cores, 0 = ∞",
|
||||
"imageMemory": "Memory limit",
|
||||
"imageMemoryHint": "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.",
|
||||
"dockerfileHeader": "dockerfile source · build from a git repo",
|
||||
"dockerfileBuildEyebrow": "build · dockerfile",
|
||||
"dockerfileContextPath": "Build context",
|
||||
"dockerfileContextPathPlaceholder": "(empty = repo root)",
|
||||
"dockerfilePath": "Dockerfile path",
|
||||
"dockerfilePort": "Container port",
|
||||
"dockerfilePortRequired": "Container port is required — pick the port your app listens on (1–65535).",
|
||||
"dockerfileFoot": "Tinyforge clones the repo, builds the image from the Dockerfile, and runs one container. Env vars and volumes live in the detail page after creation.",
|
||||
"staticHeader": "static source · pages from a repo",
|
||||
"staticProvider": "Provider",
|
||||
"staticBaseUrl": "Base URL",
|
||||
@@ -1262,7 +1305,7 @@
|
||||
"staticTestConnection": "Test connection",
|
||||
"staticConnectionOk": "Connected",
|
||||
"staticConnectionFailed": "Connection failed: {error}",
|
||||
"staticBrowseRepos": "Browse repositories",
|
||||
"staticBrowseRepos": "Browse",
|
||||
"staticBrowseBranches": "Browse branches",
|
||||
"staticBrowseFolders": "Browse folders",
|
||||
"staticPickerRepoTitle": "Select repository",
|
||||
@@ -1275,6 +1318,7 @@
|
||||
"staticTreeEmpty": "No folders found in this branch.",
|
||||
"staticDenoAutoDetected": "Auto-detected an <code>api/</code> folder — switched to Deno mode.",
|
||||
"imageConflictTag": "IMAGE IN USE",
|
||||
"imageConflictChecking": "Checking for conflicts…",
|
||||
"imageConflictHeading": "{count} workload(s) already use this image:",
|
||||
"imageConflictOpenBtn": "Open",
|
||||
"imageConflictAcknowledgeNote": "If this is intentional (for example a separate stage), continue to create a new workload.",
|
||||
@@ -1300,21 +1344,23 @@
|
||||
"submit": "Forge app",
|
||||
"submitting": "Forging…",
|
||||
"submitAnyway": "Forge anyway",
|
||||
"unsavedChanges": "You have unsaved changes to this app. Leave without creating it?",
|
||||
"unsavedChangesTitle": "Unsaved changes",
|
||||
"unsavedChangesConfirm": "Leave",
|
||||
"errors": {
|
||||
"detectionFailed": "Provider detection failed.",
|
||||
"connectionFailed": "Connection failed.",
|
||||
"reposFailed": "Failed to load repositories.",
|
||||
"branchesFailed": "Failed to load branches.",
|
||||
"treeFailed": "Failed to load folder tree.",
|
||||
"detectionFailed": "Couldn't detect a Git provider at that URL. Check the base URL is correct and reachable.",
|
||||
"connectionFailed": "Couldn't reach the repository. Check the provider URL, owner/repo, and access token (for private repos).",
|
||||
"reposFailed": "Couldn't list repositories. Check the base URL and access token.",
|
||||
"branchesFailed": "Couldn't list branches. Check the repository and access token.",
|
||||
"treeFailed": "Couldn't load the folder tree. Check the repository, branch, and access token.",
|
||||
"sourceConfigInvalid": "Source config is not valid JSON.",
|
||||
"triggerBindUnknown": "unknown error",
|
||||
"createFailed": "Workload create failed.",
|
||||
"inspectFailed": "Image inspect failed."
|
||||
"inspectFailed": "Couldn't inspect that image — make sure it's pulled locally and the reference is correct."
|
||||
},
|
||||
"imageInspect": "Inspect",
|
||||
"imageInspectHint": "Pulls port + healthcheck from the image so you don't have to type them.",
|
||||
"imageInspectOk": "Inspected — port + healthcheck filled.",
|
||||
"imageInspectError": "Inspect failed: {error}",
|
||||
"triggers": {
|
||||
"section": "Trigger",
|
||||
"sectionSub": "Optional. Pick how this app gets a redeploy signal — registry watcher, git event, or manual button.",
|
||||
@@ -1334,6 +1380,18 @@
|
||||
"pickWebhookOn": "WEBHOOK ON",
|
||||
"skippedNote": "No trigger will be bound. You can add one from the app's Triggers panel after it's created.",
|
||||
"bindError": "App created, but the trigger binding failed: {error}. Open the app's Triggers panel to retry."
|
||||
},
|
||||
"manifest": {
|
||||
"title": "Manifest",
|
||||
"name": "Name",
|
||||
"source": "Source",
|
||||
"trigger": "Trigger",
|
||||
"publicFace": "Public face",
|
||||
"unnamed": "(unnamed)",
|
||||
"registryPublic": "public registry",
|
||||
"folderRoot": "root",
|
||||
"triggerManual": "Manual only",
|
||||
"internalOnly": "Internal only"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
@@ -1365,6 +1423,40 @@
|
||||
"unavailable": "Usage probe unavailable (container may be stopped).",
|
||||
"loading": "Computing usage…"
|
||||
},
|
||||
"buildLog": {
|
||||
"title": "Build log",
|
||||
"sub": "Live tail of the Docker daemon's build output.",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notification routes",
|
||||
"sub": "Multi-destination fan-out for deploy/build events. Falls back to the workload's legacy URL when empty.",
|
||||
"loading": "Loading routes…",
|
||||
"empty": "No per-workload notification routes configured. Add one to get a per-channel destination.",
|
||||
"addFirst": "Add first route",
|
||||
"add": "Add route",
|
||||
"edit": "Edit route",
|
||||
"delete": "Delete route",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Slack #alerts",
|
||||
"url": "Webhook URL",
|
||||
"secret": "Signing secret",
|
||||
"secretPlaceholder": "Optional — receiver verifies HMAC if set",
|
||||
"secretEditPlaceholder": "Leave empty to keep the existing secret",
|
||||
"secretHint": "HMAC-SHA256 over the request body, sent as X-Hub-Signature-256.",
|
||||
"eventTypes": "Event types",
|
||||
"eventTypesPlaceholder": "deploy_failure,build_failure (empty = all)",
|
||||
"eventTypesHint": "Comma-separated allow-list. Empty means every event fires this route.",
|
||||
"enabled": "Enabled",
|
||||
"save": "Save route",
|
||||
"saving": "Saving…",
|
||||
"cancel": "Cancel",
|
||||
"allEvents": "all events",
|
||||
"signed": "signed",
|
||||
"disabled": "disabled",
|
||||
"confirmDeleteTitle": "Delete notification route?",
|
||||
"confirmDeleteMessage": "This route will stop firing immediately. The workload's legacy notification URL (if set) will resume catching events when no routes match."
|
||||
},
|
||||
"toolbar": {
|
||||
"stop": "Stop",
|
||||
"start": "Start",
|
||||
@@ -1455,6 +1547,19 @@
|
||||
"editStaticModeDenoDesc": "— Deno runtime with dynamic routing.",
|
||||
"editStaticRenderMarkdown": "Render markdown",
|
||||
"editStaticRenderMarkdownDesc": "— auto-render <code>.md</code> as HTML.",
|
||||
"editDockerfileHeader": "dockerfile source · build from a git repo",
|
||||
"editDockerfileBuildEyebrow": "build · dockerfile",
|
||||
"editDockerfileContextPath": "Build context",
|
||||
"editDockerfileContextPathPlaceholder": "(empty = repo root)",
|
||||
"editDockerfilePath": "Dockerfile path",
|
||||
"editDockerfilePort": "Container port",
|
||||
"editTestConnection": "Test connection",
|
||||
"editTestConnectionOk": "Connection OK",
|
||||
"editTestConnectionFailed": "Connection failed: {error}",
|
||||
"editTestConnectionUnknownError": "Unknown error",
|
||||
"overrideKeyUnitSingular": "KEY",
|
||||
"overrideKeyUnitPlural": "KEYS",
|
||||
"editTestConnectionIncomplete": "Fill provider, base URL, owner, and name first.",
|
||||
"editSourceJsonHeader": "source_config.json",
|
||||
"editSourceJsonAria": "Source plugin configuration (JSON)",
|
||||
"editPublicFaces": "Public faces",
|
||||
@@ -1494,6 +1599,29 @@
|
||||
"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.",
|
||||
"previews": {
|
||||
"title": "Preview environments",
|
||||
"subEmpty": "no active previews",
|
||||
"subCountOne": "1 active preview",
|
||||
"subCount": "{count} active previews",
|
||||
"tag": "Preview",
|
||||
"tagTitle": "Per-branch preview deploy of this workload",
|
||||
"armedEmpty": "No active previews — push to a branch matching",
|
||||
"noneEmpty": "No active previews yet.",
|
||||
"open": "Open",
|
||||
"noUrl": "no public URL",
|
||||
"teardown": "Tear down",
|
||||
"teardownTitle": "Tear down preview?",
|
||||
"teardownMessage": "This deletes the preview for branch \"{name}\" and removes its containers and proxy routes. Pushing to the branch again will recreate it.",
|
||||
"teardownConfirm": "Tear down",
|
||||
"teardownPending": "Tearing down…",
|
||||
"teardownFailed": "Teardown failed",
|
||||
"stateRunning": "Running",
|
||||
"statePending": "Pending",
|
||||
"stateStopped": "Stopped",
|
||||
"stateUnknown": "Unknown",
|
||||
"hint": "Previews are created automatically when a push lands on a branch matching a git trigger's <code>branch_pattern</code>, and torn down when the branch is deleted. Each gets its own slug-prefixed subdomain."
|
||||
},
|
||||
"volumesTitle": "Volumes",
|
||||
"volumesEmpty": "No mounts",
|
||||
"volumesCountSingular": "{count} mount",
|
||||
|
||||
Reference in New Issue
Block a user