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
21 changes: 21 additions & 0 deletions .github/workflows/validate-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ on:
schedule:
- cron: "0 7 * * *"

env:
INTERFACECTL_CI_REF: 1f2377c28ae8349250afae04408309d1d551d4e3

jobs:
validate:
name: Validate Contract
Expand All @@ -32,6 +35,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: Surfaces-Platform/interfacectl
ref: ${{ env.INTERFACECTL_CI_REF }}
path: interfacectl
token: ${{ secrets.INTERFACECTL_TOKEN || github.token }}

Expand Down Expand Up @@ -123,12 +127,22 @@ jobs:
permissions:
contents: read
pull-requests: read
env:
SURFACES_INTERFACECTL_ROOT: ${{ github.workspace }}/interfacectl
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.INTERFACECTL_TOKEN || github.token }}

- name: Checkout interfacectl
uses: actions/checkout@v4
with:
repository: Surfaces-Platform/interfacectl
ref: ${{ env.INTERFACECTL_CI_REF }}
path: interfacectl
token: ${{ secrets.INTERFACECTL_TOKEN || github.token }}

- name: Setup pnpm
uses: pnpm/action-setup@v4

Expand All @@ -141,9 +155,16 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build interfacectl
run: pnpm run interfacectl:build

- name: Build platform auth package
run: pnpm --filter @surfaces/platform-auth build

- name: Install interfacectl Playwright browser
working-directory: interfacectl
run: pnpm --filter @surfaces/interfacectl-cli exec playwright install chromium

- name: Install Playwright browser
run: pnpm exec playwright install --with-deps chromium

Expand Down
3 changes: 2 additions & 1 deletion apps/surfaceops-web/app/api/onboarding/extract/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ export async function POST(request: Request) {
);
}

const result = runOnboardingExtraction({
const result = await runOnboardingExtraction({
url: payload.url,
appRoot: payload.appRoot,
surfaceName: payload.surfaceName,
surfaceId: payload.surfaceId,
authMode: payload.authMode === "browser-session" ? "browser-session" : "none",
Expand Down
113 changes: 64 additions & 49 deletions apps/surfaceops-web/app/contracts/_components/onboarding-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function OnboardingForm() {
const [state, setState] = useState<OnboardingState>("idle");
const [error, setError] = useState<string | null>(null);
const [url, setUrl] = useState("");
const [appRoot, setAppRoot] = useState("");
const [surfaceName, setSurfaceName] = useState("");
const [surfaceId, setSurfaceId] = useState("");
const [authMode, setAuthMode] = useState<"none" | "browser-session">("none");
Expand All @@ -25,22 +26,24 @@ export function OnboardingForm() {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
url,
url: extractMode === "remote-url" ? url : undefined,
appRoot: extractMode === "local-root" ? appRoot : undefined,
surfaceName: surfaceName.trim() || undefined,
surfaceId: surfaceId.trim() || undefined,
authMode,
authMode: extractMode === "remote-url" ? authMode : "none",
extractMode,
}),
});
const data = (await response.json()) as {
state?: string;
surfaceId?: string;
recommendedRoute?: string;
error?: string;
};
if (!response.ok || data.state !== "completed" || !data.surfaceId) {
throw new Error(data.error || "Onboarding failed.");
}
router.push(`/contracts/runs/${data.surfaceId}`);
router.push(data.recommendedRoute || "/contracts/governance");
router.refresh();
} catch (submissionError) {
setState("failed");
Expand All @@ -58,19 +61,47 @@ export function OnboardingForm() {
padding: "20px",
}}
>
<div style={{ display: "grid", gap: "12px", marginBottom: "12px" }}>
<div>
<label style={{ fontSize: "12px", color: colors.neutral[600], marginBottom: "6px", display: "block" }}>
Source
</label>
<select
value={extractMode}
onChange={(event) => setExtractMode(event.target.value as "remote-url" | "local-root")}
style={{
width: "100%",
borderRadius: "8px",
border: `1px solid ${colors.neutral[200]}`,
padding: "8px 10px",
fontSize: "13px",
backgroundColor: colors.neutral[0],
}}
>
<option value="remote-url">Inspect a live URL</option>
<option value="local-root">Inspect a local app root</option>
</select>
</div>
</div>
<label
htmlFor="onboarding-url"
htmlFor={extractMode === "remote-url" ? "onboarding-url" : "onboarding-app-root"}
style={{ display: "block", fontSize: "13px", fontWeight: 600, marginBottom: "8px" }}
>
Surface URL
{extractMode === "remote-url" ? "Surface URL" : "Local app root"}
</label>
<input
id="onboarding-url"
type="url"
id={extractMode === "remote-url" ? "onboarding-url" : "onboarding-app-root"}
type={extractMode === "remote-url" ? "url" : "text"}
required
placeholder="https://customer.example.com"
value={url}
onChange={(event) => setUrl(event.target.value)}
placeholder={extractMode === "remote-url" ? "https://customer.example.com" : "apps/reference-target-web"}
value={extractMode === "remote-url" ? url : appRoot}
onChange={(event) => {
if (extractMode === "remote-url") {
setUrl(event.target.value);
return;
}
setAppRoot(event.target.value);
}}
style={{
width: "100%",
borderRadius: "8px",
Expand Down Expand Up @@ -112,44 +143,28 @@ export function OnboardingForm() {
padding: "8px 10px",
fontSize: "13px",
}}
/>
</div>
<div>
<label style={{ fontSize: "12px", color: colors.neutral[600] }}>Auth mode</label>
<select
value={authMode}
onChange={(event) => setAuthMode(event.target.value as "none" | "browser-session")}
style={{
width: "100%",
borderRadius: "8px",
border: `1px solid ${colors.neutral[200]}`,
padding: "8px 10px",
fontSize: "13px",
backgroundColor: colors.neutral[0],
}}
>
<option value="none">none</option>
<option value="browser-session">browser-session</option>
</select>
</div>
<div>
<label style={{ fontSize: "12px", color: colors.neutral[600] }}>Extraction mode</label>
<select
value={extractMode}
onChange={(event) => setExtractMode(event.target.value as "remote-url" | "local-root")}
style={{
width: "100%",
borderRadius: "8px",
border: `1px solid ${colors.neutral[200]}`,
padding: "8px 10px",
fontSize: "13px",
backgroundColor: colors.neutral[0],
}}
>
<option value="remote-url">remote-url</option>
<option value="local-root">local-root</option>
</select>
</div>
/>
</div>
{extractMode === "remote-url" && (
<div>
<label style={{ fontSize: "12px", color: colors.neutral[600] }}>Auth mode</label>
<select
value={authMode}
onChange={(event) => setAuthMode(event.target.value as "none" | "browser-session")}
style={{
width: "100%",
borderRadius: "8px",
border: `1px solid ${colors.neutral[200]}`,
padding: "8px 10px",
fontSize: "13px",
backgroundColor: colors.neutral[0],
}}
>
<option value="none">none</option>
<option value="browser-session">browser-session</option>
</select>
</div>
)}
</div>
</details>

Expand All @@ -172,7 +187,7 @@ export function OnboardingForm() {
cursor: state === "submitting" ? "wait" : "pointer",
}}
>
{state === "submitting" ? "Generating..." : "Generate First Contract"}
{state === "submitting" ? "Bootstrapping UI AST..." : "Create Governed Surface"}
</button>
</form>
);
Expand Down
Loading
Loading