// Dashboard — full Claude Design report viewer, wired to real job data
// Preview fallback helper — reads from window.FOSSYL_DEMO (set by graph.jsx).
// Hardcoded fallback matches graph.jsx RISK_CLUSTERS sums so both files stay in sync.
function D(key) {
return (window.FOSSYL_DEMO || { total:145, ttw:125, diverged:8, synced:12, ttwPct:86, edges:481, elapsed:52, deathKb:187 })[key];
}
const DASH_API = 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod';
const DASH_COG = '7nffjheavp985k7b4bgug45jf4';
function dashToken() {
const last = localStorage.getItem(`CognitoIdentityServiceProvider.${DASH_COG}.LastAuthUser`);
if (last) { const t = localStorage.getItem(`CognitoIdentityServiceProvider.${DASH_COG}.${last}.idToken`); if (t) return t; }
return window.getIdToken ? window.getIdToken() : '';
}
async function dashApi(path) {
const res = await fetch(`${DASH_API}${path}`, { headers: { Authorization: `Bearer ${dashToken()}` } });
const d = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(d.error || `HTTP ${res.status}`);
return d;
}
function getActiveJobId() { return localStorage.getItem('fossyl_active_job') || ''; }
function Dashboard() {
const { go } = useNav();
const { toast } = useToast();
const [section, setSection] = useState('divergence');
const [job, setJob] = useState(null);
const [downloading, setDownloading] = useState(false);
const jobId = getActiveJobId();
// If no active job, send to archive
useEffect(() => { if (!jobId) go('reports'); }, []);
useEffect(() => {
if (!jobId) return;
dashApi(`/status/${jobId}`)
.then(data => {
setJob(data);
if (window.validateRealStats) window.validateRealStats({
ttw: data.ttw_count || 0,
diverged: data.diverged_count || 0,
synced: data.synced_count || 0,
artifacts:data.artifact_count || 0,
});
})
.catch(() => {});
}, [jobId]);
const handleDownload = async () => {
setDownloading(true);
try {
const d = await dashApi(`/download/${jobId}`);
window.location.href = d.download_url;
} catch (e) { toast(e.message || 'Download failed', { tone: 'error' }); }
finally { setDownloading(false); }
};
const handleShare = async () => {
try {
const d = await dashApi(`/download/${jobId}?view=1`);
navigator.clipboard?.writeText(d.download_url).catch(() => {});
toast('Link copied — valid 1 hour', { tone: 'success' });
} catch (e) { toast(e.message || 'Failed', { tone: 'error' }); }
};
// Real stats from DynamoDB job record
const realStats = job ? {
job_id: job.job_id || jobId,
ttw: job.ttw_count || 0,
artifacts: job.artifact_count || 0,
edges: job.edge_count || 0,
nodes: job.node_count || 0,
diverged: job.diverged_count || 0,
synced: job.synced_count || 0,
elapsed: job.elapsed_s || 0,
size_kb: job.report_size_kb || 0,
completed_at: job.completed_at || '',
} : null;
const scanLabel = realStats ? realStats.job_id.slice(0, 8) : 'F-8a3f';
const ttwPct = realStats
? Math.min(100, Math.round(((realStats.ttw || 0) / Math.max((realStats.ttw || 0) + (realStats.diverged || 0) + (realStats.synced || 0), 1)) * 100))
: D('ttwPct');
// totalZodb = all ZODB objects (TTW-only + diverged + synchronized).
// Distinct from realStats.artifacts (filesystem artifacts in the dep graph).
const totalZodb = realStats
? (realStats.ttw || 0) + (realStats.diverged || 0) + (realStats.synced || 0)
: D('ttw') + D('diverged') + D('synced');
const scanDate = realStats?.completed_at ? new Date(realStats.completed_at).toISOString().slice(0, 10) : '—';
return (
);
}
function DashSidebar({ section, setSection, go, scanLabel, ttwPct, zodbCount }) {
const sections = [
{ id: 'overview', num: '§0', label: 'Overview' },
{ id: 'divergence',num: '§1', label: 'Divergence', badge: `${ttwPct}%` },
{ id: 'graph', num: '§2', label: 'Dependency graph' },
{ id: 'coupling', num: '§3', label: 'Coupling / risk' },
{ id: 'versions', num: '§4', label: 'Version groups' },
{ id: 'dead', num: '§5', label: 'Dead code' },
{ id: 'ai', num: '§6', label: 'Field explanations' },
{ id: 'source', num: '§7', label: 'Source browser' },
{ id: 'schema', num: '§8', label: 'Schema map' },
];
return (
Current dig
scan_id {scanLabel}
Excavation {scanLabel}
{zodbCount} ZODB objects · {ttwPct}% untracked
Report
{sections.map((s) => (
setSection(s.id)} style={{
background: section === s.id ? 'var(--bg-2)' : 'transparent',
border: 0, textAlign: 'left', padding: '8px 12px',
color: section === s.id ? 'var(--ink)' : 'var(--ink-dim)',
fontFamily: 'var(--sans)', fontSize: 13,
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
cursor: 'pointer',
borderLeft: section === s.id ? '1px solid var(--bone)' : '1px solid transparent',
letterSpacing: '-0.005em',
}}>
{s.num} {s.label}
{s.badge && {s.badge} }
))}
go('reports')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
← All excavations
go('upload')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
§X New excavation
go('billing')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
§B Billing
go('landing')} style={{ background: 'transparent', border: 0, textAlign: 'left', padding: '8px 12px', color: 'var(--ink-dim)', fontFamily: 'var(--sans)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', borderLeft: '1px solid transparent' }}>
↩ Back to landing
go('account')}>
EK
E. Kirsten
Owner · settings →
);
}
function DashTopBar({ scanLabel, go, toast, onDownload, onShare, downloading, hasJob }) {
const { signOut } = useAuth();
return (
go('reports')}>excavations
/
{scanLabel}
);
}
function DashContent({ section, realStats, go, toast, jobId }) {
return (
{section === 'overview' &&
}
{section === 'divergence' && }
{section === 'graph' && }
{section === 'coupling' && }
{section === 'versions' && }
{section === 'dead' && }
{section === 'ai' && }
{section === 'source' && }
{section === 'schema' && }
);
}
function DemoDataNote() {
return (
Detailed section data (coupling tables, version groups, source listing) shown as design reference.
The full interactive report with all real data is available via Download HTML →
);
}
// ── §0 Overview ───────────────────────────────────────────────────────────────
function Overview({ realStats: r }) {
const ttw = r?.ttw || D('ttw');
const fsArtifacts= r?.artifacts || D('total');
const edges = r?.edges || D('edges');
const diverged = r?.diverged || D('diverged');
const synced = r?.synced || D('synced');
const elapsed = r?.elapsed || D('elapsed');
const scanLabel = r ? r.job_id.slice(0, 8) : 'F-8a3f';
// ZODB objects = all objects in the live database (TTW-only + diverged + synchronized).
// Distinct from fsArtifacts (scripts found in the filesystem/ZIP).
const totalZodb = ttw + diverged + synced;
return (
Fig. 0.1 — Section index
{[
['§1', 'Divergence', `${ttw} of ${totalZodb} ZODB objects untracked`, 'rust'],
['§2', 'Dependency graph', `${edges} edges · ${fsArtifacts} FS artifacts`, 'slate'],
['§3', 'Coupling / risk', 'High-in-degree artifacts', 'bone'],
['§4', 'Version groups', 'Duplicate script clusters', 'slate'],
['§5', 'Dead code', 'Zero-caller candidates', 'lichen'],
['§6', 'AI explanations', 'Plain-English field notes', 'slate'],
['§7', 'Source browser', 'Full artifact index', 'slate'],
['§8', 'Schema map', 'MySQL cross-reference', 'slate'],
].map((row) => (
{row[0]}
{row[1]}
{row[2]}
))}
Fig. 0.2 — Scan pipeline
{[
['Ingest ZIP', null],
['Parse ZODB', null],
['Resolve traversal', null],
['Build dep graph', null],
['Coupling analysis', null],
['AI field notes', null],
['Emit HTML', null],
].map((row, i) => (
{i + 1}. {row[0]}
✓
))}
{ttw} scripts run in live production but exist only in the ZODB — not in version control. In event of hardware loss, these artifacts are permanently unrecoverable.
);
}
// §01 TTW table — exposed as window.FOSSYL_TTW_TABLE for validateDemoFixture inDeg cross-check.
// Columns: [id, path, type, lastEdit, callers, risk]
const TTW_TABLE = [
['F-0418', '/portal_skins/custom/admitPatient', 'Python Script', '2014-11-22', 41, 100],
['F-0371', '/portal_skins/custom/claim_to_ledger', 'Python Script', '2012-02-17', 38, 89],
['F-0329', '/portal_skins/custom/hl7_in', 'Python Script', '2013-12-01', 27, 64],
['F-0427', '/portal_skins/custom/sendReferralFax', 'Python Script', '2011-03-08', 23, 52],
['F-0312', '/portal_skins/custom/discharge_pt', 'Page Template', '2012-08-19', 18, 44],
['F-0392', '/portal_skins/custom/ward_roster_pt', 'Page Template', '2013-06-04', 14, 35],
['F-0355', '/portal_skins/custom/printLabel', 'External Method', '2010-09-30', 12, 28],
['F-0341', '/portal_skins/custom/mergePatient', 'Python Script', '2015-04-12', 9, 23],
];
window.FOSSYL_TTW_TABLE = TTW_TABLE;
// ── §1 Divergence ─────────────────────────────────────────────────────────────
function Divergence({ realStats: r }) {
const ttw = r?.ttw || D('ttw');
const diverged = r?.diverged || D('diverged');
const synced = r?.synced || D('synced');
const ttwPct = Math.min(100, Math.round((ttw / Math.max(ttw + diverged + synced, 1)) * 100)) || D('ttwPct');
return (
}
/>
Fig. 1.1 — Stratigraphic reconciliation
per-folder breakdown · top 8 shown · demo data
{[
{ p: '/Plone/portal_skins/custom', fs: 122, zodb: 487, ttw: 412 },
{ p: '/Plone/patient_portal', fs: 304, zodb: 288, ttw: 41 },
{ p: '/Plone/admission', fs: 88, zodb: 124, ttw: 68 },
{ p: '/Plone/billing', fs: 140, zodb: 206, ttw: 91 },
{ p: '/Plone/lab_results', fs: 67, zodb: 77, ttw: 22 },
{ p: '/Plone/pharmacy', fs: 54, zodb: 89, ttw: 38 },
{ p: '/Plone/portal_workflow', fs: 44, zodb: 51, ttw: 9 },
{ p: '/Plone/acl_users', fs: 28, zodb: 34, ttw: 12 },
].map((row) =>
)}
Fig. 1.2 — TTW-only artifacts, highest coupling first
TTW · no VCS backup
showing 8 of {ttw} · demo data
ID Path Type Last edit Callers Risk
{TTW_TABLE.map((r) => (
{r[0]}
{r[1]}
{r[2]}
{r[3]}
{r[4]}
open →
))}
);
}
function DivRow({ p, fs, zodb, ttw }) {
const matched = Math.min(fs, zodb - ttw);
const orphanFs = Math.max(0, fs - matched);
const total2 = matched + orphanFs + ttw;
const pct = (n) => (n / total2) * 100;
return (
{p}
● {matched}
● {orphanFs}
● {ttw}
);
}
// ── §2 Dependency graph ───────────────────────────────────────────────────────
function GraphSection({ realStats: r }) {
const fsArtifacts = r?.artifacts || D('total');
const ttw = r?.ttw || D('ttw');
const edges = r?.edges || D('edges');
const nodes = r?.nodes || D('total');
const diverged = r?.diverged || D('diverged');
const synced = r?.synced || D('synced');
const totalZodb = ttw + diverged + synced;
const highlyCoupled = (window.COUPLING_TOP || []).filter(c => c.inDeg >= 5).length || 15;
return (
▸
Fig. 2.2 — Raw dependency graph (technical detail)
{nodes} nodes · {edges} edges · click to expand
Illustrative graph — download the HTML report for the real interactive graph.
layout
Force
Radial
Matrix
);
}
function GraphToggle({ val, children }) {
const [current, setCurrent] = useState(window.__graph || 'force');
useEffect(() => {
const onChange = () => setCurrent(window.__graph || 'force');
window.addEventListener('graph-change', onChange);
return () => window.removeEventListener('graph-change', onChange);
}, []);
const active = current === val;
return (
{ window.__graph = val; window.dispatchEvent(new Event('graph-change')); }} style={{
padding: '5px 12px', background: active ? 'var(--bg-2)' : 'transparent',
border: 0, borderRight: '1px solid var(--line-2)',
color: active ? 'var(--ink)' : 'var(--muted)',
fontFamily: 'var(--mono)', fontSize: 11, cursor: 'pointer',
}}>{children}
);
}
function GraphSurface() {
const [current, setCurrent] = useState(window.__graph || 'force');
useEffect(() => {
const onChange = () => setCurrent(window.__graph || 'force');
window.addEventListener('graph-change', onChange);
return () => window.removeEventListener('graph-change', onChange);
}, []);
return ;
}
// ── §3 Coupling ───────────────────────────────────────────────────────────────
// risk = W.in·in + W.ch·churn + W.ttw·ttw (ttw=1 for through-the-web only), normalized 0–100.
// Rows sorted descending by raw score. Weights exposed for validateDemoFixture cross-check.
(function buildCouplingRows() {
const W = { in: 0.6, ch: 0.2, ttw: 0.2 };
window.FOSSYL_RISK_WEIGHTS = W;
// Formula label generated from W so validator can assert label === constants
window.FOSSYL_RISK_FORMULA = `risk = ${W.in}·in + ${W.ch}·churn + ${W.ttw}·ttw, norm. to 100`;
const raw = [
{ id: 'F-0418', n: 'admitPatient', in: 41, out: 14, ch: 18, ttw: 1 },
{ id: 'F-0371', n: 'claim_to_ledger', in: 38, out: 22, ch: 12, ttw: 1 },
{ id: 'F-0285', n: 'triage_rules', in: 29, out: 11, ch: 14, ttw: 0 },
{ id: 'F-0329', n: 'hl7_in', in: 27, out: 16, ch: 9, ttw: 1 },
{ id: 'F-0427', n: 'sendReferralFax', in: 23, out: 8, ch: 4, ttw: 1 },
{ id: 'F-0312', n: 'discharge_pt', in: 18, out: 9, ch: 8, ttw: 1 },
{ id: 'F-0392', n: 'ward_roster_pt', in: 14, out: 7, ch: 6, ttw: 1 },
{ id: 'F-0341', n: 'mergePatient', in: 9, out: 13, ch: 5, ttw: 1 },
];
const score = r => W.in*r.in + W.ch*r.ch + W.ttw*r.ttw;
const maxRaw = Math.max(...raw.map(score));
const rows = raw
.map(r => ({ ...r, risk: Math.round(score(r) / maxRaw * 100) }))
.sort((a, b) => score(b) - score(a));
window.FOSSYL_COUPLING_ROWS = rows;
})();
function Coupling() {
const files = window.FOSSYL_COUPLING_ROWS || [];
const maxIn = files.length ? files.reduce((m, f) => Math.max(m, f.in), 0) : 1;
return (
Fig. 3.1 — Top coupling risk · demo data
{window.FOSSYL_RISK_FORMULA || 'risk = 0.6·in + 0.2·churn + 0.2·ttw, norm. to 100'}
ID Artifact In-deg Churn TTW Risk In-deg bar
{files.map((f) => (
{f.id}
{f.n}
{f.in}
{f.ch}
{f.ttw ? '✓' : '—'}
))}
);
}
// ── §4 Versions ───────────────────────────────────────────────────────────────
// sims: per-copy similarity, all in (0.86, 0.999] (threshold ≥ 0.86, never ≥ 1.0).
// Header range derived from actual min/max of each group's sims.
// Exposed as window.FOSSYL_VERSION_GROUPS for validateDemoFixture.
const VERSION_GROUPS = [
{ id: 'V-01', n: 'send_mail', copies: 5,
paths: ['portal_skins/custom/sendMail', 'billing/sendMail_v2', 'admission/sendMail', 'pharmacy/sendMail_legacy', 'shared/mailer_patch'],
sims: [0.97, 0.94, 0.91, 0.88, 0.86] },
{ id: 'V-02', n: 'patient_lookup', copies: 4,
paths: ['patient_portal/lookup', 'admission/lookup_v2', 'lab_results/lookup', 'pharmacy/lookup'],
sims: [0.96, 0.92, 0.89, 0.87] },
{ id: 'V-03', n: 'format_date', copies: 7,
paths: ['utils/format_date', 'patient_portal/fmt', 'billing/fmt_date', 'lab_results/dateFmt', 'admission/dateFmt', 'discharge/dateFmt', 'archive/fmt_date'],
sims: [0.99, 0.96, 0.93, 0.91, 0.89, 0.87, 0.86] },
];
window.FOSSYL_VERSION_GROUPS = VERSION_GROUPS;
function Versions() {
return (
{VERSION_GROUPS.map((g) => {
const minSim = Math.min(...g.sims).toFixed(2);
const maxSim = Math.max(...g.sims).toFixed(2);
return (
{g.id}
{g.n}
{g.copies} copies
similarity {minSim}–{maxSim}
{g.paths.map((p, i) => (
0 ? '1px solid var(--line)' : 'none' }}>
copy_{i + 1}
/{p}
))}
);
})}
);
}
// ── §5 Dead code ──────────────────────────────────────────────────────────────
const DEAD_CANDIDATES = [
{ id: 'F-1201', path: '/portal_skins/custom/old_admit_v1', type: 'Python Script', last: '2009-04-18', kb: 12.4 },
{ id: 'F-1177', path: '/portal_skins/custom/print_v1', type: 'Python Script', last: '2010-02-04', kb: 8.1 },
{ id: 'F-1102', path: '/portal_skins/custom/legacy_fax', type: 'External Method', last: '2011-08-22', kb: 5.7 },
{ id: 'F-1044', path: '/portal_skins/custom/test_harness', type: 'Python Script', last: '2012-01-11', kb: 11.2 },
{ id: 'F-0988', path: '/portal_skins/custom/old_ward_view', type: 'Page Template', last: '2012-06-17', kb: 9.8 },
];
function DeadCode() {
const totalKb = D('deathKb'); // ~187 KB estimated for all 23 candidates
const totalMb = (totalKb / 1024).toFixed(2);
const candCount = 23;
return (
Fig. 5.1 — Candidates · demo data showing {DEAD_CANDIDATES.length} of {candCount}
ID Path Type Last edit Size Callers
{DEAD_CANDIDATES.map((r) => (
{r.id}
{r.path}
{r.type}
{r.last}
{r.kb} KB
0
))}
);
}
// ── §6 AI explanations ────────────────────────────────────────────────────────
function AIExplanations({ jobId }) {
const { toast } = useToast();
const [items, setItems] = useState(null); // null = loading, [] = no data
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!jobId) { setLoading(false); setItems([]); return; }
dashApi(`/download/${jobId}?ai=1`)
.then(d => {
// Guard: Lambda returns {"ai_url":...} for ?ai=1. If ai_url is absent
// (e.g. Lambda returned HTML download_url instead), fetch(undefined) would
// silently resolve to fetch("undefined") → getfossyl.dev/undefined 404.
if (!d.ai_url) throw new Error('No AI URL returned — scan may not have AI enabled');
return fetch(d.ai_url);
})
.then(r => {
if (!r.ok) throw new Error(`AI JSON fetch failed: ${r.status}`);
return r.json();
})
.then(data => { setItems(data.explanations || []); setLoading(false); })
.catch(() => { setItems([]); setLoading(false); });
}, [jobId]);
return (
{/* FOSSYL_AI_KICKER_COPY — payload description comes from window.FOSSYL_SIGNATURES_PAYLOAD_DESC (app.jsx).
Copy-drift validator in graph.jsx asserts both strings reference the same field-type tokens. */}
{(window.FOSSYL_AI_KICKER_COPY = "Signatures only — " + (window.FOSSYL_SIGNATURES_PAYLOAD_DESC||"") + " — are sent to Anthropic's external API at api.anthropic.com (claude-sonnet-4-6). Implementation bodies are never transmitted. Fossyl retains nothing after report generation. Tone is deliberately dry; verify before acting.") && null}
{loading && (
)}
{!loading && items && items.length > 0 && (
{items.map((item, i) => (
{item.path && {item.path.split('/').pop()} }
{item.name || '—'}{item.artifact_type ? ` — ${item.artifact_type}` : ''}
{item.risk > 0 &&
70 ? 'risk' : item.risk > 40 ? 'warn' : 'safe'}`}>risk {item.risk} }
{!item.summary && !item.explanation && !item.business_role && !item.data_flow && !item.change_risk && !item.plone_migration && (
No analysis returned — artifact may be a built-in Plone component or had insufficient source context for the model to analyse.
)}
{item.summary &&
{item.summary}
}
{item.explanation && !item.summary &&
{item.explanation}
}
{item.business_role && (
Business role
{item.business_role}
)}
{item.data_flow && (
Data flow
{item.data_flow}
)}
{item.change_risk && (
Change risk
{item.change_risk}
)}
{item.warning && !item.change_risk && (
Warning. {item.warning}
)}
{item.plone_migration && (
Plone 6 migration
{item.plone_migration}
)}
))}
)}
{!loading && (!items || items.length === 0) && (
No AI explanations for this scan
This excavation was run with AI explanations disabled, or was completed before this feature was available.
Re-run with AI plain-English explanations enabled to generate field notes.
)}
);
}
// ── §7 Source browser ─────────────────────────────────────────────────────────
function SourceBrowser({ jobId, go, toast }) {
const sample = `## Python Script: sendReferralFax
## Parameters: patient_id, referral_type, recipient_fax
from Products.CMFCore.utils import getToolByName
patient = context.patient_portal.get(patient_id)
if patient is None:
raise KeyError(patient_id)
pdf = context.pdf_template.render(patient, referral_type)
gateway = context.portal_properties.fax_gateway
gateway.dispatch(recipient_fax, pdf)
ledger = context.billing.claim_to_ledger
ledger(patient_id=patient_id, event='referral_fax',
amount=context.portal_properties.fax_fee,
ref=recipient_fax)
return {'ok': True, 'timestamp': DateTime()}`;
return (
{[['F-0427', 'sendReferralFax', 'Python Script', 52], ['F-0418', 'admitPatient', 'Python Script', 100], ['F-0392', 'ward_roster_pt', 'Page Template', 35]].map((f, i) => (
{f[1]} TTW
{f[0]} · {f[2]}
))}
F-0427 sendReferralFax TTW · risk 52
{sample}
);
}
// ── §8 Schema map ─────────────────────────────────────────────────────────────
function SchemaSection({ realStats }) {
// Only show real data if schema was attached to this scan.
// We detect this by checking if the job has schema stats — for now we
// always show the "no schema" state since schema is not yet wired to DynamoDB.
// When a SQL file IS attached, docker_worker.py will write schema_table_count > 0.
const hasSchema = realStats?.schema_table_count > 0;
return (
{!hasSchema && (
No schema attached to this scan
Attach a phpMyAdmin structure-only SQL export on the upload page to generate the schema cross-reference map.
This enables §8 and adds cross-links between Zope artifacts and database tables.
window.dispatchEvent(new CustomEvent('fossyl-nav', { detail: 'upload' }))}>
Run new excavation with schema →
)}
);
}
Object.assign(window, { Dashboard });
// Run after dashboard.jsx is fully initialized — all window globals (FOSSYL_RISK_WEIGHTS,
// FOSSYL_COUPLING_ROWS, FOSSYL_TTW_TABLE, FOSSYL_VERSION_GROUPS) are set by this point.
if (window.validateDemoFixture) window.validateDemoFixture();