import json, pathlib, textwrap
src_path = pathlib.Path('/Users/iggy/.hermes/profiles/ignite_team/outbound/krb-about-us-source-content.json')
source = json.loads(src_path.read_text())
out = pathlib.Path('/Users/iggy/.hermes/profiles/ignite_team/outbound/krb-about-us-wide-build-pass.js')
js = r'''// KRB WordPress -> Webflow WIDE BUILD PASS: About Us page group
// Paste this whole file into Iggy Code Lab in the KRB Webflow Designer app and run once.
// Scope: staging/designer only. It does NOT publish. It idempotently adds missing section components, then populates broad text/heading props.
// Output: compact console summary + manual-check checklist only.
(async () => {
  const SOURCE = __SOURCE_JSON__;
  const TARGETS = [
    { name: 'Our Campus', id: '6a2b8d959cfde20f4b4e0535', path: '/about-us/our-campus', desired: { 'Section / Text Content': 1, 'Section / Two Column Text & Image': 8, 'Section / Next Pages': 1 } },
    { name: 'Our People', id: '6a2b903289294179fd567656', path: '/about-us/our-people', desired: { 'Section / Text Content': 2, 'Section / Gateway CTA': 1, 'Section / Next Pages': 1 } },
    { name: 'School Board', path: '/about-us/our-people/school-board', desired: { 'Section / Text Content': 1, 'Section / Two Column Text & Image': 9, 'Section / Next Pages': 1 } },
    { name: 'Senior Executive', path: '/about-us/our-people/senior-executive', desired: { 'Section / Text Content': 1, 'Section / Two Column Text & Image': 7, 'Section / Next Pages': 1 } },
    { name: 'Employment', id: '6a2b903b944f448e28b408c3', path: '/about-us/our-people/employment', desired: { 'Section / Text Content': 2, 'Section / Next Pages': 1 } },
    { name: 'Our Policies', id: '6a2b904caada4c776865154b', path: '/about-us/our-policies', desired: { 'Section / Text Content': 1, 'Section / Downloads': 3, 'Section / Next Pages': 1 } },
  ];

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  const safe = async (fn, fb = null) => { try { return await fn(); } catch (e) { return fb; } };
  const describe = async (x) => (typeof helpers !== 'undefined' && helpers?.describe) ? await safe(() => helpers.describe(x), {}) : {};
  const norm = s => String(s || '').toLowerCase().replace(/&amp;/g, '&').replace(/[^a-z0-9]+/g, ' ').trim();
  const textOnly = (htmlOrText) => String(htmlOrText || '').replace(/<style[\s\S]*?<\/style>/gi,' ').replace(/<script[\s\S]*?<\/script>/gi,' ').replace(/<li[^>]*>/gi, '• ').replace(/<br\s*\/?>/gi,'\n').replace(/<\/p>|<\/h\d>|<\/li>|<\/ul>|<\/ol>/gi,'\n').replace(/<[^>]*>/g,' ').replace(/&nbsp;/g,' ').replace(/&amp;/g,'&').replace(/&quot;/g,'"').replace(/&#8217;|&rsquo;/g,'’').replace(/&#8211;|&ndash;/g,'–').replace(/\s+\n/g,'\n').replace(/\n\s+/g,'\n').replace(/[ \t]{2,}/g,' ').trim();
  const hasHtmlToManuallyCheck = (html) => /<\s*(ul|ol|li|h[1-6]|table|blockquote|img|a)\b/i.test(String(html || ''));

  async function pageInfo(p) {
    const d = await describe(p);
    return {
      id: await safe(() => p.getId(), d.id),
      name: await safe(() => p.getName(), d.name),
      publishPath: await safe(() => p.getPublishPath(), d.publishPath),
      type: await safe(() => p.getType(), d.type),
      title: await safe(() => p.getTitle(), d.title),
    };
  }
  async function walk(el, depth=0, out=[]) {
    out.push({ el, depth, desc: await describe(el) });
    const kids = await safe(() => el.getChildren(), []);
    for (const k of (Array.isArray(kids) ? kids : [])) await walk(k, depth + 1, out);
    return out;
  }
  async function compName(el) {
    const c = await safe(() => el.getComponent(), null);
    return c ? await safe(() => c.getName(), null) : null;
  }
  async function componentRows() {
    const rows = [];
    for (const n of await walk(await webflow.getRootElement())) {
      const name = await compName(n.el);
      if (name) rows.push({ el: n.el, name, depth: n.depth, desc: n.desc });
    }
    return rows;
  }
  async function findMainDropTarget() {
    const nodes = await walk(await webflow.getRootElement());
    return (nodes.find(n => n.desc?.type === 'DropTarget') || nodes.find(n => n.desc?.tag === 'main') || nodes[0])?.el || await webflow.getRootElement();
  }
  async function insertInMain(componentName) {
    const component = await webflow.getComponentByName(componentName);
    if (!component) throw new Error(`Component not found: ${componentName}`);
    const target = await findMainDropTarget();
    if (typeof target.append !== 'function') throw new Error(`Append target unavailable for ${componentName}`);
    await target.append(component);
  }
  async function props(el) { return await safe(() => el.searchProps(), []); }
  function bestProp(ps, matchers, used = new Set()) {
    const matches = ps.filter(p => {
      const label = norm(p.display?.label || p.label || p.name || '');
      if (!label || used.has(p.propId || p.id)) return false;
      return matchers.some(m => typeof m === 'string' ? label === norm(m) : m.test(label));
    });
    return matches[0] || null;
  }
  function valueFor(prop, raw, preferHtml = false) {
    const vt = String(prop.valueType || '').toLowerCase();
    if (vt.includes('bool')) return Boolean(raw);
    if (vt.includes('number')) return Number(raw) || 0;
    // Rich text props in this app have been safest with text/html strings; keep html when a prop appears rich/content-like.
    return String(raw == null ? '' : raw);
  }
  async function setByLabel(el, matchers, rawValue, issues, ctx, opts = {}) {
    const ps = await props(el);
    const target = bestProp(ps, matchers, opts.used || new Set());
    if (!target) { issues.push(`${ctx}: prop not found (${opts.label || 'text'})`); return false; }
    const propId = target.propId || target.id;
    await el.setProps([{ propId, value: valueFor(target, rawValue, opts.preferHtml) }]);
    if (opts.used) opts.used.add(propId);
    return true;
  }
  async function setBoolean(el, matchers, value) {
    const ps = await props(el);
    const target = bestProp(ps, matchers);
    if (!target) return false;
    await el.setProps([{ propId: target.propId || target.id, value: Boolean(value) }]);
    return true;
  }
  async function populateHero(el, pageName, issues) {
    await setByLabel(el, [/^heading$/, /^title$/, /page heading/, /hero heading/], pageName, issues, `${pageName} hero`, { label:'heading' });
    await setBoolean(el, [/hide section/, /^hide$/], false);
  }
  async function populateTextContent(el, block, pageName, index, issues, manual) {
    const used = new Set();
    if (block.heading) await setByLabel(el, [/^heading$/, /^title$/, /section heading/, /main heading/, /content heading/], block.heading, issues, `${pageName} text ${index}`, { used, label:'heading' });
    const content = block.html || block.text || '';
    await setByLabel(el, [/paragraph/, /rich text/, /^text$/, /content/, /body/, /copy/], content, issues, `${pageName} text ${index}`, { used, label:'content', preferHtml:true });
    await setBoolean(el, [/hide section/, /^hide$/], false);
    if (hasHtmlToManuallyCheck(block.html)) manual.push(`${pageName}: verify rich-text formatting in Text Content ${index}${/<a\b/i.test(block.html||'') ? ' (links)' : ''}${/<img\b/i.test(block.html||'') ? ' (images)' : ''}`);
  }
  async function populateTwoColumn(el, block, pageName, index, issues, manual) {
    const used = new Set();
    await setByLabel(el, [/^heading$/, /^title$/, /section heading/, /main heading/, /card heading/], block.heading || '', issues, `${pageName} two-col ${index}`, { used, label:'heading' });
    await setByLabel(el, [/paragraph/, /rich text/, /^text$/, /content/, /body/, /copy/], block.html || block.text || '', issues, `${pageName} two-col ${index}`, { used, label:'content', preferHtml:true });
    // Optional layout only when present. Ignore missing layout prop.
    const ps = await props(el);
    const layout = bestProp(ps, [/image position/, /media position/, /layout/]);
    if (layout) await safe(() => el.setProps([{ propId: layout.propId || layout.id, value: valueFor(layout, index % 2 === 0 ? 'Right' : 'Left') }]));
    await setBoolean(el, [/hide section/, /^hide$/], false);
    manual.push(`${pageName}: add/verify image for ${block.heading || `Two Column ${index}`}`);
    if (hasHtmlToManuallyCheck(block.html)) manual.push(`${pageName}: verify rich-text formatting in ${block.heading || `Two Column ${index}`}`);
  }
  async function populateGateway(el, links, pageName, issues, manual) {
    const used = new Set();
    const unique = [];
    for (const l of links || []) if (l.title && !unique.find(x => x.title === l.title)) unique.push(l);
    if (!unique.length) return;
    await setByLabel(el, [/^heading$/, /^title$/, /section heading/, /main heading/], 'Our People', issues, `${pageName} gateway`, { used, label:'heading' });
    // Best-effort for first few card/link props. Component prop labels vary, so unresolved links go to manual checklist.
    for (let i = 0; i < Math.min(unique.length, 3); i++) {
      const n = i + 1;
      const item = unique[i];
      const okTitle = await setByLabel(el, [new RegExp(`(card|link|item).*${n}.*(heading|title|text)`), new RegExp(`${n}.*(heading|title|text)`)], item.title, issues, `${pageName} gateway item ${n}`, { used, label:'item title' });
      await setByLabel(el, [new RegExp(`(card|link|item).*${n}.*(url|href|link)`), new RegExp(`${n}.*(url|href|link)`)], item.url, issues, `${pageName} gateway item ${n}`, { used, label:'item url' });
      if (!okTitle) manual.push(`${pageName}: manually wire Gateway CTA item ${n}: ${item.title} -> ${item.url}`);
    }
    await setBoolean(el, [/hide section/, /^hide$/], false);
  }
  async function populateDownloads(el, block, pageName, index, issues, manual) {
    await setByLabel(el, [/^heading$/, /^title$/, /section heading/, /downloads heading/, /content heading/], block.heading || '', issues, `${pageName} downloads ${index}`, { label:'heading' });
    await setBoolean(el, [/hide section/, /^hide$/], false);
    const files = block.files || [];
    manual.push(`${pageName}: manually attach/download-link ${files.length} file(s) for “${block.heading || `Downloads ${index}`}”`);
  }

  const allPages = [];
  for (const p of await webflow.getAllPagesAndFolders()) allPages.push({ page: p, info: await pageInfo(p) });
  const summary = [];
  const manual = [];
  const issues = [];

  for (const target of TARGETS) {
    const source = SOURCE[target.name];
    const matches = allPages.filter(({info}) => info.id === target.id || info.publishPath === target.path || info.publishPath === target.path + '/' || (info.name === target.name && info.type === 'Page'));
    if (!source || matches.length !== 1) {
      issues.push(`${target.name}: ${!source ? 'missing source' : `page match count ${matches.length}`}`);
      summary.push({ page: target.name, status: 'skipped', inserted: 0, populated: 0 });
      continue;
    }
    await webflow.switchPage(matches[0].page);
    await sleep(700);

    let rows = await componentRows();
    const countsBefore = {};
    for (const r of rows) countsBefore[r.name] = (countsBefore[r.name] || 0) + 1;
    const inserted = [];
    for (const [componentName, desiredCount] of Object.entries(target.desired || {})) {
      let have = countsBefore[componentName] || 0;
      const needed = Math.max(0, desiredCount - have);
      for (let i = 0; i < needed; i++) {
        await insertInMain(componentName);
        inserted.push(componentName);
        countsBefore[componentName] = ++have;
        await sleep(180);
      }
    }
    if (inserted.length) { await sleep(700); rows = await componentRows(); }
    const byName = name => rows.filter(r => r.name === name).map(r => r.el);
    let populated = 0;

    const heroes = byName('Section / Hero');
    if (heroes[0]) { await populateHero(heroes[0], target.name, issues); populated++; }
    else issues.push(`${target.name}: missing Hero`);

    const textEls = byName('Section / Text Content');
    const textBlocks = [...(source.textContent || [])];
    if (target.name === 'Employment' && source.accordions?.length && textBlocks.length < textEls.length) {
      textBlocks.push({ heading: source.accordions[0].groupHeading || 'The benefits of working at our School', text: '' });
      manual.push('Employment: paste/build 4 accordion items in the extra-content slot if available. Items: Current Vacancies; Relief Teaching and General Employment Enquiries; Privacy; Child Protection');
    }
    for (let i = 0; i < Math.min(textEls.length, textBlocks.length); i++) { await populateTextContent(textEls[i], textBlocks[i], target.name, i + 1, issues, manual); populated++; }
    if ((source.textContent || []).length > textEls.length) issues.push(`${target.name}: ${source.textContent.length - textEls.length} Text Content block(s) not placed`);

    const twoEls = byName('Section / Two Column Text & Image');
    for (let i = 0; i < Math.min(twoEls.length, (source.twoColumn || []).length); i++) { await populateTwoColumn(twoEls[i], source.twoColumn[i], target.name, i + 1, issues, manual); populated++; }
    if ((source.twoColumn || []).length > twoEls.length) issues.push(`${target.name}: ${source.twoColumn.length - twoEls.length} Two Column block(s) not placed`);

    const gatewayEls = byName('Section / Gateway CTA');
    if (gatewayEls[0] && source.gatewayLinks?.length) { await populateGateway(gatewayEls[0], source.gatewayLinks, target.name, issues, manual); populated++; }

    const dlEls = byName('Section / Downloads');
    for (let i = 0; i < Math.min(dlEls.length, (source.downloads || []).length); i++) { await populateDownloads(dlEls[i], source.downloads[i], target.name, i + 1, issues, manual); populated++; }
    if ((source.downloads || []).length > dlEls.length) issues.push(`${target.name}: ${source.downloads.length - dlEls.length} Downloads section(s) not placed`);

    const nextEls = byName('Section / Next Pages');
    for (const el of nextEls) await setBoolean(el, [/hide section/, /^hide$/], false);

    const countsAfter = {};
    for (const r of await componentRows()) countsAfter[r.name] = (countsAfter[r.name] || 0) + 1;
    summary.push({ page: target.name, status: 'processed', inserted: inserted.length, populated, hero: countsAfter['Section / Hero'] || 0, text: countsAfter['Section / Text Content'] || 0, twoCol: countsAfter['Section / Two Column Text & Image'] || 0, gateway: countsAfter['Section / Gateway CTA'] || 0, downloads: countsAfter['Section / Downloads'] || 0, next: countsAfter['Section / Next Pages'] || 0 });
  }

  // Dedupe and keep output light.
  const manualChecks = [...new Set(manual)].slice(0, 80);
  const issueChecks = [...new Set(issues)].slice(0, 80);
  console.log('KRB About Us wide build pass complete. No publish performed.');
  console.table(summary);
  if (issueChecks.length) console.warn('Needs attention / unresolved props:', issueChecks);
  if (manualChecks.length) console.warn('Manual checklist:', manualChecks);
  await safe(() => webflow.notify({ type: 'Info', message: `KRB About Us wide pass complete: ${summary.filter(s => s.status === 'processed').length}/${TARGETS.length} pages` }));
  return { ok: true, summary, needsAttention: issueChecks, manualChecklist: manualChecks };
})();
'''
js = js.replace('__SOURCE_JSON__', json.dumps(source, ensure_ascii=False))
out.write_text(js)
print(out)
print(out.stat().st_size)
