Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ _Avoid_: Public Address name, domain name label, port alias.

### Default Open Port

The App Listening Port that the Open control opens — on the AP Public Access Node header and in the pane header of every AP-owned Settings View — through its best Public Address: an accessible Custom Domain, else an accessible Platform Address; with neither, Open is shown disabled, with the reason. The node's control names the port ("Open <Port Display Name>"), its only marker there; the pane control reads just "Open" and names the port only on hover, since the App Listening Ports card beside it already shows the choice. A stored choice lives only on the AP's Service (annotation `brain.io/default-open-port`, next to Port Display Names), so a template can preset it; users set or clear it from the port's row in the App Listening Ports card ("Open by default"), and clearing returns to the automatic rule: the first App Listening Port, in declaration order, whose HTTP Public Address enters at the root; with none at the root, the first that has any HTTP Public Address — so a backend port routed under `/api` yields to the page port however the template ordered them. A stored port that has no HTTP Public Address is ignored, not surfaced. Ports reached only by WS/WSS Public Addresses are never chosen. Owned by one AP; there is no Project-level or Template-Instance-level open link.
The App Listening Port that the Open control opens — on the AP Public Access Node header and in the pane header of every AP-owned Settings View — through its best Public Address: an accessible Custom Domain, else an accessible Platform Address; with neither, Open is shown disabled, with the reason. The node's control names the port ("Open <Port Display Name>"), its only marker there; the pane control reads just "Open" and names the port only on hover, since the App Listening Ports card beside it already shows the choice. A stored choice lives only on the AP's Service (annotation `brain.io/default-open-port`, next to Port Display Names), so a template can preset it; users set or clear it from the port's row in the App Listening Ports card ("Open by default"), and clearing returns to the automatic rule: the first App Listening Port, in declaration order, whose HTTP Public Address enters at the root; with none at the root, the first that has any HTTP Public Address — so a backend port routed under `/api` yields to the page port however the template ordered them. A stored port that has no HTTP Public Address is ignored, not surfaced. Ports reached only by WS/WSS Public Addresses are never chosen. Owned by one AP. A template presets it through its Template Entries: at render time — or, for a template the provider applied, when Brain reads the created Ingresses back — Brain follows the Open Entry — the declared one, else the template's Sealos App CR url — through the Ingress rule whose host and longest-prefix path serve it to the Service port behind it, and writes the annotation there unless the template already set it, so the node's Open and the Deployment Task Success Record's Open agree; no match writes nothing (ADR 0081).

_Avoid_: primary entry, primary port, primary address, launch link, main domain.
_Avoid_: primary entry, primary port, primary address, launch link, main domain, Project open link, Template Instance open link.

### Private Address

Expand Down Expand Up @@ -354,10 +354,16 @@ _Avoid_: applied object, Kubernetes object.

### Deployment Access Endpoint

A source-independent Deployment Result Resource describing one user-facing way to reach a deployed product. It has a stable task-local identity, explicit HTTP or WebSocket protocol, a provider observer or declared URL, and an independently verified readiness state. Docker AP addresses, Template Ingress hosts, and GitHub Agent-declared URLs all converge on this contract. The observer resolves the provider's actual address; Brain never reconstructs an address or infers WSS from HTTPS. Once verified, an endpoint that reaches an App Listening Port of one of the task's APs — an AP Public Address, or a Template Ingress host the AP observed — is named by that port's Port Display Name form; an Agent-declared URL keeps its declared label.
A source-independent Deployment Result Resource describing one user-facing way to reach a deployed product. It has a stable task-local identity, explicit HTTP or WebSocket protocol, a provider observer or declared URL, and an independently verified readiness state. Docker AP addresses, Template Ingress hosts, and GitHub Agent-declared URLs all converge on this contract; a Template Entry does not — it is taken as declared on a host these endpoints already verified. The observer resolves the provider's actual address; Brain never reconstructs an address or infers WSS from HTTPS. Once verified, an endpoint that reaches an App Listening Port of one of the task's APs — an AP Public Address, or a Template Ingress host the AP observed — is named by that port's Port Display Name form; an Agent-declared URL keeps its declared label.

_Avoid_: guessed URL, inferred socket address, source-specific public access card.

### Template Entry

