← back to Tools Dw Hub
initial scaffold: tools.designerwallcoverings launcher + live-status hub
cf9e9dc8130aae76021386907e71c9040ee90bc8 · 2026-07-28 10:48:00 -0700 · Steve
Files touched
A .deploy.confA .gitignoreA README.mdA ecosystem.config.jsA package.jsonA public/index.htmlA server.jsA tools.json
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,'"')}">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 →