Skip to content

Commit 262943d

Browse files
jherforthclaude
andcommitted
Deploy UX: honest port field, rocket on the ship chip, real deploy door
Field-found trap: nginx-served builds always listen on container port 80, but the wizard's Port field (default 3000) looked authoritative — producing a mapping that targeted a port nothing listened on (502). The preview now reports the container_port the deploy will actually use, and the wizard says plainly when the App Port entry is ignored and what the mapping will target. Discoverability: the Source & Ship chip gains a rocket (it handles deployments, not just commits) and its tooltip says so; the welcome screen's Deploy card now opens the Source & Ship drawer instead of sending a chat message pretending to deploy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9f56ffb commit 262943d

5 files changed

Lines changed: 40 additions & 6 deletions

File tree

crates/harness-api/src/handlers.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3332,12 +3332,23 @@ pub async fn preview_deploy(
33323332
}));
33333333
}
33343334

3335+
// The port the deployed CONTAINER will actually listen on — same rule the Coolify deploy
3336+
// uses (generate_clone_dockerfile): nginx-served frameworks are always 80 regardless of the
3337+
// wizard's Port field; Node-server frameworks use the requested port. Surfacing this in the
3338+
// preview is what lets the wizard warn when the user's Port entry will be ignored — the
3339+
// field-found failure mode was a port mapping targeting :3000 while nginx served :80.
3340+
let container_port: u16 = match framework.as_str() {
3341+
"nextjs" | "express" | "fastify" | "node" => port,
3342+
_ => 80, // vite-react | vue | react | static | unknown → nginx
3343+
};
3344+
33353345
Ok(Json(serde_json::json!({
33363346
"framework": framework,
33373347
"build_command": build_cmd,
33383348
"output_dir": output_dir,
33393349
"default_port": default_port,
33403350
"port": port,
3351+
"container_port": container_port,
33413352
"app_name": app_name,
33423353
"files": files,
33433354
})))

