From 284d3171c2469dc09d13bf89de47cf66f3f9b1cd Mon Sep 17 00:00:00 2001 From: Marco0300 Date: Fri, 4 Sep 2026 15:22:54 +0200 Subject: [PATCH] fix source preview enable handling --- apps/web/app.js | 2 +- apps/web/asset-manifest.json | 4 ++-- apps/web/scripts/final-acceptance.mjs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/app.js b/apps/web/app.js index 7467286..0ee87cb 100644 --- a/apps/web/app.js +++ b/apps/web/app.js @@ -280,7 +280,7 @@ const sourceText = (source, keys, fallback='Not returned') => { for (const key of keys) if (source?.[key] !== undefined && source[key] !== null && source[key] !== '') return source[key]; return fallback; }; function sourceMessage(text, error = false) { const el = $('sourcesMessage'); if (el) { el.textContent = text || ''; el.className = `sources-message${error ? ' error' : ''}`; } } function renderSourceSelect() { const select = $('discoverySource'); if (select) select.innerHTML = `${sources.filter(source=>!source.optional).map(s => ``).join('')}`; const multi=$('directDiscoverySources'); if(multi) multi.innerHTML=sources.filter(sourceState).map(s=>``).join('') || ''; } - function renderSources() { renderSourceSelect(); const list = $('sourcesList'); if (!sources.length) { list.innerHTML = '
No registered or available sources returned by the workspace.
'; return; } list.innerHTML = sources.map(source => { const policy=sourceJson(source.policy_json||source.policy), quota=sourceJson(source.quota_json||source.quota), config=sourceJson(source.config_json||source.config), status=sourceStatus(source), configured=source.configured ?? (!source.optional && (source.approved || sourceType(source)==='manual'||sourceType(source)==='csv')), available=source.available ?? (!source.optional || Boolean(source.configured)), health=sourceText(source,['health_status','health'],'Not tested'), failures=sourceText(source,['consecutive_failures'],'0'), circuit=source.circuit_open===true||source.circuit_open===1?'Open':'Closed', credential=sourceText(source,['api_credential_status','credential_status'],source.optional?'Required / not configured':'Not required'), terms=sourceText(source,['terms_status','terms_reviewed'],policy.terms_accepted===true?'Accepted':policy.terms_url||config.terms_url?'Provided':'Not reviewed'), owner=sourceText(source,['owner','owner_name'],policy.owner||config.owner||'Not assigned'), rate=sourceText(source,['rate_limit','rate_limit_label'],policy.rate_limit||config.rate_limit||'Not set'), daily=sourceText(source,['daily_quota','daily_limit'],quota.daily_limit||'Not set'), lastHealth=sourceText(source,['last_health_at','last_checked_at','updated_at'],'Not checked'), success=sourceText(source,['last_success_at'],'No successful run'), error=sourceText(source,['last_error','error'],'None recorded'); return `
${esc(sourceLabel(source))}${esc(sourceType(source))}
${source.optional?'Optional adapter · configuration-gated':'Registered workspace source'}
${esc(status==='unavailable'?'Unavailable':status)}
Integration
${esc(source.optional?'Optional adapter':'Registered')}
Configured
${configured?'Yes':'No'}
Available
${available?'Yes':'No'}
Enabled
${sourceState(source)?'Yes':'No'}
API credential
${esc(credential)}
Terms
${esc(terms)}
Owner
${esc(owner)}
Rate limit
${esc(rate)}
Daily quota
${esc(daily)}
Last health
${esc(lastHealth)} · ${esc(health)}
Success / error
${esc(success)}
${esc(error)}
Circuit
${esc(circuit)} · ${esc(failures)} failures
`; }).join(''); } + function renderSources() { renderSourceSelect(); const list = $('sourcesList'); if (!sources.length) { list.innerHTML = '
No registered or available sources returned by the workspace.
'; return; } list.innerHTML = sources.map(source => { const policy=sourceJson(source.policy_json||source.policy), quota=sourceJson(source.quota_json||source.quota), config=sourceJson(source.config_json||source.config), status=sourceStatus(source), configured=source.configured ?? (!source.optional && (source.approved || sourceType(source)==='manual'||sourceType(source)==='csv')), available=source.available ?? (!source.optional || Boolean(source.configured)), health=sourceText(source,['health_status','health'],'Not tested'), failures=sourceText(source,['consecutive_failures'],'0'), circuit=source.circuit_open===true||source.circuit_open===1?'Open':'Closed', credential=sourceText(source,['api_credential_status','credential_status'],source.optional?'Required / not configured':'Not required'), terms=sourceText(source,['terms_status','terms_reviewed'],policy.terms_accepted===true?'Accepted':policy.terms_url||config.terms_url?'Provided':'Not reviewed'), owner=sourceText(source,['owner','owner_name'],policy.owner||config.owner||'Not assigned'), rate=sourceText(source,['rate_limit','rate_limit_label'],policy.rate_limit||config.rate_limit||'Not set'), daily=sourceText(source,['daily_quota','daily_limit'],quota.daily_limit||'Not set'), lastHealth=sourceText(source,['last_health_at','last_checked_at','updated_at'],'Not checked'), success=sourceText(source,['last_success_at'],'No successful run'), error=sourceText(source,['last_error','error'],'None recorded'); return `
${esc(sourceLabel(source))}${esc(sourceType(source))}
${source.optional?'Optional adapter · configuration-gated':'Registered workspace source'}
${esc(status==='unavailable'?'Unavailable':status)}
Integration
${esc(source.optional?'Optional adapter':'Registered')}
Configured
${configured?'Yes':'No'}
Available
${available?'Yes':'No'}
Enabled
${sourceState(source)?'Yes':'No'}
API credential
${esc(credential)}
Terms
${esc(terms)}
Owner
${esc(owner)}
Rate limit
${esc(rate)}
Daily quota
${esc(daily)}
Last health
${esc(lastHealth)} · ${esc(health)}
Success / error
${esc(success)}
${esc(error)}
Circuit
${esc(circuit)} · ${esc(failures)} failures
`; }).join(''); } function renderSourceRecords(items) { const list = $('sourceRecordsList'); if (!items.length) { list.innerHTML = '
No source records returned by the workspace.
'; return; } list.innerHTML = `
${items.slice(0,25).map(record => ``).join('')}
RecordSourceStatusObserved
${esc(record.name || record.title || record.external_id || record.id || 'Unnamed record')}${esc(record.source_name || record.source || 'Unknown source')}${esc(record.status || 'Pending')}${esc(record.observed_at || record.created_at || 'Time unavailable')}
`; } async function loadSources() { sourceMessage('Loading sources…'); $('sourcesList').innerHTML = '
Loading source registry…
'; $('sourceRecordsList').innerHTML = '
Loading source records…
'; try { const [sourcePayload, adapterPayload, recordPayload] = await Promise.all([jsonRequest('/api/v1/sources'), jsonRequest('/api/v1/sources/adapters'), jsonRequest('/api/v1/source-records?page_size=25')]); const configured=sourceItems(sourcePayload), registeredCodes=new Set(configured.map(sourceType)); const optional=sourceItems(adapterPayload).filter(adapter=>!registeredCodes.has(adapter.source_code)).map(adapter=>({...adapter,source_code:adapter.source_code,display_name:adapter.display_name,kind:adapter.source_code,optional:Boolean(adapter.optional),configured:false,available:Boolean(adapter.available),enabled:false})); sources=[...configured,...optional]; renderSources(); renderSourceRecords(sourceItems(recordPayload)); $('sourcesUpdatedAt').textContent = `Updated ${new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}`; sourceMessage(sources.some(sourceState) ? '' : 'No live source is enabled. Configure and enable a ready source to begin discovery.'); } catch (error) { sources = []; renderSources(); renderSourceRecords([]); if (error.message !== 'unauthorized') sourceMessage(error.message || 'Unable to load sources.', true); } } async function saveSource(event) { event.preventDefault(); const form = event.currentTarget, fields = Object.fromEntries(new FormData(form).entries()); if (fields.source_type === 'csv' && !fields.csv_content.trim()) { message('sourceFormMessage', 'CSV content is required for a CSV source.', true); return; } const config = {url:fields.url, terms_url:fields.terms_url, owner:fields.owner, rate_limit:fields.rate_limit}; if (fields.source_type === 'csv') config.csv = fields.csv_content; else config.rows = []; try { await jsonRequest('/api/v1/sources', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:fields.name, kind:fields.source_type, config, enabled:false})}); message('sourceFormMessage', 'Source saved. It remains disabled until explicitly enabled.'); form.reset(); $('sourceCsvField').hidden = true; await loadSources(); } catch (error) { if (error.message !== 'unauthorized') message('sourceFormMessage', error.message || 'Unable to save source.', true); } } diff --git a/apps/web/asset-manifest.json b/apps/web/asset-manifest.json index a1d4de8..0f01b24 100644 --- a/apps/web/asset-manifest.json +++ b/apps/web/asset-manifest.json @@ -1,6 +1,6 @@ { "schema": 1, - "version": "phase-29", + "version": "phase-30", "entrypoints": [ "config.js", "app.js", @@ -14,7 +14,7 @@ ], "integrity": { "config.js": "sha256-40caf170414913b9ab63df455d1510d6ce11f1904b40441f20396291861f76fe", - "app.js": "sha256-3a474d4753b84fa27ecac448056d13c4ee25341eb7877950fb52a8dc6922716f", + "app.js": "sha256-bf6e5a2ec252087066da0a5c06d3f81112e2ae56d724cc0169f88b1b7e52747a", "styles.css": "sha256-ba90290ab11e82a6b2639dfd70d1e74502c1cacb2b26cf1db92b45beb67ac03f", "index.html": "sha256-316b06d3062b77330962c303020ab4577cbbbc8c92bf33a99957ab167cb85ea2", "health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81", diff --git a/apps/web/scripts/final-acceptance.mjs b/apps/web/scripts/final-acceptance.mjs index a84456e..6c2ed97 100644 --- a/apps/web/scripts/final-acceptance.mjs +++ b/apps/web/scripts/final-acceptance.mjs @@ -83,7 +83,7 @@ check('routes.authenticated', 'protected API requests include cookie credentials check('auth.display-name-greeting', 'authenticated display name drives the greeting and identity', js.includes('currentUser.display_name') && js.includes("$('userGreetingName').textContent") && !html.includes('Good morning, Alex')); check('sources.registry-status', 'source registry exposes governed configuration and operational status fields', all(['source_code', 'display_name', 'configured', 'available', 'enabled', 'API credential', 'Terms', 'Owner', 'Rate limit', 'Daily quota', 'Last health', 'Success / error', 'Circuit', 'data-source-action="review"', 'data-source-action="test"'], token => `${html}\n${js}\n${css}`.includes(token))); check('sources.optional-gated', 'optional adapters are rendered unavailable until configured', all(['/api/v1/sources/adapters', 'configuration-gated', 'Optional adapter', 'source.optional', 'available:Boolean(adapter.available)'], token => `${html}\n${js}\n${css}`.includes(token))); -check('sources.registered-actions', 'registered sources keep Review/Test actions actionable while previews remain disabled', js.includes("${!source.id?'disabled':''}") && js.includes("if (!source || !source.id)")); +check('sources.registered-actions', 'registered sources keep Review/Test actions actionable while previews remain disabled', js.includes("${!source.id?'disabled':''}") && js.includes("${!source.id || !available?'disabled':''}") && js.includes("if (!source || !source.id)")); check('sources.toggle-guard', 'source toggle handler ignores unknown rows before reading source fields', js.includes("const source = findRegisteredSource(actionButton.dataset.sourceId);\n if (!source) return;\n const needsPromptConfig")); check('sources.setup-affordances', 'manual and CSV setup affordances remain explicit and disabled by default', all(['Manual records', 'CSV import', 'CSV content is required', 'enabled:false', 'remains disabled'], token => `${html}\n${js}\n${css}`.includes(token))); check('discovery.operator-controls', 'discovery builder and run controls are represented', all(['data-run-action="pause"', 'data-run-action="resume"', 'data-run-action="cancel"', 'live-log', 'source-health', 'daily_limit', 'source_ids', 'schedule'], token => `${html}\n${js}\n${css}`.includes(token)));