← back to Sku Check Skill
pills app: add last-portal-pull cache (reads yw-results, write-through on live)
803ecd65766895f0bfb353be3afbaa4fb80068ca · 2026-07-10 07:51:03 -0700 · Steve
- readLastLive(mfr): newest saved portal result by mtime, matched on the file's
mfr field (unifies the 3 AM overnight job + app-triggered pulls).
- Endpoint attaches out.lastLive always; a successful fresh live pull is written
back via saveLive() so the next locked/cooldown request shows it instantly.
- Use process.execPath (not bare 'node') to spawn the scraper — PATH-safe.
- pills.html renders a 'Last portal pull · Nm ago' section (hidden when a fresh
live pull succeeded).
Files touched
M public/pills.htmlM server.js
Diff
commit 803ecd65766895f0bfb353be3afbaa4fb80068ca
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jul 10 07:51:03 2026 -0700
pills app: add last-portal-pull cache (reads yw-results, write-through on live)
- readLastLive(mfr): newest saved portal result by mtime, matched on the file's
mfr field (unifies the 3 AM overnight job + app-triggered pulls).
- Endpoint attaches out.lastLive always; a successful fresh live pull is written
back via saveLive() so the next locked/cooldown request shows it instantly.
- Use process.execPath (not bare 'node') to spawn the scraper — PATH-safe.
- pills.html renders a 'Last portal pull · Nm ago' section (hidden when a fresh
live pull succeeded).
---
public/pills.html | 17 +++++++++++++++++
server.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/public/pills.html b/public/pills.html
index 44843e2..f933bcd 100644
--- a/public/pills.html
+++ b/public/pills.html
@@ -160,6 +160,23 @@
h += '</div>';
}
+ // last portal pull (from overnight job / prior pull) — skip if a fresh live succeeded
+ const freshLiveOk = d.live && d.live.loggedIn;
+ if (d.lastLive && !freshLiveOk) {
+ const L = d.lastLive;
+ const age = L.ageMin < 60 ? L.ageMin+'m ago' : Math.round(L.ageMin/60)+'h ago';
+ h += '<div class="sect"><h3>Last portal pull · '+age+'</h3>';
+ if (L.loggedIn && L.priceStock && L.priceStock.length) {
+ h += '<div class="banner ok">Saved from the trade portal at '+esc(new Date(L.at).toLocaleString())+'</div>';
+ h += '<ul class="live-list">'+ L.priceStock.map(x=>'<li>'+esc(x)+'</li>').join('') +'</ul>';
+ } else if (L.locked) {
+ h += '<div class="banner bad">Last attempt hit a portal lockout ('+esc(new Date(L.at).toLocaleString())+').</div>';
+ } else {
+ h += '<div class="banner warn">Last saved pull had no price/stock text.</div>';
+ }
+ h += '</div>';
+ }
+
// live
if (d.live) {
h += '<div class="sect"><h3>Live (trade portal)</h3>';
diff --git a/server.js b/server.js
index 03ef20f..d36b870 100644
--- a/server.js
+++ b/server.js
@@ -1,4 +1,5 @@
try { require('dotenv').config({ path: '/root/.env' }); } catch (_) {}
+const fs = require('fs');
const os = require('os');
const path = require('path');
// Canonical secrets store — holds YORKWALL_* (parsed safely, never bash-sourced).
@@ -1190,10 +1191,51 @@ let lockoutUntilMs = 0;
const LIVE_MIN_GAP_MS = 20 * 1000; // ignore rapid double-clicks
const LIVE_LOCKOUT_MS = 30 * 60 * 1000; // honor the portal's 30-min lockout
+// Cache dir where the overnight job (and app-triggered pulls) drop portal results.
+const YW_RESULTS_DIR = path.join(path.dirname(YW_SCRIPT), 'yw-results');
+
+// Read the most-recent saved portal result for this MFR (from the 3 AM overnight
+// job or a prior app pull). Matches by the file's `mfr` field, newest by mtime —
+// so the pills app can show the last known live number without hitting the portal.
+function readLastLive(mfrSku) {
+ try {
+ if (!fs.existsSync(YW_RESULTS_DIR)) return null;
+ let best = null;
+ for (const f of fs.readdirSync(YW_RESULTS_DIR)) {
+ if (!f.endsWith('.json')) continue;
+ const fp = path.join(YW_RESULTS_DIR, f);
+ let st, data;
+ try { st = fs.statSync(fp); data = JSON.parse(fs.readFileSync(fp, 'utf8')); } catch (_) { continue; }
+ if (!data || data.mfr !== mfrSku) continue;
+ if (!best || st.mtimeMs > best.mtimeMs) best = { mtimeMs: st.mtimeMs, data };
+ }
+ if (!best) return null;
+ const d = best.data;
+ return {
+ at: new Date(best.mtimeMs).toISOString(),
+ ageMin: Math.round((Date.now() - best.mtimeMs) / 60000),
+ loggedIn: !!d.loggedIn,
+ locked: !!d.locked,
+ priceStock: Array.isArray(d.priceStock) ? d.priceStock : [],
+ abort: d.abort || null,
+ };
+ } catch (_) { return null; }
+}
+
+// Persist a successful app-triggered pull so it becomes the next "last live".
+function saveLive(mfrSku, data) {
+ try {
+ fs.mkdirSync(YW_RESULTS_DIR, { recursive: true });
+ fs.writeFileSync(path.join(YW_RESULTS_DIR, `${mfrSku}-app-${Date.now()}.json`), JSON.stringify(data));
+ } catch (_) { /* best-effort cache */ }
+}
+
// Run the yorkwall scraper for one MFR sku (Brewster/York family only).
function runYorkwallLive(mfrSku) {
return new Promise((resolve) => {
- execFile('node', [YW_SCRIPT, mfrSku], {
+ // Use the exact node binary running this server (process.execPath) rather than
+ // relying on 'node' being on PATH — launchd/pm2 run with a stripped PATH.
+ execFile(process.execPath, [YW_SCRIPT, mfrSku], {
cwd: path.dirname(YW_SCRIPT),
timeout: 90000,
maxBuffer: 4 * 1024 * 1024,
@@ -1245,6 +1287,11 @@ app.post('/api/check/live', async (req, res) => {
message: specsResult.message,
};
+ // Last-live tier — most recent saved portal pull (overnight job or prior app
+ // pull), shown even when the portal is locked / not queried this call.
+ const last = readLastLive(match.mfr_sku);
+ if (last) out.lastLive = last;
+
// Live tier — only if the user asked for stock/price.
if (wantStock || wantPrice) {
const hasPortal = !!(vc && vc.tradePortal); // Brewster/York family only
@@ -1267,6 +1314,8 @@ app.post('/api/check/live', async (req, res) => {
const d = r.data;
// If the scraper reports a lockout, arm the 30-min backoff.
if (d.locked || d.abort === 'STILL_LOCKED') lockoutUntilMs = Date.now() + LIVE_LOCKOUT_MS;
+ // Write-through: cache a successful pull as the next "last live".
+ if (d.loggedIn) { saveLive(match.mfr_sku, d); out.lastLive = readLastLive(match.mfr_sku); }
out.live = {
attempted: true,
loggedIn: !!d.loggedIn,
← 59bd09a Add pills live-check app: enter MFR or DW number, tick stock
·
back to Sku Check Skill
·
pills app: parse yorkwall trade price + stock, show Retail/N a55b434 →