[object Object]

← back to Tools Dw Hub

initial scaffold: tools.designerwallcoverings launcher + live-status hub

cf9e9dc8130aae76021386907e71c9040ee90bc8 · 2026-07-28 10:48:00 -0700 · Steve

Files touched

Diff

commit cf9e9dc8130aae76021386907e71c9040ee90bc8
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 28 10:48:00 2026 -0700

    initial scaffold: tools.designerwallcoverings launcher + live-status hub
---
 .deploy.conf        |   3 +
 .gitignore          |   8 ++
 README.md           |  32 ++++++++
 ecosystem.config.js |  14 ++++
 package.json        |  10 +++
 public/index.html   | 148 ++++++++++++++++++++++++++++++++++++
 server.js           |  81 ++++++++++++++++++++
 tools.json          | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 506 insertions(+)

diff --git a/.deploy.conf b/.deploy.conf
new file mode 100644
index 0000000..c7fee65
--- /dev/null
+++ b/.deploy.conf
@@ -0,0 +1,3 @@
+PROJECT_NAME=tools-dw-hub
+DEPLOY_PATH=/root/Projects/tools-dw-hub
+HEALTH_URL=http://127.0.0.1:9815/healthz
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..25aad9f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# tools.designerwallcoverings.com — DW Tools Hub
+
+One launcher page for every Designer Wallcoverings build tool, with a live up/down
+status dot per tool. Zero dependencies (Node built-ins only).
+
+## Run locally
+```bash
+node server.js            # OS-assigned free port, basic-auth admin/DW2024!
+PORT=9815 node server.js  # fixed port
+```
+- `/`         launcher UI (auth)
+- `/api/tools`  the manifest (auth)
+- `/api/status` live TCP probe of every tool's port (auth)
+- `/healthz`  open, unauthenticated 200 (fleet keepalive)
+
+## The tools
+All entries live in `tools.json` — add/edit a tool = a data edit, no code change.
+Each tool is a **separate app** under `~/Projects/Designer-Wallcoverings/DW-Programming/`
+on its own port; this hub just links to them and probes whether they're running.
+
+Named ones: **150 DPI + Spoonflower flow** (150dpi), **Duplicate & Recolor / color
+replacement / art coordinates** (v1.4/v1.3/v1), **Search by Color Wheel**, room-setting
+tools, SKU importer, the vendor taggers, and the vendor updater dashboards.
+
+## Status probe host
+`PROBE_HOST` (default `127.0.0.1`) is where the tool apps listen. On Mac2 that's
+localhost. If the hub goes public on Kamatera, set `PROBE_HOST` to the tailnet host
+(Mac2) so the dots reflect the real app state, or deploy the tools alongside.
+
+## Go-live (GATED)
+Public `tools.designerwallcoverings.com` needs DNS (Cloudflare) + Kamatera deploy —
+both Steve-gated. See `pending-approval/` memo. Note the DW CF zone is at its 200-record cap.
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..8e3fac9
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,14 @@
+module.exports = {
+  apps: [{
+    name: 'tools-dw-hub',
+    script: 'server.js',
+    cwd: __dirname,
+    env: {
+      PORT: 9815,               // fixed port for pm2/prod; local dev uses 0 (OS-assigned) unless PORT set
+      BASIC_AUTH: 'admin:DW2024!',
+      PROBE_HOST: '127.0.0.1'   // set to the tailnet/Kamatera-local host where the tool apps listen when public
+    },
+    autorestart: true,
+    max_restarts: 10
+  }]
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..79d7af1
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "tools-dw-hub",
+  "version": "1.0.0",
+  "private": true,
+  "description": "tools.designerwallcoverings.com — launcher + live-status hub for all DW build tools",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js"
+  }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..183f24f
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>Designer Wallcoverings · Tools</title>
+<style>
+  :root { --cols: 3; --ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e2da; --bg:#faf8f4; --accent:#8a7a5c; }
+  * { margin:0; padding:0; box-sizing:border-box; }
+  body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:var(--bg); color:var(--ink); }
+  header { padding:34px 28px 14px; }
+  h1 { font-weight:300; letter-spacing:.14em; text-transform:uppercase; font-size:1.5rem; }
+  h1 b { font-weight:600; }
+  .sub { color:var(--muted); font-size:.85rem; margin-top:6px; letter-spacing:.02em; }
+  .bar { position:sticky; top:0; z-index:5; background:rgba(250,248,244,.94); backdrop-filter:blur(6px);
+         border-bottom:1px solid var(--line); padding:12px 28px; display:flex; gap:16px; align-items:center; flex-wrap:wrap; }
+  .bar input[type=search], .bar select { padding:8px 12px; border:1px solid var(--line); border-radius:8px; background:#fff; font-size:.85rem; color:var(--ink); }
+  .bar input[type=search] { min-width:220px; }
+  .bar label { font-size:.72rem; color:var(--muted); text-transform:uppercase; letter-spacing:.08em; display:flex; align-items:center; gap:8px; }
+  .pills { display:flex; gap:6px; flex-wrap:wrap; padding:12px 28px 0; }
+  .pill { font-size:.72rem; padding:5px 12px; border:1px solid var(--line); border-radius:999px; background:#fff; cursor:pointer; color:var(--muted); }
+  .pill.on { background:var(--ink); color:#fff; border-color:var(--ink); }
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:16px; padding:18px 28px 60px; }
+  .card { background:#fff; border:1px solid var(--line); border-radius:12px; padding:16px 16px 14px; display:flex; flex-direction:column; gap:9px; transition:box-shadow .15s, transform .15s; }
+  .card:hover { box-shadow:0 8px 24px rgba(0,0,0,.07); transform:translateY(-2px); }
+  .ct { display:flex; align-items:center; justify-content:space-between; gap:8px; }
+  .name { font-weight:600; font-size:.98rem; line-height:1.25; }
+  .dot { width:9px; height:9px; border-radius:50%; flex:0 0 auto; background:#c9c4ba; box-shadow:0 0 0 3px rgba(0,0,0,.03); }
+  .dot.up { background:#22c55e; box-shadow:0 0 0 3px rgba(34,197,94,.15); }
+  .dot.down { background:#ef4444; box-shadow:0 0 0 3px rgba(239,68,68,.12); }
+  .dot.na { background:#c9c4ba; }
+  .desc { font-size:.82rem; color:var(--muted); line-height:1.45; flex:1; }
+  .meta { display:flex; align-items:center; gap:10px; flex-wrap:wrap; font-size:.7rem; color:var(--muted); }
+  .cat { text-transform:uppercase; letter-spacing:.08em; color:var(--accent); font-weight:600; }
+  .when { display:inline-flex; align-items:center; gap:4px; }
+  .actions { display:flex; gap:8px; margin-top:4px; }
+  .btn { font-size:.78rem; padding:7px 12px; border-radius:8px; text-decoration:none; border:1px solid var(--line); background:#fff; color:var(--ink); cursor:pointer; }
+  .btn.go { background:var(--ink); color:#fff; border-color:var(--ink); }
+  .btn.go.off { opacity:.45; pointer-events:none; }
+  .btn.copy:active { background:#f0ece3; }
+  .foot { padding:0 28px 40px; color:var(--muted); font-size:.72rem; }
+  .count { color:var(--muted); font-size:.75rem; margin-left:auto; }
+</style>
+</head>
+<body>
+<header>
+  <h1>Designer <b>Wallcoverings</b> · Tools</h1>
+  <div class="sub" id="sub">Every DW build tool, one launcher</div>
+</header>
+
+<div class="bar">
+  <input type="search" id="q" placeholder="Search tools…" />
+  <label>Sort
+    <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="oldest">Oldest</option>
+      <option value="name">Name A→Z</option>
+      <option value="category">Category</option>
+      <option value="status">Status (up first)</option>
+    </select>
+  </label>
+  <label>Density
+    <input type="range" id="density" min="2" max="5" step="1" value="3" />
+  </label>
+  <span class="count" id="count"></span>
+</div>
+<div class="pills" id="pills"></div>
+
+<div class="grid" id="grid"></div>
+<div class="foot" id="foot"></div>
+
+<script>
+let TOOLS = [], STATUS = {}, CAT = 'All';
+const $ = (s) => document.querySelector(s);
+const LS = { get:(k,d)=>{try{return JSON.parse(localStorage.getItem('dwtools_'+k))??d}catch{return d}}, set:(k,v)=>localStorage.setItem('dwtools_'+k,JSON.stringify(v)) };
+
+function fmtDate(iso){
+  try { const d = new Date(iso+'T12:00:00'); return d.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }
+  catch { return iso; }
+}
+const rank = (s)=> s==='up'?0 : s==='down'?1 : 2;
+
+function render(){
+  const q = $('#q').value.trim().toLowerCase();
+  const sort = $('#sort').value;
+  let list = TOOLS.filter(t => (CAT==='All'||t.category===CAT) &&
+    (!q || (t.name+' '+t.desc+' '+t.category).toLowerCase().includes(q)));
+  list.sort((a,b)=>{
+    if(sort==='newest') return (b.created||'').localeCompare(a.created||'');
+    if(sort==='oldest') return (a.created||'').localeCompare(b.created||'');
+    if(sort==='name') return a.name.localeCompare(b.name);
+    if(sort==='category') return a.category.localeCompare(b.category)||a.name.localeCompare(b.name);
+    if(sort==='status') return rank(STATUS[a.slug])-rank(STATUS[b.slug])||a.name.localeCompare(b.name);
+    return 0;
+  });
+  $('#count').textContent = list.length+' of '+TOOLS.length+' tools';
+  $('#grid').innerHTML = list.map(t=>{
+    const st = STATUS[t.slug]||'na';
+    const cls = st==='up'?'up':st==='down'?'down':'na';
+    const url = t.port ? ('http://'+location.hostname+':'+t.port+'/') : null;
+    const go = url ? `<a class="btn go ${st==='up'?'':'off'}" href="${url}" target="_blank" rel="noopener noreferrer">Launch ↗</a>`
+                   : `<span class="btn go off">Local app</span>`;
+    return `<div class="card">
+      <div class="ct"><span class="name">${t.name}</span><span class="dot ${cls}" title="${st}"></span></div>
+      <div class="desc">${t.desc}</div>
+      <div class="meta">
+        <span class="cat">${t.category}</span>
+        <span class="when" title="${t.created}">🕓 ${fmtDate(t.created)}</span>
+        ${t.port?`<span>:${t.port}</span>`:''}
+      </div>
+      <div class="actions">
+        ${go}
+        <button class="btn copy" data-cmd="${(t.start||'').replace(/"/g,'&quot;')}">Copy start ⌘</button>
+      </div>
+    </div>`;
+  }).join('');
+  document.querySelectorAll('.btn.copy').forEach(b=>b.onclick=()=>{
+    navigator.clipboard.writeText(b.dataset.cmd); const o=b.textContent; b.textContent='Copied ✓'; setTimeout(()=>b.textContent=o,1200);
+  });
+}
+
+function renderPills(cats){
+  $('#pills').innerHTML = ['All',...cats].map(c=>`<span class="pill ${c===CAT?'on':''}" data-c="${c}">${c}</span>`).join('');
+  document.querySelectorAll('.pill').forEach(p=>p.onclick=()=>{CAT=p.dataset.c; renderPills(cats); render();});
+}
+
+async function loadStatus(){
+  try{ STATUS = await (await fetch('/api/status')).json(); render(); }catch{}
+}
+
+async function init(){
+  const m = await (await fetch('/api/tools')).json();
+  TOOLS = m.tools; $('#sub').textContent = m.hub.subtitle;
+  renderPills(m.categories);
+  // restore prefs
+  $('#sort').value = LS.get('sort','newest');
+  const dens = LS.get('density',3); $('#density').value = dens; document.documentElement.style.setProperty('--cols',dens);
+  $('#q').oninput = render;
+  $('#sort').onchange = ()=>{LS.set('sort',$('#sort').value); render();};
+  $('#density').oninput = ()=>{const v=$('#density').value; document.documentElement.style.setProperty('--cols',v); LS.set('density',v);};
+  render(); loadStatus(); setInterval(loadStatus, 20000);
+  const up = Object.values(STATUS).filter(s=>s==='up').length;
+  $('#foot').textContent = 'Live status refreshes every 20s. Tools run as separate apps — a red dot means that app isn’t running; use “Copy start” to launch it.';
+}
+init();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..5dd5d4f
--- /dev/null
+++ b/server.js
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+// tools.designerwallcoverings.com — zero-dependency launcher hub for all DW build tools.
+// Node built-ins only (http/fs/path/net) — no npm install required.
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const net = require('net');
+
+const PORT = process.env.PORT || 0;                       // 0 = OS-assigned free port (local dev)
+const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';
+const PROBE_HOST = process.env.PROBE_HOST || '127.0.0.1'; // where the tool apps listen (localhost on Mac2; tailnet/Kamatera-local when public)
+const [AUTH_USER, AUTH_PASS] = BASIC_AUTH.split(':');
+const ROOT = __dirname;
+
+const manifest = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'tools.json'), 'utf8'));
+
+// --- TCP port probe (up/down) with a short timeout ---
+function probe(port, timeout = 900) {
+  return new Promise((resolve) => {
+    if (!port) return resolve('n/a');
+    const sock = new net.Socket();
+    let done = false;
+    const finish = (v) => { if (!done) { done = true; sock.destroy(); resolve(v); } };
+    sock.setTimeout(timeout);
+    sock.once('connect', () => finish('up'));
+    sock.once('timeout', () => finish('down'));
+    sock.once('error', () => finish('down'));
+    sock.connect(port, PROBE_HOST);
+  });
+}
+
+function unauthorized(res) {
+  res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Tools"', 'Content-Type': 'text/plain' });
+  res.end('Authentication required');
+}
+function checkAuth(req) {
+  const h = req.headers.authorization || '';
+  if (!h.startsWith('Basic ')) return false;
+  const [u, p] = Buffer.from(h.slice(6), 'base64').toString().split(':');
+  return u === AUTH_USER && p === AUTH_PASS;
+}
+
+const server = http.createServer(async (req, res) => {
+  const url = req.url.split('?')[0];
+
+  // /healthz is OPEN (before auth) so the fleet keepalive never misreads a healthy 401 as dead.
+  if (url === '/healthz') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ ok: true, service: 'tools-dw-hub' }));
+  }
+
+  if (!checkAuth(req)) return unauthorized(res);
+
+  if (url === '/api/tools') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify(manifest()));
+  }
+
+  if (url === '/api/status') {
+    const { tools } = manifest();
+    const results = await Promise.all(tools.map(async (t) => ({ slug: t.slug, status: await probe(t.port) })));
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify(Object.fromEntries(results.map((r) => [r.slug, r.status]))));
+  }
+
+  // static
+  const file = url === '/' ? '/index.html' : url;
+  const fp = path.join(ROOT, 'public', path.normalize(file).replace(/^(\.\.[/\\])+/, ''));
+  fs.readFile(fp, (err, data) => {
+    if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); return res.end('Not found'); }
+    const ext = path.extname(fp);
+    const type = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml' }[ext] || 'application/octet-stream';
+    res.writeHead(200, { 'Content-Type': type });
+    res.end(data);
+  });
+});
+
+server.listen(PORT, () => {
+  const addr = server.address();
+  console.log(`[tools-dw-hub] listening on http://127.0.0.1:${addr.port}  (auth ${AUTH_USER}/****, probing ${PROBE_HOST})`);
+});
diff --git a/tools.json b/tools.json
new file mode 100644
index 0000000..78a340c
--- /dev/null
+++ b/tools.json
@@ -0,0 +1,210 @@
+{
+  "hub": {
+    "title": "Designer Wallcoverings · Tools",
+    "subtitle": "Every DW build tool, one launcher",
+    "domain": "tools.designerwallcoverings.com"
+  },
+  "categories": ["Print & Image", "Recolor", "Room Settings", "Search & Color", "Import", "Tagging", "Vendor Updaters", "Ops"],
+  "tools": [
+    {
+      "slug": "150dpi",
+      "name": "150 DPI Converter + Spoonflower Flow",
+      "desc": "Resize artwork to 24\" wide @ 150 DPI and run the Spoonflower upload automation (the full print-prep + upload flow, incl. art coordinates).",
+      "category": "Print & Image",
+      "folder": "Designer-Wallcoverings/DW-Programming/150dpi",
+      "port": 3150,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/150dpi && npm start",
+      "created": "2026-05-26"
+    },
+    {
+      "slug": "recolor-v14",
+      "name": "Duplicate & Recolor (v1.4 · latest)",
+      "desc": "DesignerWallcoverings.AI recolor tool — color replacement + art-coordinate placement on artwork. Newest build.",
+      "category": "Recolor",
+      "folder": "Designer-Wallcoverings/DW-Programming/DW-Duplicate-Recolor-v1.4",
+      "port": 3000,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/DW-Duplicate-Recolor-v1.4 && npm start",
+      "created": "2026-05-18"
+    },
+    {
+      "slug": "recolor-v13",
+      "name": "Duplicate & Recolor (v1.3)",
+      "desc": "Prior stable recolor build — color replacement on duplicated designs.",
+      "category": "Recolor",
+      "folder": "Designer-Wallcoverings/DW-Programming/DW-Duplicate-Recolor-v1.3",
+      "port": 5000,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/DW-Duplicate-Recolor-v1.3 && npm start",
+      "created": "2026-03-22"
+    },
+    {
+      "slug": "recolor-v1",
+      "name": "Duplicate & Recolor (v1 · original)",
+      "desc": "The original Duplicate & Recolor tool.",
+      "category": "Recolor",
+      "folder": "Designer-Wallcoverings/DW-Programming/DW-Duplicate-Recolor",
+      "port": 3001,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/DW-Duplicate-Recolor && npm start",
+      "created": "2026-03-22"
+    },
+    {
+      "slug": "image-to-room",
+      "name": "Image → Room Setting",
+      "desc": "Drop a pattern image, generate photorealistic room-setting mockups.",
+      "category": "Room Settings",
+      "folder": "Designer-Wallcoverings/DW-Programming/ImagetoRoomSetting",
+      "port": 3004,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/ImagetoRoomSetting && npm run dev",
+      "created": "2025-12-08"
+    },
+    {
+      "slug": "room-setting-app",
+      "name": "Room Setting App",
+      "desc": "Standalone room-setting generator app.",
+      "category": "Room Settings",
+      "folder": "Designer-Wallcoverings/DW-Programming/room-setting-app",
+      "port": 3075,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/room-setting-app && npm run dev",
+      "created": "2026-06-26"
+    },
+    {
+      "slug": "roomsetting-creator",
+      "name": "RoomSetting Creator",
+      "desc": "Room mockup composer (static build).",
+      "category": "Room Settings",
+      "folder": "Designer-Wallcoverings/DW-Programming/RoomSetting Creator",
+      "port": null,
+      "start": "open '~/Projects/Designer-Wallcoverings/DW-Programming/RoomSetting Creator/index.html'",
+      "created": "2026-01-07"
+    },
+    {
+      "slug": "color-wheel",
+      "name": "Search by Color Wheel",
+      "desc": "Browse the catalog by picking a hue off an interactive color wheel.",
+      "category": "Search & Color",
+      "folder": "Designer-Wallcoverings/DW-Programming/SearchByColorWheel",
+      "port": 9840,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/SearchByColorWheel && npm run dev",
+      "created": "2025-12-08"
+    },
+    {
+      "slug": "import-sku",
+      "name": "Import New SKU from URL",
+      "desc": "Paste a vendor product URL → scrape → import into Shopify.",
+      "category": "Import",
+      "folder": "Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL",
+      "port": 3002,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL && npm run dev",
+      "created": "2026-02-15"
+    },
+    {
+      "slug": "tagger-kravet",
+      "name": "Kravet Design Tagger",
+      "desc": "Gemini-AI design/style tagging for the Kravet line.",
+      "category": "Tagging",
+      "folder": "Designer-Wallcoverings/DW-Programming/KravetDesignTagger",
+      "port": 3851,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/KravetDesignTagger && npm start",
+      "created": "2026-05-29"
+    },
+    {
+      "slug": "tagger-thibaut",
+      "name": "Thibaut Tagger",
+      "desc": "Gemini-AI design/style tagging for the Thibaut line.",
+      "category": "Tagging",
+      "folder": "Designer-Wallcoverings/DW-Programming/ThibautTagger",
+      "port": 3850,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/ThibautTagger && npm start",
+      "created": "2026-05-29"
+    },
+    {
+      "slug": "tagger-pr",
+      "name": "Philippe Romano Tagger",
+      "desc": "Gemini-AI design/style tagging for the Philippe Romano line.",
+      "category": "Tagging",
+      "folder": "Designer-Wallcoverings/DW-Programming/PhillipeRomanoTagger",
+      "port": 3160,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/PhillipeRomanoTagger && npm start",
+      "created": "2026-06-23"
+    },
+    {
+      "slug": "style-tag-updater",
+      "name": "DW Style Tag Updater",
+      "desc": "Bulk style-tag updater across the catalog (static tool).",
+      "category": "Tagging",
+      "folder": "Designer-Wallcoverings/DW-Programming/DW Style Tag Updater",
+      "port": null,
+      "start": "cd '~/Projects/Designer-Wallcoverings/DW-Programming/DW Style Tag Updater' && npm start",
+      "created": "2025-11-13"
+    },
+    {
+      "slug": "updater-schumacher",
+      "name": "Schumacher DWSW Updater",
+      "desc": "Architectural Wallcoverings updater dashboard for the Schumacher DWSW line.",
+      "category": "Vendor Updaters",
+      "folder": "Designer-Wallcoverings/DW-Programming/schumacher-updater-dashboard",
+      "port": 9898,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/schumacher-updater-dashboard && npm start",
+      "created": "2026-01-22"
+    },
+    {
+      "slug": "updater-architectural",
+      "name": "Architectural Updater",
+      "desc": "Transform vendor products to Architectural Wallcoverings/Fabrics branding.",
+      "category": "Vendor Updaters",
+      "folder": "Designer-Wallcoverings/DW-Programming/architectural-updater",
+      "port": 9897,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/architectural-updater && npm start",
+      "created": "2026-01-26"
+    },
+    {
+      "slug": "updater-maya",
+      "name": "Maya Romanoff Updater",
+      "desc": "Variant/spec/metafield updater for the Maya Romanoff line (static tool).",
+      "category": "Vendor Updaters",
+      "folder": "Designer-Wallcoverings/DW-Programming/maya-romanoff-updater",
+      "port": null,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/maya-romanoff-updater && npm start",
+      "created": "2026-07-09"
+    },
+    {
+      "slug": "vendor-dashboard-v2",
+      "name": "Vendor Dashboard V2",
+      "desc": "Vendor-catalog management dashboard, v2.",
+      "category": "Vendor Updaters",
+      "folder": "Designer-Wallcoverings/DW-Programming/VendorDashboard-V2",
+      "port": 9832,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/VendorDashboard-V2 && npm start",
+      "created": "2026-06-09"
+    },
+    {
+      "slug": "dw-dashboard",
+      "name": "DW Dashboard",
+      "desc": "Catalog/ops overview dashboard (static).",
+      "category": "Ops",
+      "folder": "Designer-Wallcoverings/DW-Programming/DW-Dashboard",
+      "port": null,
+      "start": "open ~/Projects/Designer-Wallcoverings/DW-Programming/DW-Dashboard/index.html",
+      "created": "2025-11-13"
+    },
+    {
+      "slug": "system-dashboard",
+      "name": "System Dashboard",
+      "desc": "System/process status dashboard (static).",
+      "category": "Ops",
+      "folder": "Designer-Wallcoverings/DW-Programming/system-dashboard",
+      "port": null,
+      "start": "open ~/Projects/Designer-Wallcoverings/DW-Programming/system-dashboard/index.html",
+      "created": "2026-01-06"
+    },
+    {
+      "slug": "pm2-dashboard",
+      "name": "PM2 Dashboard",
+      "desc": "Live view of PM2-managed processes.",
+      "category": "Ops",
+      "folder": "Designer-Wallcoverings/DW-Programming/pm2-dashboard",
+      "port": 7401,
+      "start": "cd ~/Projects/Designer-Wallcoverings/DW-Programming/pm2-dashboard && npm start",
+      "created": "2026-01-30"
+    }
+  ]
+}

(oldest)  ·  back to Tools Dw Hub  ·  wire TOOL_HOST/PROBE_HOST for public deploy (tailnet-reachab d1d5a15 →