@archerjessop/utilities 7.6.0 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export{extractBedrooms,extractPhoneNumber}from"./data/extractors.js";export{calculateCashFlowTooltip,calculateDownPaymentTooltip,parseCashFlowData,parseFinancialData}from"./financial/tooltip-calculations.js";export{generateCapRateTooltipHTML,generateCashFlowTooltipHTML,generateDownPaymentTooltipHTML,generatePriceTooltipHTML}from"./financial/tooltip-content-generators.js";export{setupCapRateClickHandler,setupDiscountButtonHandler,setupDownPaymentClickHandler,setupPriceClickHandler}from"./ui/click-handlers.js";export{CLICKABLE_TOOLTIPS,TOOLTIP_ENABLED_METRICS}from"./ui/tooltip-config.js";export{attachTooltip,hasTooltip,isTooltipVisible,removeAllTooltips,removeTooltip,updateTooltipContent}from"./ui/tooltip-manager.js";export{createNavigationGuard}from"./widget/createNavigationGuard.js";export{createPanel}from"./widget/createPanel.js";export{createAnalyzer}from"./widget/createAnalyzer.js";export{createAnalyzerState}from"./widget/createAnalyzerState.js";export{calculateFinancials}from"./financial/calculateFinancials.js";export{fetchEquity}from"./services/equity.js";export{fetchStrRevenue}from"./services/str-revenue.js";
|
|
1
|
+
export{extractBedrooms,extractPhoneNumber}from"./data/extractors.js";export{calculateCashFlowTooltip,calculateDownPaymentTooltip,parseCashFlowData,parseFinancialData}from"./financial/tooltip-calculations.js";export{generateCapRateTooltipHTML,generateCashFlowTooltipHTML,generateDownPaymentTooltipHTML,generatePriceTooltipHTML}from"./financial/tooltip-content-generators.js";export{setupCapRateClickHandler,setupDiscountButtonHandler,setupDownPaymentClickHandler,setupPriceClickHandler}from"./ui/click-handlers.js";export{CLICKABLE_TOOLTIPS,TOOLTIP_ENABLED_METRICS}from"./ui/tooltip-config.js";export{attachTooltip,hasTooltip,isTooltipVisible,removeAllTooltips,removeTooltip,updateTooltipContent}from"./ui/tooltip-manager.js";export{createNavigationGuard}from"./widget/createNavigationGuard.js";export{createPanel}from"./widget/createPanel.js";export{runReveals}from"./widget/runReveals.js";export{createAnalyzer}from"./widget/createAnalyzer.js";export{createAnalyzerState}from"./widget/createAnalyzerState.js";export{calculateFinancials}from"./financial/calculateFinancials.js";export{fetchEquity}from"./services/equity.js";export{fetchStrRevenue}from"./services/str-revenue.js";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createNavigationGuard as e}from"./createNavigationGuard.js";import{createPanel as t}from"./createPanel.js";import{setupPriceClickHandler as
|
|
1
|
+
import{createNavigationGuard as e}from"./createNavigationGuard.js";import{createPanel as t}from"./createPanel.js";import{runReveals as a}from"./runReveals.js";import{setupPriceClickHandler as n,setupCapRateClickHandler as r,setupDownPaymentClickHandler as o,setupDiscountButtonHandler as c}from"../ui/click-handlers.js";import{calculateFinancials as i}from"../financial/calculateFinancials.js";import{calculateDOM as l}from"../../date/utilities.js";function createPipeline({adapter:s,config:p,ctx:u,exportOps:d,finance:m,render:y,resolveCssUrls:f,services:g}){const{state:E,updateState:b}=u,listingId=()=>s.getListingId(window.location.href);async function updateFooterData(){const t=e(listingId);if(t.capture(),p.reveals?.length&&(await a(p.reveals),t.isStale()))return;const s=m.scrapeAndApply();if(!s)return console.error("❌ Malformed listing data — missing a contract field, refusing to render"),void y.updateElement("prop-name","Data error — see console");const d=s.unitCount??4;b({numberOfUnits:d});const f=document.getElementById("ln-units-input");if(f&&(f.value=d),d>11){const e=document.getElementById("ln-interest-rate-type");e&&"dscr_commercial"!==e.value&&(e.value="dscr_commercial",b({currentInterestRateType:"dscr_commercial"}))}y.updateElement("prop-name",s.name),y.updateElement("prop-price",E.priceWasDefaulted?"No price":s.price),y.updateElement("prop-contact",s.contact),y.updateElement("prop-phone",s.phone),y.updateElement("prop-dom",l(s.listingDate)),y.updatePriceLabel(),y.updateCapRateLabel(),y.syncUnitsFieldForType(E.currentPropertyType,s.bedroomCount),function(e){const t=document.getElementById("prop-name");t&&e.name&&"Not found"!==e.name&&(t.style.cursor="pointer",t.style.textDecoration="underline",t.onclick=()=>{const t=`https://www.google.com/maps/search/${encodeURIComponent(e.name)}`;window.open(t,"_blank")});const a={getCurrentPrice:y.getCurrentPrice,recalculateFinancials:m.recalculateFinancials,state:E,updatePercentageLabels:y.updatePercentageLabels,updatePriceLabel:y.updatePriceLabel,updateState:b},i=document.getElementById("prop-price");n(i,i?.closest(".metric")?.querySelector(".metric-label"),a);const l=document.getElementById("prop-cap");r(l,l?.closest(".metric")?.querySelector(".metric-label"),a);const s=document.getElementById("prop-down");o(s,s?.closest(".metric")?.querySelector(".metric-label"),a),c(document.getElementById("ln-discount-btn"),a)}(s);const C=E.isUsingEstimatedCapRate?`${E.currentEstimatedCapRate}%`:s.capRate,P=await i(u,s.price,C,E.currentPropertyType,s.name);if(t.isStale())return;y.applyFinancials(P),y.updateActiveCapDisplay();const w=await g.loadLeadStatus(s.name);if(t.isStale())return;y.updateElement("prop-lead-status",w.leadStatus),y.updateLeadStatusTooltip(w);const S=await g.loadStrValue(s.name,t);if(t.isStale())return;if(S&&"str"===E.currentPropertyType&&(b({baseNOI:null}),await m.recalculateFinancials(),t.isStale()))return;const F=await g.loadEquity(s.name,t);t.isStale()||y.updateElement("prop-equity",F)}let C=!1,P=null;return{runPipeline:function(){C=!1,P&&(P.disconnect(),P=null),t({callbacks:{onExportClick:d.handleExportClick,onInterestRateTypeChange:()=>m.recalculateFinancials(),onPropertyTypeChange:()=>{m.handlePropertyTypeChange(),y.updateCapRateLabel();const e=s.scrape();y.syncUnitsFieldForType(E.currentPropertyType,e?.bedroomCount),m.recalculateFinancials()},state:E,updateState:b},cssUrls:f(p.cssFiles),defaultPropertyType:p.defaultPropertyType});const runUpdateOnce=async()=>{C||(C=!0,await updateFooterData())},tryImmediateUpdate=()=>{const e=document.getElementById("prop-name"),t=document.getElementById("prop-price");return!!(e&&t&&e.textContent.trim()&&t.textContent.trim())&&(runUpdateOnce(),!0)};tryImmediateUpdate()||(P=new MutationObserver((e,t)=>{tryImmediateUpdate()&&(t.disconnect(),P=null)}),P.observe(document.body,{childList:!0,subtree:!0}),"complete"!==document.readyState&&window.addEventListener("load",()=>{setTimeout(()=>{C||(runUpdateOnce(),P&&(P.disconnect(),P=null))},5e3)}))},updateFooterData:updateFooterData}}export{createPipeline};
|
|
2
2
|
//# sourceMappingURL=pipeline.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.js","sources":["../../../src/browser/widget/pipeline.js"],"sourcesContent":["// Pipeline unit: the re-runnable footer pipeline — builds the shared panel, wires the clickable\r\n// elements, runs the main async update (scrape -> financials -> lead status -> STR -> equity with\r\n// the navigation guard between awaits), and drives the immediate/observer/load entry points.\r\n// Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createNavigationGuard } from \"./createNavigationGuard.js\";\r\nimport { createPanel } from \"./createPanel.js\";\r\nimport {\r\n setupCapRateClickHandler,\r\n setupDiscountButtonHandler,\r\n setupDownPaymentClickHandler,\r\n setupPriceClickHandler,\r\n} from \"../ui/click-handlers.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { calculateDOM } from \"../../date/utilities.js\";\r\n\r\nexport function createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services }) {\r\n const { state, updateState } = ctx;\r\n const listingId = () => adapter.getListingId(window.location.href);\r\n\r\n function setupClickableElements(data) {\r\n const nameElement = document.getElementById(\"prop-name\");\r\n if (nameElement && data.name && data.name !== \"Not found\") {\r\n nameElement.style.cursor = \"pointer\";\r\n nameElement.style.textDecoration = \"underline\";\r\n nameElement.onclick = () => {\r\n const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(data.name)}`;\r\n window.open(searchUrl, \"_blank\");\r\n };\r\n }\r\n\r\n // The shared click-handlers read callbacks.state / callbacks.updateState — the engine's\r\n // ctx is injected here (no global-state coupling).\r\n const callbacks = {\r\n getCurrentPrice: render.getCurrentPrice,\r\n recalculateFinancials: finance.recalculateFinancials,\r\n state,\r\n updatePercentageLabels: render.updatePercentageLabels,\r\n updatePriceLabel: render.updatePriceLabel,\r\n updateState,\r\n };\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n setupPriceClickHandler(priceElement, priceElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const capElement = document.getElementById(\"prop-cap\");\r\n setupCapRateClickHandler(capElement, capElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const downElement = document.getElementById(\"prop-down\");\r\n setupDownPaymentClickHandler(downElement, downElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n setupDiscountButtonHandler(document.getElementById(\"ln-discount-btn\"), callbacks);\r\n }\r\n\r\n async function updateFooterData() {\r\n // The listing this run is for. On an SPA the page can navigate mid-flight; after each\r\n // await we drop out if the identity changed, so a stale run never writes onto another\r\n // listing's panel. On a full-reload site getListingId is stable, so isStale() is always\r\n // false and this is a no-op.\r\n const guard = createNavigationGuard(listingId);\r\n guard.capture();\r\n\r\n const data = finance.scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — missing a contract field, refusing to render\");\r\n render.updateElement(\"prop-name\", \"Data error — see console\");\r\n return;\r\n }\r\n\r\n const unitCount = data.unitCount ?? 4;\r\n updateState({ numberOfUnits: unitCount });\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) unitsInput.value = unitCount;\r\n\r\n if (unitCount > 11) {\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n }\r\n }\r\n\r\n render.updateElement(\"prop-name\", data.name);\r\n // Display guard (H2): a defaulted price shows \"No price\"; the metrics fall through to N/A.\r\n render.updateElement(\"prop-price\", state.priceWasDefaulted ? \"No price\" : data.price);\r\n render.updateElement(\"prop-contact\", data.contact);\r\n render.updateElement(\"prop-phone\", data.phone);\r\n render.updateElement(\"prop-dom\", calculateDOM(data.listingDate));\r\n\r\n render.updatePriceLabel();\r\n render.updateCapRateLabel();\r\n render.syncUnitsFieldForType(state.currentPropertyType, data.bedroomCount);\r\n setupClickableElements(data);\r\n\r\n const calculationCapRate = state.isUsingEstimatedCapRate ? `${state.currentEstimatedCapRate}%` : data.capRate;\r\n const financials = await calculateFinancials(ctx, data.price, calculationCapRate, state.currentPropertyType, data.name);\r\n if (guard.isStale()) return;\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n\r\n const loiData = await services.loadLeadStatus(data.name);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-lead-status\", loiData.leadStatus);\r\n render.updateLeadStatusTooltip(loiData);\r\n\r\n // STR revenue seam: the footer already shows the 5.5%-of-price estimate. If the backend\r\n // returns real data, recompute the STR NOI with it. Dormant until that backend ships.\r\n const strResult = await services.loadStrValue(data.name, guard);\r\n if (guard.isStale()) return;\r\n if (strResult && state.currentPropertyType === \"str\") {\r\n updateState({ baseNOI: null });\r\n await finance.recalculateFinancials();\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const equity = await services.loadEquity(data.name, guard);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-equity\", equity);\r\n }\r\n\r\n // One running pipeline at a time. Re-runnable so the SPA watcher can rebuild per listing;\r\n // the observer is tracked so a re-run detaches the previous one.\r\n let footerUpdated = false;\r\n let pipelineObserver = null;\r\n\r\n function runPipeline() {\r\n footerUpdated = false;\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n\r\n createPanel({\r\n callbacks: {\r\n onExportClick: exportOps.handleExportClick,\r\n onInterestRateTypeChange: () => finance.recalculateFinancials(),\r\n onPropertyTypeChange: () => {\r\n finance.handlePropertyTypeChange();\r\n render.updateCapRateLabel();\r\n const listing = adapter.scrape();\r\n render.syncUnitsFieldForType(state.currentPropertyType, listing?.bedroomCount);\r\n finance.recalculateFinancials();\r\n },\r\n state,\r\n updateState,\r\n },\r\n cssUrls: resolveCssUrls(config.cssFiles),\r\n defaultPropertyType: config.defaultPropertyType,\r\n });\r\n\r\n const runUpdateOnce = async () => {\r\n if (footerUpdated) return;\r\n footerUpdated = true;\r\n await updateFooterData();\r\n };\r\n\r\n const tryImmediateUpdate = () => {\r\n const nameEl = document.getElementById(\"prop-name\");\r\n const priceEl = document.getElementById(\"prop-price\");\r\n if (nameEl && priceEl && nameEl.textContent.trim() && priceEl.textContent.trim()) {\r\n runUpdateOnce();\r\n return true;\r\n }\r\n return false;\r\n };\r\n\r\n if (tryImmediateUpdate()) return;\r\n\r\n pipelineObserver = new MutationObserver((mutations, obs) => {\r\n if (tryImmediateUpdate()) {\r\n obs.disconnect();\r\n pipelineObserver = null;\r\n }\r\n });\r\n pipelineObserver.observe(document.body, { childList: true, subtree: true });\r\n\r\n // Safety fallback before the page has loaded; SPA navigations fire after load, so the\r\n // observer (not load) drives those.\r\n if (document.readyState !== \"complete\") {\r\n window.addEventListener(\"load\", () => {\r\n setTimeout(() => {\r\n if (!footerUpdated) {\r\n runUpdateOnce();\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n }\r\n }, 5000);\r\n });\r\n }\r\n }\r\n\r\n return { runPipeline, updateFooterData };\r\n}\r\n"],"names":["createPipeline","adapter","config","ctx","exportOps","finance","render","resolveCssUrls","services","state","updateState","listingId","getListingId","window","location","href","async","updateFooterData","guard","createNavigationGuard","capture","data","scrapeAndApply","console","error","updateElement","unitCount","numberOfUnits","unitsInput","document","getElementById","value","irDropdown","currentInterestRateType","name","priceWasDefaulted","price","contact","phone","calculateDOM","listingDate","updatePriceLabel","updateCapRateLabel","syncUnitsFieldForType","currentPropertyType","bedroomCount","nameElement","style","cursor","textDecoration","onclick","searchUrl","encodeURIComponent","open","callbacks","getCurrentPrice","recalculateFinancials","updatePercentageLabels","priceElement","setupPriceClickHandler","closest","querySelector","capElement","setupCapRateClickHandler","downElement","setupDownPaymentClickHandler","setupDiscountButtonHandler","setupClickableElements","calculationCapRate","isUsingEstimatedCapRate","currentEstimatedCapRate","capRate","financials","calculateFinancials","isStale","applyFinancials","updateActiveCapDisplay","loiData","loadLeadStatus","leadStatus","updateLeadStatusTooltip","strResult","loadStrValue","baseNOI","equity","loadEquity","footerUpdated","pipelineObserver","runPipeline","disconnect","createPanel","onExportClick","handleExportClick","onInterestRateTypeChange","onPropertyTypeChange","handlePropertyTypeChange","listing","scrape","cssUrls","cssFiles","defaultPropertyType","runUpdateOnce","tryImmediateUpdate","nameEl","priceEl","textContent","trim","MutationObserver","mutations","obs","observe","body","childList","subtree","readyState","addEventListener","setTimeout"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pipeline.js","sources":["../../../src/browser/widget/pipeline.js"],"sourcesContent":["// Pipeline unit: the re-runnable footer pipeline — builds the shared panel, wires the clickable\r\n// elements, runs the main async update (scrape -> financials -> lead status -> STR -> equity with\r\n// the navigation guard between awaits), and drives the immediate/observer/load entry points.\r\n// Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createNavigationGuard } from \"./createNavigationGuard.js\";\r\nimport { createPanel } from \"./createPanel.js\";\r\nimport { runReveals } from \"./runReveals.js\";\r\nimport {\r\n setupCapRateClickHandler,\r\n setupDiscountButtonHandler,\r\n setupDownPaymentClickHandler,\r\n setupPriceClickHandler,\r\n} from \"../ui/click-handlers.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { calculateDOM } from \"../../date/utilities.js\";\r\n\r\nexport function createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services }) {\r\n const { state, updateState } = ctx;\r\n const listingId = () => adapter.getListingId(window.location.href);\r\n\r\n function setupClickableElements(data) {\r\n const nameElement = document.getElementById(\"prop-name\");\r\n if (nameElement && data.name && data.name !== \"Not found\") {\r\n nameElement.style.cursor = \"pointer\";\r\n nameElement.style.textDecoration = \"underline\";\r\n nameElement.onclick = () => {\r\n const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(data.name)}`;\r\n window.open(searchUrl, \"_blank\");\r\n };\r\n }\r\n\r\n // The shared click-handlers read callbacks.state / callbacks.updateState — the engine's\r\n // ctx is injected here (no global-state coupling).\r\n const callbacks = {\r\n getCurrentPrice: render.getCurrentPrice,\r\n recalculateFinancials: finance.recalculateFinancials,\r\n state,\r\n updatePercentageLabels: render.updatePercentageLabels,\r\n updatePriceLabel: render.updatePriceLabel,\r\n updateState,\r\n };\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n setupPriceClickHandler(priceElement, priceElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const capElement = document.getElementById(\"prop-cap\");\r\n setupCapRateClickHandler(capElement, capElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const downElement = document.getElementById(\"prop-down\");\r\n setupDownPaymentClickHandler(downElement, downElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n setupDiscountButtonHandler(document.getElementById(\"ln-discount-btn\"), callbacks);\r\n }\r\n\r\n async function updateFooterData() {\r\n // The listing this run is for. On an SPA the page can navigate mid-flight; after each\r\n // await we drop out if the identity changed, so a stale run never writes onto another\r\n // listing's panel. On a full-reload site getListingId is stable, so isStale() is always\r\n // false and this is a no-op.\r\n const guard = createNavigationGuard(listingId);\r\n guard.capture();\r\n\r\n // Click-to-reveal any data gated behind a button (broker phone/email, OM access) so the\r\n // pure scrape() below reads it. Platform-declared (config.reveals); a no-op when absent.\r\n if (config.reveals?.length) {\r\n await runReveals(config.reveals);\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const data = finance.scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — missing a contract field, refusing to render\");\r\n render.updateElement(\"prop-name\", \"Data error — see console\");\r\n return;\r\n }\r\n\r\n const unitCount = data.unitCount ?? 4;\r\n updateState({ numberOfUnits: unitCount });\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) unitsInput.value = unitCount;\r\n\r\n if (unitCount > 11) {\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n }\r\n }\r\n\r\n render.updateElement(\"prop-name\", data.name);\r\n // Display guard (H2): a defaulted price shows \"No price\"; the metrics fall through to N/A.\r\n render.updateElement(\"prop-price\", state.priceWasDefaulted ? \"No price\" : data.price);\r\n render.updateElement(\"prop-contact\", data.contact);\r\n render.updateElement(\"prop-phone\", data.phone);\r\n render.updateElement(\"prop-dom\", calculateDOM(data.listingDate));\r\n\r\n render.updatePriceLabel();\r\n render.updateCapRateLabel();\r\n render.syncUnitsFieldForType(state.currentPropertyType, data.bedroomCount);\r\n setupClickableElements(data);\r\n\r\n const calculationCapRate = state.isUsingEstimatedCapRate ? `${state.currentEstimatedCapRate}%` : data.capRate;\r\n const financials = await calculateFinancials(ctx, data.price, calculationCapRate, state.currentPropertyType, data.name);\r\n if (guard.isStale()) return;\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n\r\n const loiData = await services.loadLeadStatus(data.name);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-lead-status\", loiData.leadStatus);\r\n render.updateLeadStatusTooltip(loiData);\r\n\r\n // STR revenue seam: the footer already shows the 5.5%-of-price estimate. If the backend\r\n // returns real data, recompute the STR NOI with it. Dormant until that backend ships.\r\n const strResult = await services.loadStrValue(data.name, guard);\r\n if (guard.isStale()) return;\r\n if (strResult && state.currentPropertyType === \"str\") {\r\n updateState({ baseNOI: null });\r\n await finance.recalculateFinancials();\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const equity = await services.loadEquity(data.name, guard);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-equity\", equity);\r\n }\r\n\r\n // One running pipeline at a time. Re-runnable so the SPA watcher can rebuild per listing;\r\n // the observer is tracked so a re-run detaches the previous one.\r\n let footerUpdated = false;\r\n let pipelineObserver = null;\r\n\r\n function runPipeline() {\r\n footerUpdated = false;\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n\r\n createPanel({\r\n callbacks: {\r\n onExportClick: exportOps.handleExportClick,\r\n onInterestRateTypeChange: () => finance.recalculateFinancials(),\r\n onPropertyTypeChange: () => {\r\n finance.handlePropertyTypeChange();\r\n render.updateCapRateLabel();\r\n const listing = adapter.scrape();\r\n render.syncUnitsFieldForType(state.currentPropertyType, listing?.bedroomCount);\r\n finance.recalculateFinancials();\r\n },\r\n state,\r\n updateState,\r\n },\r\n cssUrls: resolveCssUrls(config.cssFiles),\r\n defaultPropertyType: config.defaultPropertyType,\r\n });\r\n\r\n const runUpdateOnce = async () => {\r\n if (footerUpdated) return;\r\n footerUpdated = true;\r\n await updateFooterData();\r\n };\r\n\r\n const tryImmediateUpdate = () => {\r\n const nameEl = document.getElementById(\"prop-name\");\r\n const priceEl = document.getElementById(\"prop-price\");\r\n if (nameEl && priceEl && nameEl.textContent.trim() && priceEl.textContent.trim()) {\r\n runUpdateOnce();\r\n return true;\r\n }\r\n return false;\r\n };\r\n\r\n if (tryImmediateUpdate()) return;\r\n\r\n pipelineObserver = new MutationObserver((mutations, obs) => {\r\n if (tryImmediateUpdate()) {\r\n obs.disconnect();\r\n pipelineObserver = null;\r\n }\r\n });\r\n pipelineObserver.observe(document.body, { childList: true, subtree: true });\r\n\r\n // Safety fallback before the page has loaded; SPA navigations fire after load, so the\r\n // observer (not load) drives those.\r\n if (document.readyState !== \"complete\") {\r\n window.addEventListener(\"load\", () => {\r\n setTimeout(() => {\r\n if (!footerUpdated) {\r\n runUpdateOnce();\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n }\r\n }, 5000);\r\n });\r\n }\r\n }\r\n\r\n return { runPipeline, updateFooterData };\r\n}\r\n"],"names":["createPipeline","adapter","config","ctx","exportOps","finance","render","resolveCssUrls","services","state","updateState","listingId","getListingId","window","location","href","async","updateFooterData","guard","createNavigationGuard","capture","reveals","length","runReveals","isStale","data","scrapeAndApply","console","error","updateElement","unitCount","numberOfUnits","unitsInput","document","getElementById","value","irDropdown","currentInterestRateType","name","priceWasDefaulted","price","contact","phone","calculateDOM","listingDate","updatePriceLabel","updateCapRateLabel","syncUnitsFieldForType","currentPropertyType","bedroomCount","nameElement","style","cursor","textDecoration","onclick","searchUrl","encodeURIComponent","open","callbacks","getCurrentPrice","recalculateFinancials","updatePercentageLabels","priceElement","setupPriceClickHandler","closest","querySelector","capElement","setupCapRateClickHandler","downElement","setupDownPaymentClickHandler","setupDiscountButtonHandler","setupClickableElements","calculationCapRate","isUsingEstimatedCapRate","currentEstimatedCapRate","capRate","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","loiData","loadLeadStatus","leadStatus","updateLeadStatusTooltip","strResult","loadStrValue","baseNOI","equity","loadEquity","footerUpdated","pipelineObserver","runPipeline","disconnect","createPanel","onExportClick","handleExportClick","onInterestRateTypeChange","onPropertyTypeChange","handlePropertyTypeChange","listing","scrape","cssUrls","cssFiles","defaultPropertyType","runUpdateOnce","tryImmediateUpdate","nameEl","priceEl","textContent","trim","MutationObserver","mutations","obs","observe","body","childList","subtree","readyState","addEventListener","setTimeout"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function waitForSelector(e,t){return new Promise(r=>{let o=Math.max(0,Math.ceil(t/100));const check=()=>{document.querySelector(e)?r(!0):o--<=0?r(!1):setTimeout(check,100)};check()})}function delay(e){return new Promise(t=>setTimeout(t,e))}async function runReveals(e){if(Array.isArray(e)&&0!==e.length)for(const t of e){if(!t||"string"!=typeof t.trigger)continue;const e=Number.isFinite(t.timeout)?t.timeout:1500;if(t.waitFor&&document.querySelector(t.waitFor))continue;const r=document.querySelector(t.trigger);if(r){try{r.click()}catch(e){console.error(`❌ Reveal "${t.name||t.trigger}" click failed:`,e);continue}t.waitFor?await waitForSelector(t.waitFor,e):await delay(e)}}}export{runReveals};
|
|
2
|
+
//# sourceMappingURL=runReveals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runReveals.js","sources":["../../../src/browser/widget/runReveals.js"],"sourcesContent":["// Shared \"click to reveal\" runner. Some listing sites gate data (broker phone, email, OM\r\n// access) behind a button the user must click. A platform declares these as DATA in its\r\n// config.reveals; the engine awaits runReveals(config.reveals) BEFORE scrape() so the pure\r\n// scraper reads the already-revealed DOM. The engine stays generic — it knows nothing about\r\n// phones; the platform/config dictates how (which selectors) and when (which page).\r\n//\r\n// Each reveal: { name?, trigger, waitFor?, timeout? }\r\n// trigger — CSS selector for the element to click (comma lists allowed)\r\n// waitFor — CSS selector for the content the click reveals; also the idempotency check\r\n// (if it is already present, the trigger is NOT clicked again on a re-run)\r\n// timeout — ms to wait for waitFor to appear before giving up (default 1500)\r\n// When waitFor is omitted, the runner clicks then waits the full timeout (a fixed delay).\r\n\r\nconst DEFAULT_TIMEOUT = 1500;\r\nconst POLL_INTERVAL = 100;\r\n\r\n// Resolve once `selector` is in the DOM, or after `timeout` ms — whichever comes first.\r\n// Poll-count based (not Date.now) so it behaves under both real and faked timers.\r\nfunction waitForSelector(selector, timeout) {\r\n return new Promise((resolve) => {\r\n let remaining = Math.max(0, Math.ceil(timeout / POLL_INTERVAL));\r\n const check = () => {\r\n if (document.querySelector(selector)) {\r\n resolve(true);\r\n return;\r\n }\r\n if (remaining-- <= 0) {\r\n resolve(false);\r\n return;\r\n }\r\n setTimeout(check, POLL_INTERVAL);\r\n };\r\n check();\r\n });\r\n}\r\n\r\n// Wait a fixed number of ms (used when a reveal has no waitFor selector).\r\nfunction delay(ms) {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n\r\nexport async function runReveals(reveals) {\r\n if (!Array.isArray(reveals) || reveals.length === 0) return;\r\n\r\n for (const reveal of reveals) {\r\n if (!reveal || typeof reveal.trigger !== \"string\") continue;\r\n const timeout = Number.isFinite(reveal.timeout) ? reveal.timeout : DEFAULT_TIMEOUT;\r\n\r\n // Idempotent: if the revealed content is already on the page, skip the click. Lets the\r\n // pipeline re-run (SPA) without re-triggering, and no-ops on pages that ship the data.\r\n if (reveal.waitFor && document.querySelector(reveal.waitFor)) continue;\r\n\r\n const triggerEl = document.querySelector(reveal.trigger);\r\n if (!triggerEl) continue;\r\n\r\n try {\r\n triggerEl.click();\r\n } catch (error) {\r\n console.error(`❌ Reveal \"${reveal.name || reveal.trigger}\" click failed:`, error);\r\n continue;\r\n }\r\n\r\n if (reveal.waitFor) {\r\n await waitForSelector(reveal.waitFor, timeout);\r\n } else {\r\n await delay(timeout);\r\n }\r\n }\r\n}\r\n"],"names":["waitForSelector","selector","timeout","Promise","resolve","remaining","Math","max","ceil","check","document","querySelector","setTimeout","delay","ms","async","runReveals","reveals","Array","isArray","length","reveal","trigger","Number","isFinite","waitFor","triggerEl","click","error","console","name"],"mappings":"AAkBA,SAASA,gBAAgBC,EAAUC,GACjC,OAAO,IAAIC,QAASC,IAClB,IAAIC,EAAYC,KAAKC,IAAI,EAAGD,KAAKE,KAAKN,EANpB,MAOlB,MAAMO,MAAQ,KACRC,SAASC,cAAcV,GACzBG,GAAQ,GAGNC,KAAe,EACjBD,GAAQ,GAGVQ,WAAWH,MAhBK,MAkBlBA,SAEJ,CAGA,SAASI,MAAMC,GACb,OAAO,IAAIX,QAASC,GAAYQ,WAAWR,EAASU,GACtD,CAEOC,eAAeC,WAAWC,GAC/B,GAAKC,MAAMC,QAAQF,IAA+B,IAAnBA,EAAQG,OAEvC,IAAK,MAAMC,KAAUJ,EAAS,CAC5B,IAAKI,GAAoC,iBAAnBA,EAAOC,QAAsB,SACnD,MAAMpB,EAAUqB,OAAOC,SAASH,EAAOnB,SAAWmB,EAAOnB,QAjCrC,KAqCpB,GAAImB,EAAOI,SAAWf,SAASC,cAAcU,EAAOI,SAAU,SAE9D,MAAMC,EAAYhB,SAASC,cAAcU,EAAOC,SAChD,GAAKI,EAAL,CAEA,IACEA,EAAUC,OACZ,CAAE,MAAOC,GACPC,QAAQD,MAAM,aAAaP,EAAOS,MAAQT,EAAOC,yBAA0BM,GAC3E,QACF,CAEIP,EAAOI,cACHzB,gBAAgBqB,EAAOI,QAASvB,SAEhCW,MAAMX,EAZE,CAclB,CACF"}
|