packages/web-ui/src/components/ChatPane.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,12 +458,14 @@ export function ChatPane({
458458
<div className="font-medium text-monastery-text-primary mb-0.5">Testing</div>
459459
Add tests to the existing module
460460
</button>
461-
<button
462-
onClick={() => onSendMessage('Deploy this project to my homelab server')}
461+
<button
462+
// Deploying is a real flow (Source & Ship → wizard), not a chat request —
463+
// point first-time users at the actual door.
464+
onClick={() => window.dispatchEvent(new CustomEvent('monastery:open-source-ship'))}
463465
className="p-3 bg-monastery-dark-bg rounded-xl text-xs text-monastery-text-secondary hover:bg-monastery-dark-tertiary hover:text-monastery-text-primary transition-all border border-monastery-dark-border hover:border-monastery-pine text-left"
464466
>
465-
<div className="font-medium text-monastery-text-primary mb-0.5">Deploy</div>
466-
Deploy this to my homelab
467+
<div className="font-medium text-monastery-text-primary mb-0.5">Deploy 🚀</div>
468+
Ship this to your homelab (Source & Ship)
467469
</button>
468470
</div>
469471

packages/web-ui/src/components/SelfHostWizard.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,24 @@ export function SelfHostWizard({ isOpen, onClose, onFixBuildError }: SelfHostWiz
305305
<div className="flex-1"><label className="block text-xs font-medium text-monastery-text-secondary mb-1">Domain (optional)</label>
306306
<input type="text" value={domain} onChange={e => setDomain(e.target.value)} placeholder="app.yourdomain.com"
307307
className="w-full px-3 py-2 bg-monastery-dark-bg border border-monastery-dark-border rounded-lg text-monastery-text-primary text-sm placeholder-monastery-text-muted focus:border-monastery-pine focus:outline-none" /></div>
308-
<div className="w-24"><label className="block text-xs font-medium text-monastery-text-secondary mb-1">Port</label>
308+
<div className="w-24"><label className="block text-xs font-medium text-monastery-text-secondary mb-1">App Port</label>
309309
<input type="number" value={port} onChange={e => setPort(e.target.value)}
310310
className="w-full px-3 py-2 bg-monastery-dark-bg border border-monastery-dark-border rounded-lg text-monastery-text-primary text-sm focus:border-monastery-pine focus:outline-none" /></div></div>
311+
{/* The Port field is a classic trap: nginx-served builds always listen on 80, so a
312+
mismatched entry used to produce a port mapping pointing at nothing. Say which
313+
port will really be used, up front. */}
314+
{preview?.container_port !== undefined && (
315+
String(preview.container_port) !== port ? (
316+
<p className="text-xs text-amber-300 flex items-start gap-1 -mt-2">
317+
<AlertTriangle size={11} className="shrink-0 mt-0.5" />
318+
Detected <b>{preview.framework}</b> — the container serves on port <b>{preview.container_port}</b> (nginx serves the built site), so the App Port field is ignored and the published mapping targets {preview.container_port}.
319+
</p>
320+
) : (
321+
<p className="text-xs text-monastery-text-muted -mt-2">
322+
Container listens on port {preview.container_port}; it will be published on a stable high port on the server (shown after deploy).
323+
</p>
324+
)
325+
)}
311326
{dokploy && coolify && (<div><label className="block text-xs font-medium text-monastery-text-secondary mb-1">Platform</label>
312327
<select value={selectedPlatform || (dokploy ? 'dokploy' : 'coolify')} onChange={e => setSelectedPlatform(e.target.value)}
313328
className="w-full px-3 py-2 bg-monastery-dark-bg border border-monastery-dark-border rounded-lg text-monastery-text-primary text-sm focus:border-monastery-pine focus:outline-none">

packages/web-ui/src/components/TopBar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export function TopBar({ availableProjects = [], endpoints = [], availableModels
393393
<button
394394
onClick={() => window.dispatchEvent(new CustomEvent('monastery:open-source-ship'))}
395395
className="flex items-center gap-1.5 px-2.5 py-1.5 bg-monastery-dark-surface rounded-lg border border-monastery-dark-border hover:border-monastery-pine transition-colors"
396-
title={`Source & Ship — branch ${gitStatus.branch}, ${gitStatus.changed_files.length} changed, ${gitStatus.ahead} ahead / ${gitStatus.behind} behind`}
396+
title={`Source & Ship — pull, commit & push, snapshots, and DEPLOY. Branch ${gitStatus.branch}, ${gitStatus.changed_files.length} changed, ${gitStatus.ahead} ahead / ${gitStatus.behind} behind`}
397397
>
398398
<GitBranch size={14} className={gitStatus.is_clean ? 'text-green-400' : 'text-amber-400'} />
399399
<span className="text-xs text-monastery-text-secondary">
@@ -414,6 +414,9 @@ export function TopBar({ availableProjects = [], endpoints = [], availableModels
414414
<ArrowDown size={10} />{gitStatus.behind}
415415
</span>
416416
)}
417+
{/* Deployment lives behind this chip too — the rocket says so. */}
418+
<span className="h-3.5 w-px bg-monastery-dark-border" aria-hidden />
419+
<Rocket size={12} className="text-monastery-lantern" />
417420
<ChevronDown size={12} className="text-monastery-text-muted" />
418421
</button>
419422
) : (

packages/web-ui/src/hooks/useHostingServices.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export interface PreviewResult {
7575
output_dir: string;
7676
default_port: number;
7777
port: number;
78+
/** The port the deployed container will ACTUALLY listen on (nginx-served frameworks are
79+
* always 80; the wizard's Port field only applies to Node-server frameworks). */
80+
container_port?: number;
7881
app_name: string;
7982
files: Array<{
8083
name: string;

0 commit comments

Comments
 (0)