fca32396c0
Create title used to look enabled while review echoed a pre-typed title when species/primary were missing from taxonomy (no error). Persist wood cats before the deterministic build, return title_built / title_build_missing from review, and surface a Create-title nudge like Create desc. WooSetupGate now turns on content_llm_enabled when inference URL+key are set (Call 2 / Create desc). Call 1 stays out. Create-desc gate also requires inference URL+key so empty Settings show a clear missing list.
321 lines
10 KiB
Svelte
321 lines
10 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from 'svelte';
|
||
import {
|
||
getDeviceWooCreds,
|
||
setDeviceWooCreds,
|
||
withWooHeaders,
|
||
} from '../../api/wooDeviceCreds';
|
||
import { getSettings, updateSettings, type SettingsView } from '../../api/slabs';
|
||
|
||
/** Called only after Save + Test Woo succeeds. */
|
||
export let onReady: () => void;
|
||
|
||
type Secret = { configured: boolean } | string;
|
||
const isUnmasked = (s: Secret) => typeof s === 'string';
|
||
const secretValue = (s: Secret): string => (isUnmasked(s) ? (s as string) : '');
|
||
const secretConfigured = (s: Secret | undefined | null): boolean => {
|
||
if (s == null) return false;
|
||
return isUnmasked(s) ? (s as string).length > 0 : !!(s as { configured: boolean }).configured;
|
||
};
|
||
const inferenceUrlConfigured = (): boolean =>
|
||
settings ? secretConfigured(settings.inference_base_url as Secret) : false;
|
||
const inferenceKeyConfigured = (): boolean =>
|
||
settings ? secretConfigured(settings.inference_api_key as Secret) : false;
|
||
const inferenceUrlMaskedKeep = (): boolean =>
|
||
!!(settings && !isUnmasked(settings.inference_base_url as Secret) && inferenceUrlConfigured());
|
||
const inferenceKeyMaskedKeep = (): boolean =>
|
||
!!(settings && !isUnmasked(settings.inference_api_key as Secret) && inferenceKeyConfigured());
|
||
|
||
let loading = true;
|
||
let loadError = '';
|
||
let settings: SettingsView | null = null;
|
||
|
||
let username = '';
|
||
let appPassword = '';
|
||
let inferenceUrl = '';
|
||
let inferenceKey = '';
|
||
|
||
let busy = false;
|
||
let status = '';
|
||
let statusOk = false;
|
||
|
||
onMount(() => {
|
||
const existing = getDeviceWooCreds();
|
||
username = existing.username;
|
||
appPassword = existing.appPassword;
|
||
void loadSettings();
|
||
});
|
||
|
||
async function loadSettings() {
|
||
loading = true;
|
||
loadError = '';
|
||
try {
|
||
settings = await getSettings();
|
||
// Prefill inference only when unmasked (UAT); otherwise leave blank write-only.
|
||
inferenceUrl = secretValue(settings.inference_base_url as Secret);
|
||
inferenceKey = secretValue(settings.inference_api_key as Secret);
|
||
} catch {
|
||
loadError = 'Could not load settings. Check the connection and try again.';
|
||
} finally {
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
async function saveAndTest() {
|
||
const user = username.trim();
|
||
const pass = appPassword.trim();
|
||
if (!user || !pass) {
|
||
status = 'Enter this device’s WordPress user and application password.';
|
||
statusOk = false;
|
||
return;
|
||
}
|
||
busy = true;
|
||
status = 'Saving…';
|
||
statusOk = false;
|
||
try {
|
||
// Inference only — never write device Woo into Settings.
|
||
// When URL+key are (or become) configured, turn on content_llm_enabled so
|
||
// Create desc / Call 2 works. Call 1 stays out (do not set inference_enabled).
|
||
const inferPatch: Record<string, string | boolean> = {};
|
||
if (inferenceUrl.trim()) inferPatch.inference_base_url = inferenceUrl.trim();
|
||
if (inferenceKey.trim()) inferPatch.inference_api_key = inferenceKey.trim();
|
||
const willHaveUrl = !!(inferenceUrl.trim() || inferenceUrlConfigured());
|
||
const willHaveKey = !!(inferenceKey.trim() || inferenceKeyConfigured());
|
||
if (willHaveUrl && willHaveKey) {
|
||
inferPatch.content_llm_enabled = true;
|
||
}
|
||
if (Object.keys(inferPatch).length) {
|
||
settings = await updateSettings(inferPatch);
|
||
inferenceUrl = secretValue(settings.inference_base_url as Secret);
|
||
inferenceKey = secretValue(settings.inference_api_key as Secret);
|
||
}
|
||
|
||
setDeviceWooCreds({ username: user, appPassword: pass });
|
||
username = user;
|
||
appPassword = pass;
|
||
|
||
status = 'Testing WooCommerce…';
|
||
const res = await fetch('/api/v1/settings/test-woo', {
|
||
method: 'POST',
|
||
headers: withWooHeaders(),
|
||
});
|
||
let body: any = null;
|
||
try {
|
||
body = await res.json();
|
||
} catch {
|
||
body = null;
|
||
}
|
||
if (res.status === 401 || (body && body.error?.code === 'woo_device_creds_required')) {
|
||
status = body?.error?.message || 'Device Woo credentials required.';
|
||
statusOk = false;
|
||
return;
|
||
}
|
||
if (!res.ok) {
|
||
status = body?.error?.message || body?.detail || `Test failed (${res.status})`;
|
||
statusOk = false;
|
||
return;
|
||
}
|
||
if (body && body.ok) {
|
||
status = body.detail || 'Connected & synced';
|
||
statusOk = true;
|
||
onReady();
|
||
return;
|
||
}
|
||
status = (body && body.detail) || 'Store rejected the credentials. Check user and app password.';
|
||
statusOk = false;
|
||
} catch {
|
||
status = 'Network error — try again.';
|
||
statusOk = false;
|
||
} finally {
|
||
busy = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<div class="gate" role="dialog" aria-modal="true" aria-labelledby="woo-gate-title" data-testid="woo-setup-gate">
|
||
<div class="card">
|
||
<h1 id="woo-gate-title">Set up WooCommerce for this device</h1>
|
||
<p class="lede">
|
||
Each browser uses its own WordPress user and application password. They stay on this
|
||
device only — not in shared server Settings. Inference stays shared for the shop.
|
||
</p>
|
||
|
||
{#if loading}
|
||
<p class="busy">Loading…</p>
|
||
{:else}
|
||
{#if loadError}
|
||
<p class="err" role="alert">{loadError}</p>
|
||
<button type="button" class="ghost" on:click={() => void loadSettings()}>Retry</button>
|
||
{:else}
|
||
<div class="field">
|
||
<label for="woo-base">Woo store URL</label>
|
||
<input id="woo-base" type="text" value={settings?.woo_base_url || ''} disabled data-testid="woo-gate-base-url" />
|
||
<p class="hint">From server env — display only.</p>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="woo-user">Device WordPress username</label>
|
||
<input
|
||
id="woo-user"
|
||
type="text"
|
||
bind:value={username}
|
||
autocomplete="username"
|
||
data-testid="woo-gate-username"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label for="woo-pass">Device application password</label>
|
||
<input
|
||
id="woo-pass"
|
||
type="password"
|
||
bind:value={appPassword}
|
||
autocomplete="current-password"
|
||
placeholder="••••••••"
|
||
data-testid="woo-gate-app-password"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>Inference (shared)</h2>
|
||
<div class="field">
|
||
<label for="inf-url">
|
||
Inference base URL
|
||
{#if inferenceUrlConfigured()}
|
||
<span class="tag">configured</span>
|
||
{:else}
|
||
<span class="tag miss">not set</span>
|
||
{/if}
|
||
</label>
|
||
<input
|
||
id="inf-url"
|
||
type="text"
|
||
bind:value={inferenceUrl}
|
||
placeholder={inferenceUrlMaskedKeep() ? '•••••••• (leave blank to keep)' : 'https://…'}
|
||
data-testid="woo-gate-inference-url"
|
||
/>
|
||
</div>
|
||
<div class="field">
|
||
<label for="inf-key">
|
||
Inference API key
|
||
{#if inferenceKeyConfigured()}
|
||
<span class="tag">configured</span>
|
||
{:else}
|
||
<span class="tag miss">not set</span>
|
||
{/if}
|
||
</label>
|
||
<input
|
||
id="inf-key"
|
||
type="password"
|
||
bind:value={inferenceKey}
|
||
placeholder={inferenceKeyMaskedKeep() ? '•••••••• (leave blank to keep)' : ''}
|
||
data-testid="woo-gate-inference-key"
|
||
/>
|
||
</div>
|
||
<p class="hint">Only filled values are saved to shared Settings. When both are set, Content LLM (Create desc / Call 2) turns on. Device Woo is never written there.</p>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
class="primary"
|
||
disabled={busy || !username.trim() || !appPassword.trim()}
|
||
data-testid="woo-gate-save-test"
|
||
on:click={() => void saveAndTest()}
|
||
>
|
||
{busy ? 'Working…' : 'Save device creds & Test Woo'}
|
||
</button>
|
||
{#if status}
|
||
<p class="msg" class:ok={statusOk} class:err={!statusOk} role="status" data-testid="woo-gate-status">{status}</p>
|
||
{/if}
|
||
{/if}
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.gate {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1000;
|
||
background: rgba(8, 8, 8, 0.92);
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
overflow-y: auto;
|
||
padding: 24px 16px 48px;
|
||
box-sizing: border-box;
|
||
}
|
||
.card {
|
||
width: 100%;
|
||
max-width: 520px;
|
||
background: #1a1a1a;
|
||
border: 1px solid #333;
|
||
border-radius: 4px;
|
||
padding: 20px 18px 24px;
|
||
color: #eee;
|
||
box-sizing: border-box;
|
||
}
|
||
h1 { font-size: 1.35rem; margin: 0 0 8px; }
|
||
h2 { font-size: 1.05rem; margin: 0 0 10px; color: #ddd; }
|
||
.lede { color: #aaa; font-size: 0.9rem; margin: 0 0 18px; line-height: 1.4; }
|
||
.section {
|
||
margin: 18px 0 8px;
|
||
padding-top: 14px;
|
||
border-top: 1px solid #333;
|
||
}
|
||
.field { margin-bottom: 12px; }
|
||
label { display: block; margin: 0 0 4px; font-size: 0.9rem; color: #ccc; }
|
||
input {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 10px 10px;
|
||
border-radius: 3px;
|
||
border: 1px solid #444;
|
||
background: #121212;
|
||
color: #eee;
|
||
font-size: 1rem;
|
||
}
|
||
input:disabled { color: #888; background: #1c1c1c; }
|
||
.hint { margin: 4px 0 0; color: #888; font-size: 0.8rem; }
|
||
.tag {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
font-size: 0.7rem;
|
||
padding: 1px 6px;
|
||
border-radius: 2px;
|
||
background: #2a3a2a;
|
||
color: #7fd37f;
|
||
font-weight: 600;
|
||
}
|
||
.tag.miss { background: #3a2a2a; color: #e8a; }
|
||
.primary {
|
||
width: 100%;
|
||
margin-top: 12px;
|
||
padding: 14px;
|
||
font-size: 1rem;
|
||
border-radius: 3px;
|
||
border: 0;
|
||
background: #00e5ff;
|
||
color: #002;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
.primary:disabled { background: #333; color: #777; cursor: not-allowed; }
|
||
.ghost {
|
||
margin-top: 8px;
|
||
padding: 10px 14px;
|
||
border-radius: 3px;
|
||
border: 1px solid #444;
|
||
background: transparent;
|
||
color: #ccc;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.msg { margin-top: 12px; font-weight: 600; }
|
||
.msg.ok { color: #7fd37f; }
|
||
.msg.err, .err { color: #ffb4a2; }
|
||
.busy { color: #aaa; }
|
||
</style>
|