feat(build): UI surfaces richer discovery + stack settings + Apple/Xcode Cloud + CSS fixes

- ui/src/build-config.ts: surface suggested_stack, distro, git ref/tag/SHA,
  Linux packages, computed build options, stack-specific settings
  (build_tags, obfuscation, cache, Deno, NSIS, WebView2, Docker/LinuxKit),
  Apple/Xcode Cloud config
- CSS fixes: colour → color across build-config/artifacts/panel/sdk .ts
- pkg/api/ui/dist/core-build.js: rebuilt embedded bundle

Verified: npm run build + go test ./... passes

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-14 23:18:28 +01:00
parent 1f06189eff
commit 116a556c2f
5 changed files with 1282 additions and 652 deletions

File diff suppressed because it is too large Load diff

View file

@ -31,13 +31,13 @@ export class BuildArtifacts extends LitElement {
.toolbar-info {
font-size: 0.8125rem;
colour: #6b7280;
color: #6b7280;
}
button.build {
padding: 0.5rem 1.25rem;
background: #6366f1;
colour: #fff;
color: #fff;
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
@ -69,13 +69,13 @@ export class BuildArtifacts extends LitElement {
.confirm-text {
flex: 1;
colour: #92400e;
color: #92400e;
}
button.confirm-yes {
padding: 0.375rem 1rem;
background: #dc2626;
colour: #fff;
color: #fff;
border: none;
border-radius: 0.375rem;
font-size: 0.8125rem;
@ -115,29 +115,29 @@ export class BuildArtifacts extends LitElement {
font-size: 0.875rem;
font-family: monospace;
font-weight: 500;
colour: #111827;
color: #111827;
}
.artifact-size {
font-size: 0.75rem;
colour: #6b7280;
color: #6b7280;
}
.empty {
text-align: center;
padding: 2rem;
colour: #9ca3af;
color: #9ca3af;
font-size: 0.875rem;
}
.loading {
text-align: center;
padding: 2rem;
colour: #6b7280;
color: #6b7280;
}
.error {
colour: #dc2626;
color: #dc2626;
padding: 0.75rem;
background: #fef2f2;
border-radius: 0.375rem;
@ -151,7 +151,7 @@ export class BuildArtifacts extends LitElement {
border: 1px solid #bbf7d0;
border-radius: 0.375rem;
font-size: 0.875rem;
colour: #166534;
color: #166534;
margin-bottom: 1rem;
}
`;

View file

@ -9,6 +9,50 @@ interface TargetConfig {
arch: string;
}
interface CacheConfigData {
enabled?: boolean;
path?: string;
paths?: string[];
}
interface AppleTriggerData {
branch?: string;
tag?: string;
action?: string;
}
interface AppleConfigData {
team_id?: string;
bundle_id?: string;
arch?: string;
cert_identity?: string;
profile_path?: string;
keychain_path?: string;
metadata_path?: string;
sign?: boolean;
notarise?: boolean;
dmg?: boolean;
testflight?: boolean;
appstore?: boolean;
api_key_id?: string;
api_key_issuer_id?: string;
api_key_path?: string;
apple_id?: string;
password?: string;
bundle_display_name?: string;
min_system_version?: string;
category?: string;
copyright?: string;
privacy_policy_url?: string;
dmg_background?: string;
dmg_volume_name?: string;
entitlements_path?: string;
xcode_cloud?: {
workflow?: string;
triggers?: AppleTriggerData[];
};
}
interface BuildConfigData {
config: {
version: number;
@ -21,11 +65,27 @@ interface BuildConfigData {
build: {
type: string;
cgo: boolean;
obfuscate?: boolean;
deno_build?: string;
nsis?: boolean;
webview2?: string;
flags: string[];
ldflags: string[];
build_tags?: string[];
archive_format?: string;
env: string[];
cache?: CacheConfigData;
dockerfile?: string;
registry?: string;
image?: string;
tags?: string[];
push?: boolean;
load?: boolean;
linuxkit_config?: string;
formats?: string[];
};
targets: TargetConfig[];
apple?: AppleConfigData;
sign: any;
};
has_config: boolean;
@ -39,8 +99,21 @@ interface DiscoverData {
suggested_stack?: string;
dir: string;
has_frontend?: boolean;
distro?: string;
ref?: string;
branch?: string;
tag?: string;
short_sha?: string;
has_subtree_npm?: boolean;
linux_packages?: string[];
build_options?: string;
options?: {
obfuscate?: boolean;
tags?: string[];
nsis?: boolean;
webview2?: string;
ldflags?: string[];
};
}
/**
@ -65,7 +138,7 @@ export class BuildConfig extends LitElement {
.section-title {
font-size: 0.75rem;
font-weight: 700;
colour: #6b7280;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.025em;
margin-bottom: 0.75rem;
@ -86,13 +159,16 @@ export class BuildConfig extends LitElement {
.field-label {
font-size: 0.8125rem;
font-weight: 500;
colour: #374151;
color: #374151;
}
.field-value {
font-size: 0.8125rem;
font-family: monospace;
colour: #6b7280;
color: #6b7280;
max-width: 36rem;
text-align: right;
word-break: break-word;
}
.badge {
@ -105,37 +181,37 @@ export class BuildConfig extends LitElement {
.badge.present {
background: #dcfce7;
colour: #166534;
color: #166534;
}
.badge.absent {
background: #fef3c7;
colour: #92400e;
color: #92400e;
}
.badge.type-go {
background: #dbeafe;
colour: #1e40af;
color: #1e40af;
}
.badge.type-wails {
background: #f3e8ff;
colour: #6b21a8;
color: #6b21a8;
}
.badge.type-node {
background: #dcfce7;
colour: #166534;
color: #166534;
}
.badge.type-php {
background: #fef3c7;
colour: #92400e;
color: #92400e;
}
.badge.type-docker {
background: #e0e7ff;
colour: #3730a3;
color: #3730a3;
}
.targets {
@ -151,7 +227,7 @@ export class BuildConfig extends LitElement {
background: #f3f4f6;
border-radius: 0.25rem;
font-family: monospace;
colour: #374151;
color: #374151;
}
.flags {
@ -167,24 +243,24 @@ export class BuildConfig extends LitElement {
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
font-family: monospace;
colour: #6b7280;
color: #6b7280;
}
.empty {
text-align: center;
padding: 2rem;
colour: #9ca3af;
color: #9ca3af;
font-size: 0.875rem;
}
.loading {
text-align: center;
padding: 2rem;
colour: #6b7280;
color: #6b7280;
}
.error {
colour: #dc2626;
color: #dc2626;
padding: 0.75rem;
background: #fef2f2;
border-radius: 0.375rem;
@ -224,6 +300,58 @@ export class BuildConfig extends LitElement {
}
}
private hasAppleConfig(apple?: AppleConfigData): boolean {
if (!apple) {
return false;
}
return Object.entries(apple).some(([, value]) => {
if (value == null) {
return false;
}
if (Array.isArray(value)) {
return value.length > 0;
}
if (typeof value === 'object') {
return Object.keys(value).length > 0;
}
if (typeof value === 'string') {
return value.length > 0;
}
return true;
});
}
private renderToggle(label: string, enabled: boolean | undefined, onLabel = 'Enabled', offLabel = 'Disabled') {
if (enabled == null) {
return nothing;
}
return html`
<div class="field">
<span class="field-label">${label}</span>
<span class="badge ${enabled ? 'present' : 'absent'}">
${enabled ? onLabel : offLabel}
</span>
</div>
`;
}
private renderFlags(label: string, values?: string[]) {
if (!values || values.length === 0) {
return nothing;
}
return html`
<div class="field">
<span class="field-label">${label}</span>
<div class="flags">
${values.map((value: string) => html`<span class="flag">${value}</span>`)}
</div>
</div>
`;
}
render() {
if (this.loading) {
return html`<div class="loading">Loading configuration\u2026</div>`;
@ -278,6 +406,14 @@ export class BuildConfig extends LitElement {
${disc.has_subtree_npm ? 'Depth 2' : 'None'}
</span>
</div>
${disc.distro
? html`
<div class="field">
<span class="field-label">Distro</span>
<span class="field-value">${disc.distro}</span>
</div>
`
: nothing}
${disc.linux_packages && disc.linux_packages.length > 0
? html`
<div class="field">
@ -288,6 +424,58 @@ export class BuildConfig extends LitElement {
</div>
`
: nothing}
${disc.build_options
? html`
<div class="field">
<span class="field-label">Computed options</span>
<span class="field-value">${disc.build_options}</span>
</div>
`
: nothing}
${this.renderToggle('Computed obfuscation', disc.options?.obfuscate)}
${this.renderToggle('Computed NSIS', disc.options?.nsis)}
${disc.options?.webview2
? html`
<div class="field">
<span class="field-label">Computed WebView2</span>
<span class="field-value">${disc.options.webview2}</span>
</div>
`
: nothing}
${this.renderFlags('Computed tags', disc.options?.tags)}
${this.renderFlags('Computed LD flags', disc.options?.ldflags)}
${disc.ref
? html`
<div class="field">
<span class="field-label">Git ref</span>
<span class="field-value">${disc.ref}</span>
</div>
`
: nothing}
${disc.branch
? html`
<div class="field">
<span class="field-label">Branch</span>
<span class="field-value">${disc.branch}</span>
</div>
`
: nothing}
${disc.tag
? html`
<div class="field">
<span class="field-label">Tag</span>
<span class="field-value">${disc.tag}</span>
</div>
`
: nothing}
${disc.short_sha
? html`
<div class="field">
<span class="field-label">Short SHA</span>
<span class="field-value">${disc.short_sha}</span>
</div>
`
: nothing}
<div class="field">
<span class="field-label">Directory</span>
<span class="field-value">${disc.dir}</span>
@ -307,6 +495,14 @@ export class BuildConfig extends LitElement {
</div>
`
: nothing}
${cfg.project.description
? html`
<div class="field">
<span class="field-label">Description</span>
<span class="field-value">${cfg.project.description}</span>
</div>
`
: nothing}
${cfg.project.binary
? html`
<div class="field">
@ -336,6 +532,33 @@ export class BuildConfig extends LitElement {
<span class="field-label">CGO</span>
<span class="field-value">${cfg.build.cgo ? 'Enabled' : 'Disabled'}</span>
</div>
${this.renderToggle('Obfuscation', cfg.build.obfuscate)}
${this.renderToggle('NSIS packaging', cfg.build.nsis)}
${cfg.build.webview2
? html`
<div class="field">
<span class="field-label">WebView2 mode</span>
<span class="field-value">${cfg.build.webview2}</span>
</div>
`
: nothing}
${cfg.build.deno_build
? html`
<div class="field">
<span class="field-label">Deno build</span>
<span class="field-value">${cfg.build.deno_build}</span>
</div>
`
: nothing}
${cfg.build.archive_format
? html`
<div class="field">
<span class="field-label">Archive format</span>
<span class="field-value">${cfg.build.archive_format}</span>
</div>
`
: nothing}
${this.renderFlags('Build tags', cfg.build.build_tags)}
${cfg.build.flags && cfg.build.flags.length > 0
? html`
<div class="field">
@ -356,6 +579,57 @@ export class BuildConfig extends LitElement {
</div>
`
: nothing}
${this.renderFlags('Environment', cfg.build.env)}
${cfg.build.cache?.enabled || cfg.build.cache?.path || (cfg.build.cache?.paths && cfg.build.cache.paths.length > 0)
? html`
${this.renderToggle('Build cache', cfg.build.cache?.enabled)}
${cfg.build.cache?.path
? html`
<div class="field">
<span class="field-label">Cache path</span>
<span class="field-value">${cfg.build.cache.path}</span>
</div>
`
: nothing}
${this.renderFlags('Cache paths', cfg.build.cache?.paths)}
`
: nothing}
${cfg.build.dockerfile
? html`
<div class="field">
<span class="field-label">Dockerfile</span>
<span class="field-value">${cfg.build.dockerfile}</span>
</div>
`
: nothing}
${cfg.build.image
? html`
<div class="field">
<span class="field-label">Image</span>
<span class="field-value">${cfg.build.image}</span>
</div>
`
: nothing}
${cfg.build.registry
? html`
<div class="field">
<span class="field-label">Registry</span>
<span class="field-value">${cfg.build.registry}</span>
</div>
`
: nothing}
${this.renderFlags('Image tags', cfg.build.tags)}
${this.renderToggle('Push image', cfg.build.push)}
${this.renderToggle('Load image', cfg.build.load)}
${cfg.build.linuxkit_config
? html`
<div class="field">
<span class="field-label">LinuxKit config</span>
<span class="field-value">${cfg.build.linuxkit_config}</span>
</div>
`
: nothing}
${this.renderFlags('LinuxKit formats', cfg.build.formats)}
</div>
<!-- Targets -->
@ -367,6 +641,129 @@ export class BuildConfig extends LitElement {
)}
</div>
</div>
${cfg.apple && this.hasAppleConfig(cfg.apple)
? html`
<div class="section">
<div class="section-title">Apple Pipeline</div>
${cfg.apple.bundle_id
? html`
<div class="field">
<span class="field-label">Bundle ID</span>
<span class="field-value">${cfg.apple.bundle_id}</span>
</div>
`
: nothing}
${cfg.apple.team_id
? html`
<div class="field">
<span class="field-label">Team ID</span>
<span class="field-value">${cfg.apple.team_id}</span>
</div>
`
: nothing}
${cfg.apple.arch
? html`
<div class="field">
<span class="field-label">Architecture</span>
<span class="field-value">${cfg.apple.arch}</span>
</div>
`
: nothing}
${cfg.apple.bundle_display_name
? html`
<div class="field">
<span class="field-label">Display name</span>
<span class="field-value">${cfg.apple.bundle_display_name}</span>
</div>
`
: nothing}
${cfg.apple.min_system_version
? html`
<div class="field">
<span class="field-label">Minimum macOS</span>
<span class="field-value">${cfg.apple.min_system_version}</span>
</div>
`
: nothing}
${cfg.apple.category
? html`
<div class="field">
<span class="field-label">Category</span>
<span class="field-value">${cfg.apple.category}</span>
</div>
`
: nothing}
${this.renderToggle('Sign', cfg.apple.sign)}
${this.renderToggle('Notarise', cfg.apple.notarise)}
${this.renderToggle('DMG', cfg.apple.dmg)}
${this.renderToggle('TestFlight', cfg.apple.testflight)}
${this.renderToggle('App Store', cfg.apple.appstore)}
${cfg.apple.metadata_path
? html`
<div class="field">
<span class="field-label">Metadata path</span>
<span class="field-value">${cfg.apple.metadata_path}</span>
</div>
`
: nothing}
${cfg.apple.privacy_policy_url
? html`
<div class="field">
<span class="field-label">Privacy policy</span>
<span class="field-value">${cfg.apple.privacy_policy_url}</span>
</div>
`
: nothing}
${cfg.apple.dmg_volume_name
? html`
<div class="field">
<span class="field-label">DMG volume</span>
<span class="field-value">${cfg.apple.dmg_volume_name}</span>
</div>
`
: nothing}
${cfg.apple.dmg_background
? html`
<div class="field">
<span class="field-label">DMG background</span>
<span class="field-value">${cfg.apple.dmg_background}</span>
</div>
`
: nothing}
${cfg.apple.entitlements_path
? html`
<div class="field">
<span class="field-label">Entitlements</span>
<span class="field-value">${cfg.apple.entitlements_path}</span>
</div>
`
: nothing}
${cfg.apple.xcode_cloud?.workflow
? html`
<div class="field">
<span class="field-label">Xcode Cloud workflow</span>
<span class="field-value">${cfg.apple.xcode_cloud.workflow}</span>
</div>
`
: nothing}
${cfg.apple.xcode_cloud?.triggers && cfg.apple.xcode_cloud.triggers.length > 0
? html`
<div class="field">
<span class="field-label">Xcode Cloud triggers</span>
<div class="flags">
${cfg.apple.xcode_cloud.triggers.map((trigger: AppleTriggerData) => {
const ref = trigger.branch ? `branch:${trigger.branch}` : trigger.tag ? `tag:${trigger.tag}` : 'manual';
const action = trigger.action ?? 'archive';
return html`<span class="flag">${ref}${action}</span>`;
})}
</div>
</div>
`
: nothing}
</div>
`
: nothing}
`;
}
}

View file

@ -45,7 +45,7 @@ export class BuildPanel extends LitElement {
.title {
font-weight: 700;
font-size: 1rem;
colour: #111827;
color: #111827;
}
.refresh-btn {
@ -75,7 +75,7 @@ export class BuildPanel extends LitElement {
padding: 0.625rem 1rem;
font-size: 0.8125rem;
font-weight: 500;
colour: #6b7280;
color: #6b7280;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.15s;
@ -86,12 +86,12 @@ export class BuildPanel extends LitElement {
}
.tab:hover {
colour: #374151;
color: #374151;
}
.tab.active {
colour: #6366f1;
border-bottom-colour: #6366f1;
color: #6366f1;
border-bottom-color: #6366f1;
}
/* C — Content */
@ -110,7 +110,7 @@ export class BuildPanel extends LitElement {
background: #fff;
border-top: 1px solid #e5e7eb;
font-size: 0.75rem;
colour: #9ca3af;
color: #9ca3af;
}
.ws-status {

View file

@ -26,7 +26,7 @@ export class BuildSdk extends LitElement {
.section-title {
font-size: 0.75rem;
font-weight: 700;
colour: #6b7280;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.025em;
margin-bottom: 0.75rem;
@ -49,7 +49,7 @@ export class BuildSdk extends LitElement {
.diff-field label {
font-size: 0.75rem;
font-weight: 500;
colour: #6b7280;
color: #6b7280;
}
.diff-field input {
@ -62,7 +62,7 @@ export class BuildSdk extends LitElement {
.diff-field input:focus {
outline: none;
border-colour: #6366f1;
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
@ -76,7 +76,7 @@ export class BuildSdk extends LitElement {
button.primary {
background: #6366f1;
colour: #fff;
color: #fff;
border: none;
}
@ -91,7 +91,7 @@ export class BuildSdk extends LitElement {
button.secondary {
background: #fff;
colour: #374151;
color: #374151;
border: 1px solid #d1d5db;
}
@ -109,13 +109,13 @@ export class BuildSdk extends LitElement {
.diff-result.breaking {
background: #fef2f2;
border: 1px solid #fecaca;
colour: #991b1b;
color: #991b1b;
}
.diff-result.safe {
background: #f0fdf4;
border: 1px solid #bbf7d0;
colour: #166534;
color: #166534;
}
.diff-summary {
@ -152,12 +152,12 @@ export class BuildSdk extends LitElement {
.empty {
text-align: center;
padding: 2rem;
colour: #9ca3af;
color: #9ca3af;
font-size: 0.875rem;
}
.error {
colour: #dc2626;
color: #dc2626;
padding: 0.75rem;
background: #fef2f2;
border-radius: 0.375rem;
@ -171,14 +171,14 @@ export class BuildSdk extends LitElement {
border: 1px solid #bbf7d0;
border-radius: 0.375rem;
font-size: 0.875rem;
colour: #166534;
color: #166534;
margin-bottom: 1rem;
}
.loading {
text-align: center;
padding: 1rem;
colour: #6b7280;
color: #6b7280;
font-size: 0.875rem;
}
`;