@archerjessop/utilities 7.6.0 → 7.9.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.
@@ -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{computeManualOverrideNOI as t,resolveCapRateProvenance as a}from"../financial/capRate.js";import{calculateFinancials as e}from"../financial/calculateFinancials.js";import{FINANCIAL_CONSTANTS as i}from"../../config/financial.js";const n=["name","price","capRate","contact","phone","listingDate"];function isValidListingShape(t){return!(!t||"object"!=typeof t)&&n.every(a=>"string"==typeof t[a])}function createFinance({ctx:n,adapter:r,render:p}){const{state:c,updateState:l}=n;function applyCapRate(t){const{isDefault:e,estimated:n,num:r,displayCap:p}=a(t.capRate,100*i.DEFAULT_CAP_RATE);if(t.capRate=p,e)return c.originalCapRate||l({originalCapRate:p}),c.originalMultifamilyCapRate||l({originalMultifamilyCapRate:`${r}%`}),void l({currentEstimatedCapRate:r,isUsingEstimatedCapRate:!0,originalEstimatedCapRate:r});l({isUsingEstimatedCapRate:n}),n&&null!==r&&l({currentEstimatedCapRate:r}),c.originalCapRate||l({originalCapRate:p}),c.originalMultifamilyCapRate||null===r||l({originalMultifamilyCapRate:`${r}%`})}return{applyCapRate:applyCapRate,handlePropertyTypeChange:function(){const t=document.getElementById("ln-property-type");if(!t)return;const a=t.value;return l({currentPropertyType:a}),"str"!==a&&l({cachedSTRData:null}),l({baseNOI:null}),a},recalculateFinancials:async function(){const a=document.getElementById("prop-price");if(document.getElementById("prop-name"),!a)return;const i=p.getCurrentPrice()||a.textContent;let r;if(c.isUsingEstimatedCapRate)r=`${c.currentEstimatedCapRate}%*`;else{const t=document.getElementById("prop-cap");r=t?t.textContent:"8%"}if("str"===c.currentPropertyType&&l({cachedSTRData:null}),c.capManuallySet){const a=t(c.originalPrice||i,c.currentEstimatedCapRate);null!=a&&l({baseNOI:a})}const o=await e(n,i,r,c.currentPropertyType);p.applyFinancials(o),p.updateActiveCapDisplay()},scrapeAndApply:function(){const t=r.scrape();if(!isValidListingShape(t))return null;const a=t.priceWasDefaulted??"Not found"===t.price;return l({priceWasDefaulted:a}),a||c.originalPrice||l({originalPrice:t.originalPrice??t.price}),applyCapRate(t),t}}}export{createFinance,isValidListingShape};
1
+ import{computeManualOverrideNOI as t,resolveCapRateProvenance as a}from"../financial/capRate.js";import{calculateFinancials as e}from"../financial/calculateFinancials.js";import{FINANCIAL_CONSTANTS as i}from"../../config/financial.js";import{normalizeWhitespace as n}from"../../formatting/text.js";const r=["name","price","capRate","contact","phone","listingDate"];function isValidListingShape(t){return!(!t||"object"!=typeof t)&&r.every(a=>"string"==typeof t[a])}function createFinance({ctx:p,adapter:c,render:l}){const{state:o,updateState:s}=p;function applyCapRate(t){const{isDefault:e,estimated:n,num:r,displayCap:p}=a(t.capRate,100*i.DEFAULT_CAP_RATE);if(t.capRate=p,e)return o.originalCapRate||s({originalCapRate:p}),o.originalMultifamilyCapRate||s({originalMultifamilyCapRate:`${r}%`}),void s({currentEstimatedCapRate:r,isUsingEstimatedCapRate:!0,originalEstimatedCapRate:r});s({isUsingEstimatedCapRate:n}),n&&null!==r&&s({currentEstimatedCapRate:r}),o.originalCapRate||s({originalCapRate:p}),o.originalMultifamilyCapRate||null===r||s({originalMultifamilyCapRate:`${r}%`})}return{applyCapRate:applyCapRate,handlePropertyTypeChange:function(){const t=document.getElementById("ln-property-type");if(!t)return;const a=t.value;return s({currentPropertyType:a}),"str"!==a&&s({cachedSTRData:null}),s({baseNOI:null}),a},recalculateFinancials:async function(){const a=document.getElementById("prop-price");if(document.getElementById("prop-name"),!a)return;const i=l.getCurrentPrice()||a.textContent;let n;if(o.isUsingEstimatedCapRate)n=`${o.currentEstimatedCapRate}%*`;else{const t=document.getElementById("prop-cap");n=t?t.textContent:"8%"}if("str"===o.currentPropertyType&&s({cachedSTRData:null}),o.capManuallySet){const a=t(o.originalPrice||i,o.currentEstimatedCapRate);null!=a&&s({baseNOI:a})}const r=await e(p,i,n,o.currentPropertyType);l.applyFinancials(r),l.updateActiveCapDisplay()},scrapeAndApply:function(){const t=c.scrape();if(!isValidListingShape(t))return null;for(const a of r)t[a]=n(t[a]);const a=t.priceWasDefaulted??"Not found"===t.price;return s({priceWasDefaulted:a}),a||o.originalPrice||s({originalPrice:t.originalPrice??t.price}),applyCapRate(t),t}}}export{createFinance,isValidListingShape};
2
2
  //# sourceMappingURL=finance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"finance.js","sources":["../../../src/browser/widget/finance.js"],"sourcesContent":["// Finance unit (orchestration): applies cap-rate provenance to ctx, applies the scrape-derived\r\n// state, and recomputes the financial metrics. The pure rules live in financial/capRate.js; this\r\n// module is the thin state/DOM glue around them. Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { computeManualOverrideNOI, resolveCapRateProvenance } from \"../financial/capRate.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\n\r\n// The Listing fields every scraper must return as strings (default \"Not found\"). A missing or\r\n// non-string field means the scraper broke; the engine refuses to render/export rather than\r\n// letting `undefined` flow into the NOI/COCR math and silently paint wrong numbers (fail-loud).\r\nconst LISTING_CONTRACT_FIELDS = [\"name\", \"price\", \"capRate\", \"contact\", \"phone\", \"listingDate\"];\r\n\r\nexport function isValidListingShape(data) {\r\n if (!data || typeof data !== \"object\") return false;\r\n return LISTING_CONTRACT_FIELDS.every((field) => typeof data[field] === \"string\");\r\n}\r\n\r\nexport function createFinance({ ctx, adapter, render }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Resolve the cap-rate provenance from the scraped string and write the cap state the\r\n // financial calc + discount handlers read. The default (DEFAULT_CAP_RATE * 100 = 5,\r\n // whole-number percent) fixes the latent no-cap bug where the decimal 0.05 was stored\r\n // and then divided by 100, computing NOI at 0.05%.\r\n function applyCapRate(listing) {\r\n const { isDefault, estimated, num, displayCap } = resolveCapRateProvenance(\r\n listing.capRate,\r\n FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100\r\n );\r\n listing.capRate = displayCap;\r\n\r\n if (isDefault) {\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate) updateState({ originalMultifamilyCapRate: `${num}%` });\r\n updateState({\r\n currentEstimatedCapRate: num,\r\n isUsingEstimatedCapRate: true,\r\n originalEstimatedCapRate: num,\r\n });\r\n return;\r\n }\r\n\r\n updateState({ isUsingEstimatedCapRate: estimated });\r\n if (estimated && num !== null) updateState({ currentEstimatedCapRate: num });\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate && num !== null) {\r\n updateState({ originalMultifamilyCapRate: `${num}%` });\r\n }\r\n }\r\n\r\n // Scrape the page and apply the universal scrape-derived state (was extractData's side\r\n // effects). Returns the resolved listing (capRate normalized to a display string) or null\r\n // when the scrape is malformed.\r\n function scrapeAndApply() {\r\n const listing = adapter.scrape();\r\n if (!isValidListingShape(listing)) return null;\r\n\r\n const priceWasDefaulted = listing.priceWasDefaulted ?? (listing.price === \"Not found\");\r\n updateState({ priceWasDefaulted });\r\n\r\n if (!priceWasDefaulted && !state.originalPrice) {\r\n updateState({ originalPrice: listing.originalPrice ?? listing.price });\r\n }\r\n\r\n applyCapRate(listing);\r\n return listing;\r\n }\r\n\r\n function handlePropertyTypeChange() {\r\n const dropdown = document.getElementById(\"ln-property-type\");\r\n if (!dropdown) return;\r\n const newType = dropdown.value;\r\n updateState({ currentPropertyType: newType });\r\n if (newType !== \"str\") updateState({ cachedSTRData: null });\r\n updateState({ baseNOI: null });\r\n return newType;\r\n }\r\n\r\n async function recalculateFinancials() {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const addressElement = document.getElementById(\"prop-name\");\r\n if (!priceElement) return;\r\n\r\n const priceText = render.getCurrentPrice() || priceElement.textContent;\r\n const address = addressElement?.textContent || \"\";\r\n let capRateText;\r\n if (state.isUsingEstimatedCapRate) {\r\n capRateText = `${state.currentEstimatedCapRate}%*`;\r\n } else {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n capRateText = capElement ? capElement.textContent : \"8%\";\r\n }\r\n\r\n if (state.currentPropertyType === \"str\") updateState({ cachedSTRData: null });\r\n\r\n // Manual cap override: clicking the cap rate sets NOI = original price x cap for EVERY type\r\n // (analyst intent), so the active cap moves with the click even for STR/assisted whose NOI\r\n // is otherwise the type estimate / bedroom value. Pre-seed baseNOI so calculateFinancials\r\n // uses it instead of recomputing from the type model.\r\n if (state.capManuallySet) {\r\n const noi = computeManualOverrideNOI(state.originalPrice || priceText, state.currentEstimatedCapRate);\r\n if (noi != null) updateState({ baseNOI: noi });\r\n }\r\n\r\n const financials = await calculateFinancials(ctx, priceText, capRateText, state.currentPropertyType, address);\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n }\r\n\r\n return { applyCapRate, handlePropertyTypeChange, recalculateFinancials, scrapeAndApply };\r\n}\r\n"],"names":["LISTING_CONTRACT_FIELDS","isValidListingShape","data","every","field","createFinance","ctx","adapter","render","state","updateState","applyCapRate","listing","isDefault","estimated","num","displayCap","resolveCapRateProvenance","capRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","originalCapRate","originalMultifamilyCapRate","currentEstimatedCapRate","isUsingEstimatedCapRate","originalEstimatedCapRate","handlePropertyTypeChange","dropdown","document","getElementById","newType","value","currentPropertyType","cachedSTRData","baseNOI","recalculateFinancials","async","priceElement","priceText","getCurrentPrice","textContent","capRateText","capElement","capManuallySet","noi","computeManualOverrideNOI","originalPrice","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","scrapeAndApply","scrape","priceWasDefaulted","price"],"mappings":"2OAWA,MAAMA,EAA0B,CAAC,OAAQ,QAAS,UAAW,UAAW,QAAS,eAE1E,SAASC,oBAAoBC,GAClC,SAAKA,GAAwB,iBAATA,IACbF,EAAwBG,MAAOC,GAAiC,iBAAhBF,EAAKE,GAC9D,CAEO,SAASC,eAAcC,IAAEA,EAAGC,QAAEA,EAAOC,OAAEA,IAC5C,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBJ,EAM/B,SAASK,aAAaC,GACpB,MAAMC,UAAEA,EAASC,UAAEA,EAASC,IAAEA,EAAGC,WAAEA,GAAeC,EAChDL,EAAQM,QAC+B,IAAvCC,EAAoBC,kBAItB,GAFAR,EAAQM,QAAUF,EAEdH,EAQF,OAPKJ,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAA4BZ,EAAY,CAAEY,2BAA4B,GAAGP,YACpFL,EAAY,CACVa,wBAAyBR,EACzBS,yBAAyB,EACzBC,yBAA0BV,IAK9BL,EAAY,CAAEc,wBAAyBV,IACnCA,GAAqB,OAARC,GAAcL,EAAY,CAAEa,wBAAyBR,IACjEN,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAAsC,OAARP,GACvCL,EAAY,CAAEY,2BAA4B,GAAGP,MAEjD,CA6DA,MAAO,CAAEJ,0BAAce,yBAzCvB,WACE,MAAMC,EAAWC,SAASC,eAAe,oBACzC,IAAKF,EAAU,OACf,MAAMG,EAAUH,EAASI,MAIzB,OAHArB,EAAY,CAAEsB,oBAAqBF,IACnB,QAAZA,GAAmBpB,EAAY,CAAEuB,cAAe,OACpDvB,EAAY,CAAEwB,QAAS,OAChBJ,CACT,EAiCiDK,sBA/BjDC,iBACE,MAAMC,EAAeT,SAASC,eAAe,cAE7C,GADuBD,SAASC,eAAe,cAC1CQ,EAAc,OAEnB,MAAMC,EAAY9B,EAAO+B,mBAAqBF,EAAaG,YAE3D,IAAIC,EACJ,GAAIhC,EAAMe,wBACRiB,EAAc,GAAGhC,EAAMc,gCAClB,CACL,MAAMmB,EAAad,SAASC,eAAe,YAC3CY,EAAcC,EAAaA,EAAWF,YAAc,IACtD,CAQA,GANkC,QAA9B/B,EAAMuB,qBAA+BtB,EAAY,CAAEuB,cAAe,OAMlExB,EAAMkC,eAAgB,CACxB,MAAMC,EAAMC,EAAyBpC,EAAMqC,eAAiBR,EAAW7B,EAAMc,yBAClE,MAAPqB,GAAalC,EAAY,CAAEwB,QAASU,GAC1C,CAEA,MAAMG,QAAmBC,EAAoB1C,EAAKgC,EAAWG,EAAahC,EAAMuB,qBAChFxB,EAAOyC,gBAAgBF,GACvBvC,EAAO0C,wBACT,EAEwEC,eAxDxE,WACE,MAAMvC,EAAUL,EAAQ6C,SACxB,IAAKnD,oBAAoBW,GAAU,OAAO,KAE1C,MAAMyC,EAAoBzC,EAAQyC,mBAAwC,cAAlBzC,EAAQ0C,MAQhE,OAPA5C,EAAY,CAAE2C,sBAETA,GAAsB5C,EAAMqC,eAC/BpC,EAAY,CAAEoC,cAAelC,EAAQkC,eAAiBlC,EAAQ0C,QAGhE3C,aAAaC,GACNA,CACT,EA4CF"}