A URL a Sealos Template declares in its header (`spec.entries`) for one of two roles, each a full URL rendered with the template's own `${{ }}` substitution: the Open Entry is what the Open control opens after the deployment, and the Share Entry is what the Deployment Task Success Record's share strip shares. When a template declares no Open Entry, its Sealos App CR url (`spec.data.url`) stands in; with neither, the Default Open Port's automatic rule decides. The Share Entry has one fallback only, the Open URL when it carries no fragment — never the App CR url, which may embed a secret (`#token=…`), and never a fragment-bearing Open, which the record opens but the strip skips for its next verified HTTP(S) entry. An entry is kept only when an Ingress of the same deployment serves its host, and that host is a Deployment Access Endpoint whose probe already gates the record — so a Template Entry is not itself probed and is taken as declared, query string and all: whether the application answers on its path is not routing health, exactly as AP Public Access Health has it. The Open Entry is the record's first entry, headed by the App Listening Port it reaches when an AP observed that address; the Share Entry is the record's share address and is never listed as an entry. Only an HTTP(S) Share Entry is shared: a socket address (`wss://`) cannot be opened from a link or a QR code, so it yields to the Open URL as if no Share Entry were declared. The Open Entry also presets the AP's Default Open Port (ADR 0081).

_Avoid_: primary entry, launch link, main domain, share project, share deployment, App CR link (as the product term), entry point (for the field).

### Deployment Result Readiness

The condition where a task's user-visible result resources have become healthy enough for the task to count as complete — distinct from having applied Deployment Artifacts. Raw Kubernetes resources use one task-facing predicate per Kind across deterministic and Agent-managed runners: replica controllers require their Ready counts, Pods and Jobs require their Ready/Complete conditions, and a non-suspended CronJob is ready without waiting for a scheduled execution.
Expand All @@ -380,7 +386,7 @@ A Deployment Task Timeline section for one Deployment Result Resource, presentin

### Deployment Task Success Record

The conclusion a Deployment Task Timeline appends once Deployment Result Readiness is reached and every required access endpoint has passed its protocol probe. It carries only facts the deployment declared — product name, verified HTTP or WebSocket entries, first-use steps — so the Timeline never presents an address or instruction the runner cannot evidence. Each entry is headed the way a Public Address is shown everywhere else: the Port Display Name form of the App Listening Port it reaches (`game · 5200`, or `5200` alone for an unnamed port), as it stood at verification time; an entry no App Listening Port can be found for keeps the name its source declared. Two entries reaching the same port carry the same heading; the record stays a flat list. A record with a single entry heads it with nothing, whatever heading its source would have given it, as the node draws a lone Public Address. HTTP(S) entries can be opened and copied; WS(S) entries are copied. Its Open control opens the Default Open Port through its best Public Address as decided when the record was written — the record is a snapshot, so a later rename or Default Open Port change does not rewrite it. A verified deployment with no endpoint uses the neutral `Deployment completed` headline, while `You can start using it` is reserved for a verified actionable entry. A task with no required Deployment Result Resource publishes no record and keeps reporting progress. It is part of the task-owned timeline snapshot, not a Chat message or a toast, and its Timeline revision doubles as its identity. Its primary HTTP(S) entry may be shared — copied, shown as a QR code, or posted to a social network — as the product's own public address; this shares nothing of Brain and is not Public Project Preview Sharing, which no longer exists. Its first-use steps appear under the user-facing heading `Next steps`, and only when the deployment declared them; a record without declared steps shows no heading. A record written for a template deployment also snapshots the template's catalog name as its product id and the template's declared categories (`game`, `ai`, …) as they stood when the task was created, so the share copy can speak to a game or an AI app without asking the catalog again; other sources declare neither.
The conclusion a Deployment Task Timeline appends once Deployment Result Readiness is reached and every required access endpoint has passed its protocol probe. It carries only facts the deployment declared — product name, verified HTTP or WebSocket entries, the template's Open Entry on a host those entries verified, first-use steps — so the Timeline never presents an address or instruction the runner cannot evidence. Each entry is headed the way a Public Address is shown everywhere else: the Port Display Name form of the App Listening Port it reaches (`game · 5200`, or `5200` alone for an unnamed port), as it stood at verification time; an entry no App Listening Port can be found for keeps the name its source declared. Two entries reaching the same port carry the same heading; the record stays a flat list. A record with a single entry heads it with nothing, whatever heading its source would have given it, as the node draws a lone Public Address. HTTP(S) entries can be opened and copied; WS(S) entries are copied. Its Open control opens the template's Open Entry when there is one, else the Default Open Port through its best Public Address, as decided when the record was written — the record is a snapshot, so a later rename or Default Open Port change does not rewrite it. A verified deployment with no endpoint uses the neutral `Deployment completed` headline, while `You can start using it` is reserved for a verified actionable entry. A task with no required Deployment Result Resource publishes no record and keeps reporting progress. It is part of the task-owned timeline snapshot, not a Chat message or a toast, and its Timeline revision doubles as its identity. Its share address — the template's Share Entry, else its Open URL when fragment-free, else its next verified HTTP(S) entry, snapshotted as `shareUrl` (a record written before the field shares its primary fragment-free HTTP(S) entry) — may be shared: copied, shown as a QR code, or posted to a social network as the product's own public address; this shares nothing of Brain and is not Public Project Preview Sharing, which no longer exists. Its first-use steps appear under the user-facing heading `Next steps`, and only when the deployment declared them; a record without declared steps shows no heading. A record written for a template deployment also snapshots the template's catalog name as its product id and the template's declared categories (`game`, `ai`, …) as they stood when the task was created, so the share copy can speak to a game or an AI app without asking the catalog again; other sources declare neither.

