← back to Flock Fix Viewer

server.js

111 lines

#!/usr/bin/env node
// Flock Fix Viewer — live board showing each flock SKU's fields going broken -> fixed
// Reads LIVE Shopify (not the unreliable mirror). Basic auth admin/DW2024!.
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');

const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = fs.readFileSync(path.join(__dirname, '.token'), 'utf8').trim();
const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
// The flock set we are fixing (Phillipe Romano flocked velvet line)
const QUERY = "vendor:'Phillipe Romano' status:active (tag:'Flock Velvet' OR handle:flock OR title:flock)";

function gql(query, variables) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({ query, variables: variables || {} });
    const req = https.request({
      host: SHOP, path: '/admin/api/2024-10/graphql.json', method: 'POST',
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
    }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); });
    req.on('error', reject); req.write(body); req.end();
  });
}

const PRODUCT_Q = `query($c:String){
  products(first:100, query:${JSON.stringify(QUERY)}, after:$c){
    pageInfo{hasNextPage endCursor}
    edges{node{
      id handle title status createdAt
      featuredImage{url}
      tags
      variants(first:8){edges{node{sku title price}}}
      mMin: metafield(namespace:"global", key:"v_prods_quantity_order_min"){value}
      mWidth: metafield(namespace:"global", key:"width"){value}
      mUnit: metafield(namespace:"global", key:"unit_of_measure"){value}
      mSqSR: metafield(namespace:"global", key:"sqft_single_roll"){value}
      mSqDR: metafield(namespace:"global", key:"sqft_double_roll"){value}
      mRepeat: metafield(namespace:"custom", key:"pattern_repeat"){value}
      mPrint: metafield(namespace:"custom", key:"print_type"){value}
    }}
  }
}`;

let cache = { at: 0, rows: [] };
async function fetchStatus() {
  if (Date.now() - cache.at < 4000) return cache.rows; // light throttle
  let cur = null, all = [];
  do {
    const r = await gql(PRODUCT_Q, { c: cur });
    const p = r && r.data && r.data.products;
    if (!p) break;
    all = all.concat(p.edges);
    cur = p.pageInfo.hasNextPage ? p.pageInfo.endCursor : null;
  } while (cur);
  const rows = all.map(e => {
    const n = e.node;
    const tags = n.tags || [];
    const hasQuotes = tags.some(t => String(t).replace(/["{}]/g, '').trim().toLowerCase() === 'quotes');
    const vs = (n.variants.edges || []).map(v => v.node);
    const roll = vs.filter(v => !/sample/i.test(v.title || ''));
    const rollPrice = roll.reduce((m, v) => Math.max(m, parseFloat(v.price) || 0), 0);
    const sku = (roll[0] && roll[0].sku) || (vs[0] && vs[0].sku) || '?';
    const handle = n.handle;
    const isDup = handle.startsWith('copy-of-') || /-\d+$/.test(handle);
    const hasPrice = rollPrice > 10;
    const buyable = !hasQuotes && hasPrice && n.status === 'ACTIVE';
    const numericId = (n.id || '').split('/').pop();
    const adminUrl = 'https://admin.shopify.com/store/designer-laboratory-sandbox/products/' + numericId;
    return {
      sku, handle, title: n.title, status: n.status, price: rollPrice,
      img: (n.featuredImage && n.featuredImage.url) || null,
      created: n.createdAt,
      adminUrl,
      f_quotes: !hasQuotes,          // fixed when quotes gone
      f_price: hasPrice,             // has a real roll price
      f_badge: !hasQuotes,           // wrong 54"/yard badge gone when quotes gone
      f_buyable: buyable,
      f_min: !!(n.mMin && n.mMin.value),
      f_width: !!(n.mWidth && n.mWidth.value),
      f_sqft: !!(n.mSqSR && n.mSqSR.value && n.mSqDR && n.mSqDR.value),
      f_specs: !!(n.mRepeat && n.mRepeat.value && n.mPrint && n.mPrint.value),
      sqftSR: (n.mSqSR && n.mSqSR.value) || null,
      sqftDR: (n.mSqDR && n.mSqDR.value) || null,
      colorwayDup: isDup,
      fixed: !hasQuotes && hasPrice, // primary fix complete
    };
  });
  cache = { at: Date.now(), rows };
  return rows;
}

const PAGE = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');

const server = http.createServer(async (req, res) => {
  if ((req.headers.authorization || '') !== AUTH) {
    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="flock"' }); return res.end('auth required');
  }
  if (req.url.startsWith('/api/status')) {
    try { const rows = await fetchStatus(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(rows)); }
    catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: String(e) })); }
    return;
  }
  res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(PAGE);
});
server.listen(process.env.PORT || 61438, '127.0.0.1', () => {
  const port = server.address().port;
  fs.writeFileSync(path.join(__dirname, '.port'), String(port));
  console.log('FLOCK-FIX-VIEWER listening on http://127.0.0.1:' + port + '  (admin / DW2024!)');
});