1
+ {"version":3,"file":"finance.js","sources":["../../../src/browser/widget/finance.js"],"sourcesContent":["// Finance unit (orchestration): applies cap-rate provenance to ctx, applies the scrape-derived\r\n// state, and recomputes the financial metrics. The pure rules live in financial/capRate.js; this\r\n// module is the thin state/DOM glue around them. Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { computeManualOverrideNOI, resolveCapRateProvenance } from \"../financial/capRate.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\nimport { normalizeWhitespace } from \"../../formatting/text.js\";\r\n\r\n// The Listing fields every scraper must return as strings (default \"Not found\"). A missing or\r\n// non-string field means the scraper broke; the engine refuses to render/export rather than\r\n// letting `undefined` flow into the NOI/COCR math and silently paint wrong numbers (fail-loud).\r\nconst LISTING_CONTRACT_FIELDS = [\"name\", \"price\", \"capRate\", \"contact\", \"phone\", \"listingDate\"];\r\n\r\nexport function isValidListingShape(data) {\r\n if (!data || typeof data !== \"object\") return false;\r\n return LISTING_CONTRACT_FIELDS.every((field) => typeof data[field] === \"string\");\r\n}\r\n\r\nexport function createFinance({ ctx, adapter, render }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Resolve the cap-rate provenance from the scraped string and write the cap state the\r\n // financial calc + discount handlers read. The default (DEFAULT_CAP_RATE * 100 = 5,\r\n // whole-number percent) fixes the latent no-cap bug where the decimal 0.05 was stored\r\n // and then divided by 100, computing NOI at 0.05%.\r\n function applyCapRate(listing) {\r\n const { isDefault, estimated, num, displayCap } = resolveCapRateProvenance(\r\n listing.capRate,\r\n FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100\r\n );\r\n listing.capRate = displayCap;\r\n\r\n if (isDefault) {\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate) updateState({ originalMultifamilyCapRate: `${num}%` });\r\n updateState({\r\n currentEstimatedCapRate: num,\r\n isUsingEstimatedCapRate: true,\r\n originalEstimatedCapRate: num,\r\n });\r\n return;\r\n }\r\n\r\n updateState({ isUsingEstimatedCapRate: estimated });\r\n if (estimated && num !== null) updateState({ currentEstimatedCapRate: num });\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate && num !== null) {\r\n updateState({ originalMultifamilyCapRate: `${num}%` });\r\n }\r\n }\r\n\r\n // Scrape the page and apply the universal scrape-derived state (was extractData's side\r\n // effects). Returns the resolved listing (capRate normalized to a display string) or null\r\n // when the scrape is malformed.\r\n function scrapeAndApply() {\r\n const listing = adapter.scrape();\r\n if (!isValidListingShape(listing)) return null;\r\n\r\n // Normalize whitespace on every contract string field centrally, so adapters stay pure\r\n // scrapers and no consumer (panel or export) ever sees the interior newlines/tabs that\r\n // site markup splits text across (e.g. a broker name on two lines). \"Not found\" is\r\n // unchanged. This is the single enforcement point for the data contract's \"normalize text\".\r\n for (const field of LISTING_CONTRACT_FIELDS) {\r\n listing[field] = normalizeWhitespace(listing[field]);\r\n }\r\n\r\n const priceWasDefaulted = listing.priceWasDefaulted ?? (listing.price === \"Not found\");\r\n updateState({ priceWasDefaulted });\r\n\r\n if (!priceWasDefaulted && !state.originalPrice) {\r\n updateState({ originalPrice: listing.originalPrice ?? listing.price });\r\n }\r\n\r\n applyCapRate(listing);\r\n return listing;\r\n }\r\n\r\n function handlePropertyTypeChange() {\r\n const dropdown = document.getElementById(\"ln-property-type\");\r\n if (!dropdown) return;\r\n const newType = dropdown.value;\r\n updateState({ currentPropertyType: newType });\r\n if (newType !== \"str\") updateState({ cachedSTRData: null });\r\n updateState({ baseNOI: null });\r\n return newType;\r\n }\r\n\r\n async function recalculateFinancials() {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const addressElement = document.getElementById(\"prop-name\");\r\n if (!priceElement) return;\r\n\r\n const priceText = render.getCurrentPrice() || priceElement.textContent;\r\n const address = addressElement?.textContent || \"\";\r\n let capRateText;\r\n if (state.isUsingEstimatedCapRate) {\r\n capRateText = `${state.currentEstimatedCapRate}%*`;\r\n } else {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n capRateText = capElement ? capElement.textContent : \"8%\";\r\n }\r\n\r\n if (state.currentPropertyType === \"str\") updateState({ cachedSTRData: null });\r\n\r\n // Manual cap override: clicking the cap rate sets NOI = original price x cap for EVERY type\r\n // (analyst intent), so the active cap moves with the click even for STR/assisted whose NOI\r\n // is otherwise the type estimate / bedroom value. Pre-seed baseNOI so calculateFinancials\r\n // uses it instead of recomputing from the type model.\r\n if (state.capManuallySet) {\r\n const noi = computeManualOverrideNOI(state.originalPrice || priceText, state.currentEstimatedCapRate);\r\n if (noi != null) updateState({ baseNOI: noi });\r\n }\r\n\r\n const financials = await calculateFinancials(ctx, priceText, capRateText, state.currentPropertyType, address);\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n }\r\n\r\n return { applyCapRate, handlePropertyTypeChange, recalculateFinancials, scrapeAndApply };\r\n}\r\n"],"names":["LISTING_CONTRACT_FIELDS","isValidListingShape","data","every","field","createFinance","ctx","adapter","render","state","updateState","applyCapRate","listing","isDefault","estimated","num","displayCap","resolveCapRateProvenance","capRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","originalCapRate","originalMultifamilyCapRate","currentEstimatedCapRate","isUsingEstimatedCapRate","originalEstimatedCapRate","handlePropertyTypeChange","dropdown","document","getElementById","newType","value","currentPropertyType","cachedSTRData","baseNOI","recalculateFinancials","async","priceElement","priceText","getCurrentPrice","textContent","capRateText","capElement","capManuallySet","noi","computeManualOverrideNOI","originalPrice","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","scrapeAndApply","scrape","normalizeWhitespace","priceWasDefaulted","price"],"mappings":"0SAYA,MAAMA,EAA0B,CAAC,OAAQ,QAAS,UAAW,UAAW,QAAS,eAE1E,SAASC,oBAAoBC,GAClC,SAAKA,GAAwB,iBAATA,IACbF,EAAwBG,MAAOC,GAAiC,iBAAhBF,EAAKE,GAC9D,CAEO,SAASC,eAAcC,IAAEA,EAAGC,QAAEA,EAAOC,OAAEA,IAC5C,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBJ,EAM/B,SAASK,aAAaC,GACpB,MAAMC,UAAEA,EAASC,UAAEA,EAASC,IAAEA,EAAGC,WAAEA,GAAeC,EAChDL,EAAQM,QAC+B,IAAvCC,EAAoBC,kBAItB,GAFAR,EAAQM,QAAUF,EAEdH,EAQF,OAPKJ,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAA4BZ,EAAY,CAAEY,2BAA4B,GAAGP,YACpFL,EAAY,CACVa,wBAAyBR,EACzBS,yBAAyB,EACzBC,yBAA0BV,IAK9BL,EAAY,CAAEc,wBAAyBV,IACnCA,GAAqB,OAARC,GAAcL,EAAY,CAAEa,wBAAyBR,IACjEN,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAAsC,OAARP,GACvCL,EAAY,CAAEY,2BAA4B,GAAGP,MAEjD,CAqEA,MAAO,CAAEJ,0BAAce,yBAzCvB,WACE,MAAMC,EAAWC,SAASC,eAAe,oBACzC,IAAKF,EAAU,OACf,MAAMG,EAAUH,EAASI,MAIzB,OAHArB,EAAY,CAAEsB,oBAAqBF,IACnB,QAAZA,GAAmBpB,EAAY,CAAEuB,cAAe,OACpDvB,EAAY,CAAEwB,QAAS,OAChBJ,CACT,EAiCiDK,sBA/BjDC,iBACE,MAAMC,EAAeT,SAASC,eAAe,cAE7C,GADuBD,SAASC,eAAe,cAC1CQ,EAAc,OAEnB,MAAMC,EAAY9B,EAAO+B,mBAAqBF,EAAaG,YAE3D,IAAIC,EACJ,GAAIhC,EAAMe,wBACRiB,EAAc,GAAGhC,EAAMc,gCAClB,CACL,MAAMmB,EAAad,SAASC,eAAe,YAC3CY,EAAcC,EAAaA,EAAWF,YAAc,IACtD,CAQA,GANkC,QAA9B/B,EAAMuB,qBAA+BtB,EAAY,CAAEuB,cAAe,OAMlExB,EAAMkC,eAAgB,CACxB,MAAMC,EAAMC,EAAyBpC,EAAMqC,eAAiBR,EAAW7B,EAAMc,yBAClE,MAAPqB,GAAalC,EAAY,CAAEwB,QAASU,GAC1C,CAEA,MAAMG,QAAmBC,EAAoB1C,EAAKgC,EAAWG,EAAahC,EAAMuB,qBAChFxB,EAAOyC,gBAAgBF,GACvBvC,EAAO0C,wBACT,EAEwEC,eAhExE,WACE,MAAMvC,EAAUL,EAAQ6C,SACxB,IAAKnD,oBAAoBW,GAAU,OAAO,KAM1C,IAAK,MAAMR,KAASJ,EAClBY,EAAQR,GAASiD,EAAoBzC,EAAQR,IAG/C,MAAMkD,EAAoB1C,EAAQ0C,mBAAwC,cAAlB1C,EAAQ2C,MAQhE,OAPA7C,EAAY,CAAE4C,sBAETA,GAAsB7C,EAAMqC,eAC/BpC,EAAY,CAAEoC,cAAelC,EAAQkC,eAAiBlC,EAAQ2C,QAGhE5C,aAAaC,GACNA,CACT,EA4CF"}
@@ -1,2 +1,2 @@
1
- import{createNavigationGuard as e}from"./createNavigationGuard.js";import{createPanel as t}from"./createPanel.js";import{setupPriceClickHandler as a,setupCapRateClickHandler as n,setupDownPaymentClickHandler as r,setupDiscountButtonHandler as o}from"../ui/click-handlers.js";import{calculateFinancials as c}from"../financial/calculateFinancials.js";import{calculateDOM as i}from"../../date/utilities.js";function createPipeline({adapter:l,config:p,ctx:s,exportOps:u,finance:d,render:m,resolveCssUrls:y,services:f}){const{state:g,updateState:E}=s,listingId=()=>l.getListingId(window.location.href);async function updateFooterData(){const t=e(listingId);t.capture();const l=d.scrapeAndApply();if(!l)return console.error("❌ Malformed listing data — missing a contract field, refusing to render"),void m.updateElement("prop-name","Data error — see console");const p=l.unitCount??4;E({numberOfUnits:p});const u=document.getElementById("ln-units-input");if(u&&(u.value=p),p>11){const e=document.getElementById("ln-interest-rate-type");e&&"dscr_commercial"!==e.value&&(e.value="dscr_commercial",E({currentInterestRateType:"dscr_commercial"}))}m.updateElement("prop-name",l.name),m.updateElement("prop-price",g.priceWasDefaulted?"No price":l.price),m.updateElement("prop-contact",l.contact),m.updateElement("prop-phone",l.phone),m.updateElement("prop-dom",i(l.listingDate)),m.updatePriceLabel(),m.updateCapRateLabel(),m.syncUnitsFieldForType(g.currentPropertyType,l.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 c={getCurrentPrice:m.getCurrentPrice,recalculateFinancials:d.recalculateFinancials,state:g,updatePercentageLabels:m.updatePercentageLabels,updatePriceLabel:m.updatePriceLabel,updateState:E},i=document.getElementById("prop-price");a(i,i?.closest(".metric")?.querySelector(".metric-label"),c);const l=document.getElementById("prop-cap");n(l,l?.closest(".metric")?.querySelector(".metric-label"),c);const p=document.getElementById("prop-down");r(p,p?.closest(".metric")?.querySelector(".metric-label"),c),o(document.getElementById("ln-discount-btn"),c)}(l);const y=g.isUsingEstimatedCapRate?`${g.currentEstimatedCapRate}%`:l.capRate,b=await c(s,l.price,y,g.currentPropertyType,l.name);if(t.isStale())return;m.applyFinancials(b),m.updateActiveCapDisplay();const C=await f.loadLeadStatus(l.name);if(t.isStale())return;m.updateElement("prop-lead-status",C.leadStatus),m.updateLeadStatusTooltip(C);const P=await f.loadStrValue(l.name,t);if(t.isStale())return;if(P&&"str"===g.currentPropertyType&&(E({baseNOI:null}),await d.recalculateFinancials(),t.isStale()))return;const w=await f.loadEquity(l.name,t);t.isStale()||m.updateElement("prop-equity",w)}let b=!1,C=null;return{runPipeline:function(){b=!1,C&&(C.disconnect(),C=null),t({callbacks:{onExportClick:u.handleExportClick,onInterestRateTypeChange:()=>d.recalculateFinancials(),onPropertyTypeChange:()=>{d.handlePropertyTypeChange(),m.updateCapRateLabel();const e=l.scrape();m.syncUnitsFieldForType(g.currentPropertyType,e?.bedroomCount),d.recalculateFinancials()},state:g,updateState:E},cssUrls:y(p.cssFiles),defaultPropertyType:p.defaultPropertyType});const runUpdateOnce=async()=>{b||(b=!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()||(C=new MutationObserver((e,t)=>{tryImmediateUpdate()&&(t.disconnect(),C=null)}),C.observe(document.body,{childList:!0,subtree:!0}),"complete"!==document.readyState&&window.addEventListener("load",()=>{setTimeout(()=>{b||(runUpdateOnce(),C&&(C.disconnect(),C=null))},5e3)}))},updateFooterData:updateFooterData}}export{createPipeline};
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "7.6.0",
3
+ "version": "7.9.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",