_Avoid_: success toast, deploy done banner, completion notification, "Public address" as an entry heading, share project, share deployment.

Expand Down
17 changes: 11 additions & 6 deletions apps/ui/src/features/deploy/deployment-task-success-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { Check, Copy, ExternalLink } from "lucide-react";
import { memo, useEffect, useRef } from "react";
import { prefersReducedMotion } from "@/features/deploy/deployment-task-success-confetti";
import { DeploymentTaskSuccessShareStrip } from "@/features/deploy/deployment-task-success-share";
import type {
DeploymentTaskSuccessEntry,
DeploymentTaskSuccessSnapshot,
DeploymentTaskSuccessStep,
import {
type DeploymentTaskSuccessEntry,
type DeploymentTaskSuccessSnapshot,
type DeploymentTaskSuccessStep,
deploymentTaskSuccessShareUrl,
} from "@/features/deploy/task/timeline";
import { useCopyFeedback } from "@/features/deploy/use-copy-feedback";

Expand Down Expand Up @@ -248,6 +249,10 @@ export const DeploymentTaskSuccessSection = memo(
const guidance = success.guidance ?? [];
const primaryEntry = entries.find(isOpenableEntry);
const secondaryEntries = entries.filter((entry) => entry !== primaryEntry);
// What the strip shares: the record's snapshotted share address (a
// template's Share entry, else the Open URL at verification time), with
// the primary entry standing in for records written before it existed.
const shareUrl = deploymentTaskSuccessShareUrl(success);
// A lone entry is headed by nothing, whatever heading its source gave it,
// as the Public Access Node draws a lone Public Address (CONTEXT.md,
// Deployment Task Success Record).
Expand Down Expand Up @@ -347,14 +352,14 @@ export const DeploymentTaskSuccessSection = memo(
</div>
)}
</div>
{primaryEntry == null ? null : (
{shareUrl == null ? null : (
<DeploymentTaskSuccessShareStrip
className={cn(RISE_CLASS, "mt-2 delay-[420ms]")}
subject={{
productCategories: success.productCategories,
productId: success.productId,
productName: success.productName,
url: primaryEntry.url,
url: shareUrl,
}}
/>
)}
Expand Down
15 changes: 9 additions & 6 deletions apps/ui/src/features/deploy/task/ap-network-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,18 @@ describe("AP network view", () => {
expect(
apNetworkViewAddressForUrl(parsed, "wss://shared.example.com/")?.port
).toBe(5200);
// Same scheme, unknown path: the scheme still picks the row.
// Same host, another path or scheme: no row reaches that address, so a
// Success Record entry for it is headed by nothing rather than by the
// wrong port.
expect(
apNetworkViewAddressForUrl(parsed, "https://shared.example.com/other")
?.port
).toBe(8081);
// Unknown scheme falls back to the host alone.
).toBe(undefined);
expect(
apNetworkViewAddressForUrl(parsed, "http://shared.example.com/")?.port
).toBe(5200);
apNetworkViewAddressForUrl(parsed, "https://shared.example.com/")
).toBe(undefined);
expect(
apNetworkViewAddressForUrl(parsed, "http://shared.example.com/")
).toBe(undefined);
expect(apNetworkViewAddressForUrl(parsed, "https://nobody.example/")).toBe(
undefined
);
Expand Down
25 changes: 12 additions & 13 deletions apps/ui/src/features/deploy/task/ap-network-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,10 @@ function parseEndpointUrl(raw: string | undefined): ParsedEndpointUrl | null {
* The address row an endpoint URL reaches. One hostname may expose several
* Public Addresses that differ by protocol or path and target different
* App Listening Ports (ADR 0079: a `wss://` game port beside an `https://`
* admin path), so the URL is matched whole first — scheme, host, and entry
* path — then by scheme and host, and only then by host alone.
* admin path), so the URL is matched whole — scheme, host, and entry path —
* and nothing looser: an entry is headed by the port its address reaches,
* else by nothing (CONTEXT.md: Deployment Task Success Record), so a
* same-host row for another port must never stand in.
*/
export function apNetworkViewAddressForUrl(
view: ApNetworkView,
Expand All @@ -206,17 +208,14 @@ export function apNetworkViewAddressForUrl(
if (wanted === null) {
return undefined;
}
const candidates = view.addresses
.filter((address) => address.host === wanted.host)
.map((address) => ({ address, parsed: parseEndpointUrl(address.url) }));
const exact = candidates.find(
({ parsed }) =>
parsed?.scheme === wanted.scheme && parsed.path === wanted.path
);
const sameScheme = candidates.find(
({ parsed }) => parsed?.scheme === wanted.scheme
);
return (exact ?? sameScheme ?? candidates[0])?.address;
return view.addresses.find((address) => {
const parsed = parseEndpointUrl(address.url);
return (
address.host === wanted.host &&
parsed?.scheme === wanted.scheme &&
parsed.path === wanted.path
);
});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/features/deploy/task/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import YAML from "yaml";

import { childResourceName } from "@/features/deploy/project-child-resource-name";
import { joinKubeYamlDocuments } from "@/features/deploy/render-yaml-template";
import { templateHeaderFromInlineYaml } from "@/features/deploy/template-inline-yaml";
import {
evaluateTemplateCondition,
type RenderedTemplateDeployment,
renderTemplateDeploymentFromYaml,
resolveTemplateDeclarationState,
type TemplateEvaluationContext,
templateHeaderFromInlineYaml,
templateSourceFromInlineYaml,
} from "@/features/deploy/template-renderer";

Expand Down
18 changes: 9 additions & 9 deletions apps/ui/src/features/deploy/task/result-readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,15 @@ export function deploymentResultApCandidates(
}

/**
* The label an Ingress-observed endpoint carries once verified: the Port
* Display Name form of the App Listening Port behind that endpoint, read
* from the first candidate AP whose Product View observed its host. The
* endpoint is matched by its whole URL, since one host may expose several
* ports by protocol and path. A host no AP claims — a template with no
* AP-like workload, or a view that cannot be read — keeps the label the
* Ingress gave it; naming is never invented here.
* The label an Ingress-observed endpoint carries once verified — and the one
* a template's Open Entry is headed by in the Success Record: the Port
* Display Name form of the App Listening Port behind that URL, read from
* the first candidate AP whose Product View observed its host. The URL is
* matched whole, since one host may expose several ports by protocol and
* path. A host no AP claims — a template with no AP-like workload, or a view
* that cannot be read — yields nothing; naming is never invented here.
*/
async function ingressAccessEndpointPortLabel(input: {
export async function accessEndpointPortLabelForUrl(input: {
candidates: readonly DeploymentResultApCandidate[];
kubeconfig: string;
signal?: AbortSignal;
Expand Down Expand Up @@ -491,7 +491,7 @@ async function accessEndpointReadiness(
input.apCandidates != null &&
input.apCandidates.length > 0
) {
portLabel = await ingressAccessEndpointPortLabel({
portLabel = await accessEndpointPortLabelForUrl({
candidates: input.apCandidates,
kubeconfig: input.kubeconfig,
signal: input.signal,
Expand Down
Loading