[object Object]

← back to Cncp Failures Mockups

scaffold: 6 visually-distinct mockups of CNCP Failures+Hermes panel, web viewer on :9771

885ca324331fc193aee82aa1d94ff47781211636 · 2026-05-11 13:37:12 -0700 · SteveStudio2

Files touched

Diff

commit 885ca324331fc193aee82aa1d94ff47781211636
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 11 13:37:12 2026 -0700

    scaffold: 6 visually-distinct mockups of CNCP Failures+Hermes panel, web viewer on :9771
---
 .gitignore               |   3 +
 ecosystem.config.js      |  15 +
 package-lock.json        | 826 +++++++++++++++++++++++++++++++++++++++++++++++
 package.json             |  13 +
 public/index.html        |  95 ++++++
 public/v1-terminal.html  |  79 +++++
 public/v2-newspaper.html |  82 +++++
 public/v3-heatmap.html   | 101 ++++++
 public/v4-kanban.html    |  90 ++++++
 public/v5-timeline.html  | 120 +++++++
 public/v6-brutalist.html | 120 +++++++
 sample-data.json         | 116 +++++++
 server.js                |  29 ++
 13 files changed, 1689 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c45938
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+*.log
+.DS_Store
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..bee951c
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+  apps: [{
+    name: 'cncp-failures-mockups',
+    script: 'server.js',
+    cwd: __dirname,
+    instances: 1,
+    exec_mode: 'fork',
+    autorestart: true,
+    watch: false,
+    max_restarts: 10,
+    min_uptime: '30s',
+    max_memory_restart: '150M',
+    env: { NODE_ENV: 'production', PORT: '9771', BIND: '127.0.0.1' }
+  }]
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..041ac68
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,826 @@
+{
+  "name": "cncp-failures-mockups",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "cncp-failures-mockups",
+      "version": "0.1.0",
+      "dependencies": {
+        "express": "^5.2.1"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+      "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.3",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.7.0",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.1",
+        "raw-body": "^3.0.1",
+        "type-is": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+      "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+      "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.1",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+      "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+      "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.1",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+      "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+      "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.7.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.1",
+        "mime-types": "^3.0.2",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+      "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0fbdf20
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "cncp-failures-mockups",
+  "version": "0.1.0",
+  "description": "6 visually-distinct mockups of the CNCP Failures+Hermes panel, each a different display direction.",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js"
+  },
+  "private": true,
+  "dependencies": {
+    "express": "^5.2.1"
+  }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..1b72b2e
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,95 @@
+<!doctype html><html><head>
+<meta charset="utf-8"><title>CNCP Failures — 6 mockup variants</title>
+<style>
+  :root { --bg:#0a0c12; --fg:#e6eef8; --muted:#7c8aa5; --accent:#7c3aed; --panel:#161a26; --border:#252b3d; }
+  body { margin:0; background:var(--bg); color:var(--fg); font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif; min-height:100vh; }
+  .hero { padding:36px 32px 18px; max-width:1280px; margin:0 auto; }
+  .hero h1 { font-size:24px; font-weight:700; margin:0 0 4px; letter-spacing:-0.01em; }
+  .hero .sub { color:var(--muted); font-size:14px; max-width:720px; }
+  .controls { padding:6px 32px 12px; max-width:1280px; margin:0 auto; display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
+  .ctrl-btn { background:var(--panel); border:1px solid var(--border); color:var(--fg); padding:6px 12px; border-radius:6px; cursor:pointer; font-size:12px; }
+  .ctrl-btn:hover { background:#1f2535; }
+  .ctrl-btn.active { background:var(--accent); border-color:var(--accent); color:#fff; }
+  .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(440px,1fr)); gap:18px; padding:18px 32px 60px; max-width:1280px; margin:0 auto; }
+  .card { background:var(--panel); border:1px solid var(--border); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; transition: transform 0.15s, border-color 0.15s; }
+  .card:hover { transform:translateY(-2px); border-color:var(--accent); }
+  .card .preview { aspect-ratio: 16/10; width:100%; background:#000; border-bottom:1px solid var(--border); overflow:hidden; position:relative; }
+  .card iframe { width:200%; height:200%; transform-origin: 0 0; transform: scale(0.5); border:none; pointer-events:none; }
+  .card .info { padding:12px 14px 14px; }
+  .card .info h3 { font-size:14px; font-weight:600; margin:0 0 2px; }
+  .card .info .desc { color:var(--muted); font-size:12px; line-height:1.45; margin-bottom:10px; }
+  .card .info .open { display:inline-block; background:transparent; color:var(--accent); border:1px solid var(--accent); padding:5px 12px; border-radius:5px; font-size:11.5px; text-decoration:none; }
+  .card .info .open:hover { background:var(--accent); color:#fff; }
+  .full { position:fixed; inset:0; background:#000; z-index:10; display:none; }
+  .full iframe { width:100%; height:100%; border:none; }
+  .full .close { position:absolute; top:14px; right:14px; background:var(--panel); border:1px solid var(--border); color:var(--fg); padding:8px 14px; border-radius:6px; cursor:pointer; font-size:12px; z-index:11; }
+  .full .nav { position:absolute; bottom:18px; left:50%; transform:translateX(-50%); display:flex; gap:6px; background:rgba(0,0,0,0.7); padding:8px 12px; border-radius:8px; z-index:11; backdrop-filter:blur(8px); }
+  .full .nav .ctrl-btn { font-size:11px; }
+  kbd { background:#1f2535; border:1px solid var(--border); padding:1px 6px; border-radius:3px; font-size:11px; font-family:ui-monospace,Menlo,monospace; }
+</style>
+</head><body>
+<div class="hero">
+  <h1>CNCP Failures — 6 mockup variants</h1>
+  <p class="sub">Each tile is a self-contained reimagining of the Failures + Hermes panel. Same data; six different display directions. Click <b>Open</b> for full-screen, then use <kbd>←</kbd>/<kbd>→</kbd> to flip between variants.</p>
+</div>
+<div class="controls">
+  <span style="color:var(--muted); font-size:12px;">All show real CNCP-style data:</span>
+  <span style="font-size:11px; background:#1f2535; padding:3px 8px; border-radius:4px;">5 open + 1 resolved</span>
+  <span style="font-size:11px; background:#1f2535; padding:3px 8px; border-radius:4px;">1 high · 3 medium · 5 low (last 24h Hermes)</span>
+</div>
+<div class="grid" id="grid"></div>
+
+<div class="full" id="full">
+  <button class="close" onclick="closeFull()">✕ Close (Esc)</button>
+  <iframe id="full-frame"></iframe>
+  <div class="nav">
+    <button class="ctrl-btn" onclick="navVariant(-1)">← Prev</button>
+    <span id="full-label" style="color:var(--muted); font-size:12px; align-self:center; min-width:200px; text-align:center;"></span>
+    <button class="ctrl-btn" onclick="navVariant(1)">Next →</button>
+  </div>
+</div>
+
+<script>
+const variants = [
+  { id:'v1-terminal',   title:'1. Terminal · Log-Stream',   desc:'Green-on-black CRT, monospace, severity inline. Built for the engineer who lives in tmux.' },
+  { id:'v2-newspaper',  title:'2. Newspaper · Wire-Service', desc:'Old-style dispatch with masthead, columns, byline. Reads failures as news, not noise.' },
+  { id:'v3-heatmap',    title:'3. Heatmap · Dashboard',     desc:'Density-first: 24-hour severity grid + glanceable stat cells. SRE dashboard energy.' },
+  { id:'v4-kanban',     title:'4. Kanban · Status Board',   desc:'Four lanes (triage → investigating → resolved → dismissed). Atlassian-ish, action-oriented.' },
+  { id:'v5-timeline',   title:'5. Timeline · APM Trace',    desc:'Each process is a lane; spans show event windows. Datadog-style, time-as-axis.' },
+  { id:'v6-brutalist',  title:'6. Brutalist · Data-Dense',  desc:'Hard borders, full table, every field visible. No chrome — just the facts.' }
+];
+
+document.getElementById('grid').innerHTML = variants.map((v, i) => `
+  <div class="card">
+    <div class="preview"><iframe src="${v.id}.html" loading="lazy"></iframe></div>
+    <div class="info">
+      <h3>${v.title}</h3>
+      <div class="desc">${v.desc}</div>
+      <a href="${v.id}.html" class="open" onclick="event.preventDefault(); openFull(${i})">Open full-screen →</a>
+    </div>
+  </div>`).join('');
+
+let currentIdx = 0;
+function openFull(i) {
+  currentIdx = i;
+  const v = variants[i];
+  document.getElementById('full-frame').src = v.id + '.html';
+  document.getElementById('full-label').textContent = v.title;
+  document.getElementById('full').style.display = 'block';
+}
+function closeFull() {
+  document.getElementById('full').style.display = 'none';
+  document.getElementById('full-frame').src = '';
+}
+function navVariant(delta) {
+  currentIdx = (currentIdx + delta + variants.length) % variants.length;
+  openFull(currentIdx);
+}
+window.addEventListener('keydown', e => {
+  if (document.getElementById('full').style.display !== 'block') return;
+  if (e.key === 'Escape') closeFull();
+  if (e.key === 'ArrowLeft') navVariant(-1);
+  if (e.key === 'ArrowRight') navVariant(1);
+});
+</script>
+</body></html>
diff --git a/public/v1-terminal.html b/public/v1-terminal.html
new file mode 100644
index 0000000..f43fea4
--- /dev/null
+++ b/public/v1-terminal.html
@@ -0,0 +1,79 @@
+<!doctype html><html><head>
+<meta charset="utf-8"><title>v1 — Terminal / log-stream</title>
+<style>
+  :root { --green:#00ff41; --amber:#ffb300; --red:#ff3939; --dim:#0c4a17; --bg:#000; --grid:#031b08; }
+  body { margin:0; background:var(--bg); color:var(--green); font-family: ui-monospace,'Menlo','Consolas',monospace; font-size:13px; line-height:1.5; padding:24px; min-height:100vh;
+    background-image: linear-gradient(rgba(0,255,65,0.04) 1px, transparent 1px); background-size: 100% 22px; }
+  .crt { text-shadow: 0 0 4px rgba(0,255,65,0.5); }
+  h1 { font-weight:700; font-size:18px; letter-spacing:2px; margin:0 0 4px; }
+  .meta { color:var(--dim); margin-bottom:18px; }
+  .hr { border-top:1px dashed var(--dim); margin:18px 0; }
+  .row { display:grid; grid-template-columns:18px 70px 90px 30px 28px 1fr; gap:10px; padding:3px 0; align-items:start; }
+  .row .sev { font-weight:700; }
+  .sev.high { color:var(--red); }
+  .sev.medium { color:var(--amber); }
+  .sev.low { color:var(--green); }
+  .src { color:#5fbf6f; }
+  .ev { color:var(--amber); }
+  .when { color:#3a7a47; }
+  .msg { color:#a0eaa6; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+  .head { color:var(--dim); border-bottom:1px solid var(--dim); padding-bottom:4px; margin-bottom:6px; }
+  .hermes { margin-top:28px; padding:12px; border:1px dashed var(--dim); border-radius:0; }
+  .hermes h2 { font-size:13px; margin:0 0 6px; letter-spacing:2px; font-weight:700; }
+  .hermes-row { display:grid; grid-template-columns:180px 1fr; gap:8px; }
+  .bar { display:inline-block; height:6px; vertical-align:middle; }
+  .bar.high { background:var(--red); }
+  .bar.med { background:var(--amber); }
+  .bar.low { background:var(--green); }
+  .blink { animation: blink 1.1s steps(2) infinite; }
+  @keyframes blink { 0%,49% { opacity:1; } 50%,100% { opacity:0; } }
+  .cursor { display:inline-block; width:8px; height:14px; background:var(--green); vertical-align:middle; }
+</style>
+</head><body class="crt">
+<h1>CNCP // FAILURE STREAM <span class="blink">█</span></h1>
+<div class="meta">$ tail -f /failures &nbsp;|&nbsp; UPLINK OK &nbsp;|&nbsp; HERMES READY &nbsp;|&nbsp; $(date)</div>
+<div class="hr"></div>
+
+<div class="row head"><div></div><div>SRC</div><div>PROCESS</div><div>EV</div><div>SEV</div><div>SUMMARY</div></div>
+<div id="rows"></div>
+
+<div class="hr"></div>
+<div class="hermes">
+  <h2>HERMES-3 // CLASSIFIER</h2>
+  <div class="hermes-row"><div>status</div><div>ONLINE · hermes3:8b · 4.3GB · IN-MEM ✓</div></div>
+  <div class="hermes-row"><div>last 24h</div><div id="h-counts"></div></div>
+  <div class="hermes-row"><div>severity dist</div><div id="h-dist"></div></div>
+  <div class="hermes-row"><div>queue</div><div>0 pending · idle</div></div>
+  <div class="hermes-row"><div>recent</div><div id="h-recent"></div></div>
+</div>
+
+<script>
+fetch('/data').then(r=>r.json()).then(d => {
+  const sev = s => s==='high'?'<span class="sev high">HIG</span>': s==='medium'?'<span class="sev medium">MED</span>' : s==='low'?'<span class="sev low">LOW</span>' : '<span style="color:#444;">---</span>';
+  const ts = t => new Date(t).toISOString().slice(11,16);
+  const rows = d.failures.map(f => `
+    <div class="row">
+      <div class="when">${ts(f.lastSeen)}</div>
+      <div class="src">[${f.source.toUpperCase().slice(0,3)}]</div>
+      <div>${f.processName.slice(0,12)}</div>
+      <div class="ev">×${f.eventCount}</div>
+      ${sev(f.severity)}
+      <div class="msg">${(f.lastLines||'').split('\\n')[0].slice(0,110)}</div>
+    </div>
+  `).join('');
+  document.getElementById('rows').innerHTML = rows;
+  const h = d.hermes;
+  document.getElementById('h-counts').textContent = `total:${h.last24h.total} · cls:${h.last24h.byType.classify} · chat:${h.last24h.byType.chat} · idea:${h.last24h.byType['idea-chat']} · ${h.last24h.avgLatencyMs}ms avg`;
+  const max = Math.max(1, h.last24h.bySev.high, h.last24h.bySev.medium, h.last24h.bySev.low);
+  document.getElementById('h-dist').innerHTML = `
+    H<span class="bar high" style="width:${100*h.last24h.bySev.high/max}px"></span> ${h.last24h.bySev.high} ·
+    M<span class="bar med" style="width:${100*h.last24h.bySev.medium/max}px"></span> ${h.last24h.bySev.medium} ·
+    L<span class="bar low" style="width:${100*h.last24h.bySev.low/max}px"></span> ${h.last24h.bySev.low}`;
+  document.getElementById('h-recent').innerHTML = h.recent.slice(0,5).map(c => {
+    const ok = c.ok ? '✓' : '✗';
+    const col = c.ok ? 'var(--green)' : 'var(--red)';
+    return `<div style="color:${col}">${ok} ${new Date(c.ts).toISOString().slice(11,16)} ${c.type.padEnd(10)} ${c.target.slice(0,40)} ${(c.latencyMs/1000).toFixed(1)}s</div>`;
+  }).join('');
+});
+</script>
+</body></html>
diff --git a/public/v2-newspaper.html b/public/v2-newspaper.html
new file mode 100644
index 0000000..c43514c
--- /dev/null
+++ b/public/v2-newspaper.html
@@ -0,0 +1,82 @@
+<!doctype html><html><head>
+<meta charset="utf-8"><title>v2 — Newspaper / wire-service</title>
+<style>
+  :root { --ink:#1a1a1a; --paper:#f3eedf; --rule:#2a2a2a; --accent:#8b0000; }
+  body { margin:0; background:var(--paper); color:var(--ink); font-family: Georgia,'Times New Roman',serif; line-height:1.5; min-height:100vh; }
+  .masthead { padding:24px 32px 14px; border-bottom:4px double var(--ink); }
+  .masthead h1 { font-family: 'Old English Text MT', 'Goudy Old Style', Georgia, serif; font-size:48px; line-height:1; margin:0; letter-spacing:0; }
+  .masthead .stamp { display:flex; justify-content:space-between; font-size:11px; text-transform:uppercase; letter-spacing:2px; margin-top:8px; color:#555; }
+  .masthead .subtitle { font-style:italic; font-size:13px; margin-top:4px; color:#444; }
+  .columns { display:grid; grid-template-columns: 2fr 1fr; gap:32px; padding:22px 32px; }
+  .lead { font-size:13px; }
+  .lead h2 { font-family:Georgia,serif; font-size:22px; letter-spacing:1px; margin:0 0 4px; text-transform:uppercase; }
+  .lead h2.sev-high { color:var(--accent); }
+  .lead .deck { font-style:italic; color:#444; margin-bottom:10px; }
+  .byline { font-size:11px; text-transform:uppercase; color:#777; letter-spacing:1px; margin-bottom:6px; }
+  .article { border-top:1px solid var(--rule); padding-top:12px; margin-top:14px; }
+  .article p { margin:0 0 6px; }
+  .article p.body { columns:2; column-gap:20px; column-rule:1px solid #c9c2af; font-size:12.5px; }
+  .lab { display:inline-block; font-size:10px; text-transform:uppercase; letter-spacing:2px; background:var(--ink); color:var(--paper); padding:2px 6px; margin-right:6px; }
+  .lab.high { background:var(--accent); }
+  .lab.med { background:#a06000; }
+  .lab.low { background:#3d6b2a; }
+  .sidebar { font-size:12px; }
+  .sidebar h3 { font-family:Georgia,serif; font-size:14px; text-transform:uppercase; letter-spacing:1.5px; border-top:3px solid var(--ink); border-bottom:1px solid var(--ink); padding:6px 0; margin:0 0 8px; }
+  .sidebar .stat { display:flex; justify-content:space-between; padding:3px 0; border-bottom:1px dotted #bbb; }
+  .sidebar .recent { font-family:Georgia,serif; font-size:11.5px; line-height:1.5; }
+  .sidebar .recent .item { padding:6px 0; border-bottom:1px dotted #bbb; }
+  .sidebar .recent .item .time { color:#888; font-size:10px; text-transform:uppercase; letter-spacing:1px; }
+  .pill { background:var(--ink); color:var(--paper); padding:1px 5px; font-size:10px; text-transform:uppercase; letter-spacing:1.5px; }
+  .pill.h{ background:var(--accent); }
+  .pill.m{ background:#a06000; }
+  .pill.l{ background:#3d6b2a; }
+</style>
+</head><body>
+<div class="masthead">
+  <h1>The CNCP Dispatch</h1>
+  <div class="subtitle">All the failures fit to triage · A daily wire of process incidents · Vol. 1 No. 1</div>
+  <div class="stamp"><span id="dateStamp"></span><span>Edition: Wire Service</span><span>Hermes-3 desk on duty</span></div>
+</div>
+
+<div class="columns">
+  <div class="lead" id="lead"></div>
+  <div class="sidebar">
+    <h3>The Hermes Desk · 24h</h3>
+    <div id="stats"></div>
+    <h3 style="margin-top:18px;">Latest Filings</h3>
+    <div class="recent" id="recent"></div>
+  </div>
+</div>
+
+<script>
+fetch('/data').then(r=>r.json()).then(d => {
+  const date = new Date().toLocaleDateString('en-US', { weekday:'long', month:'long', day:'numeric', year:'numeric' });
+  document.getElementById('dateStamp').textContent = date.toUpperCase();
+  const lab = s => s==='high'?'<span class="lab high">crit</span>': s==='medium'?'<span class="lab med">warn</span>': s==='low'?'<span class="lab low">note</span>':'<span class="lab">unset</span>';
+  const lead = d.failures.map((f,i) => `
+    <div class="article">
+      <div class="byline">${lab(f.severity)} Filed ${new Date(f.lastSeen).toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'})} · from the ${f.source.toUpperCase()} desk</div>
+      <h2 class="${f.severity==='high'?'sev-high':''}">${f.processName} ${f.exitCode!=null?'reports exit '+f.exitCode:'flagged'} after ${f.eventCount} event${f.eventCount>1?'s':''}</h2>
+      <div class="deck">${f.suggestedAction || 'No action recommended.'}</div>
+      <p class="body">${(f.lastLines||'').split('\\n').filter(Boolean).slice(0,3).join(' ').slice(0,420)}…</p>
+    </div>
+  `).join('');
+  document.getElementById('lead').innerHTML = lead;
+  const h = d.hermes.last24h;
+  const sev = d.hermes.last24h.bySev;
+  document.getElementById('stats').innerHTML = `
+    <div class="stat"><span>Total calls</span><span>${h.total}</span></div>
+    <div class="stat"><span>Avg latency</span><span>${(h.avgLatencyMs/1000).toFixed(1)}s</span></div>
+    <div class="stat"><span>Classifications</span><span>${h.byType.classify}</span></div>
+    <div class="stat"><span>Refinements</span><span>${h.byType.chat}</span></div>
+    <div class="stat"><span>Ideas asked</span><span>${h.byType['idea-chat']}</span></div>
+    <div class="stat"><span>Severity</span><span><span class="pill h">H${sev.high}</span> <span class="pill m">M${sev.medium}</span> <span class="pill l">L${sev.low}</span></span></div>
+    <div class="stat"><span>Failed calls</span><span>${h.fail}</span></div>`;
+  document.getElementById('recent').innerHTML = d.hermes.recent.slice(0,6).map(c => `
+    <div class="item">
+      <div class="time">${new Date(c.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})} · ${c.type}${c.severity?(' · '+lab(c.severity)):''}</div>
+      <div>${c.target}</div>
+    </div>`).join('');
+});
+</script>
+</body></html>
diff --git a/public/v3-heatmap.html b/public/v3-heatmap.html
new file mode 100644
index 0000000..213acce
--- /dev/null
+++ b/public/v3-heatmap.html
@@ -0,0 +1,101 @@
+<!doctype html><html><head>
+<meta charset="utf-8"><title>v3 — Heatmap dashboard</title>
+<style>
+  :root { --bg:#0b1220; --ink:#e6eef8; --muted:#6b7a91; --grid:#1c2740; --hi:#ef4444; --med:#f59e0b; --lo:#facc15; --ok:#22c55e; --accent:#60a5fa; }
+  body { margin:0; background:radial-gradient(ellipse at top left,#142543,var(--bg) 60%); color:var(--ink); font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text','Inter',system-ui,sans-serif; padding:24px; min-height:100vh; }
+  h1 { font-size:18px; font-weight:600; letter-spacing:-0.01em; margin:0 0 4px; }
+  .sub { color:var(--muted); font-size:12px; margin-bottom:18px; }
+  .grid-frame { background:rgba(28,39,64,0.4); border:1px solid var(--grid); border-radius:10px; padding:16px; backdrop-filter: blur(10px); }
+  .stats { display:grid; grid-template-columns: repeat(6, 1fr); gap:10px; margin-bottom:18px; }
+  .stat { background:rgba(28,39,64,0.4); border:1px solid var(--grid); border-radius:8px; padding:10px 12px; }
+  .stat .label { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:1px; margin-bottom:3px; }
+  .stat .val { font-size:22px; font-weight:600; }
+  .stat.high .val { color:var(--hi); }
+  .stat.med .val { color:var(--med); }
+  .stat.low .val { color:var(--lo); }
+  .heatmap { display:grid; grid-template-columns: 1fr 280px; gap:18px; }
+  .hm-grid { display:grid; gap:6px; grid-template-columns:repeat(24,1fr); }
+  .cell { aspect-ratio:1; border-radius:3px; background:#1a2540; position:relative; transition:transform 0.15s; }
+  .cell:hover { transform:scale(1.4); z-index:5; box-shadow:0 4px 14px rgba(0,0,0,0.4); }
+  .cell.h { background:var(--hi); }
+  .cell.m { background:var(--med); }
+  .cell.l { background:var(--lo); }
+  .cell.h, .cell.m, .cell.l { box-shadow: 0 0 8px currentColor; }
+  .failure-list { display:flex; flex-direction:column; gap:6px; }
+  .failure-row { display:grid; grid-template-columns:60px 1fr 50px; gap:10px; align-items:center; padding:8px 10px; background:rgba(28,39,64,0.3); border:1px solid var(--grid); border-radius:6px; font-size:12px; }
+  .failure-row .name { font-family:ui-monospace,Menlo,monospace; color:var(--ink); }
+  .failure-row .ev { font-variant-numeric: tabular-nums; color:var(--muted); text-align:right; font-size:11px; }
+  .sev-dot { width:8px; height:8px; border-radius:50%; display:inline-block; }
+  .sev-dot.h{ background:var(--hi); }
+  .sev-dot.m{ background:var(--med); }
+  .sev-dot.l{ background:var(--lo); }
+  .sev-dot.u{ background:#475569; }
+  .spark { display:flex; gap:1px; align-items:end; height:18px; }
+  .spark .bar { width:3px; background:var(--accent); min-height:1px; }
+  .hermes-strip { margin-top:18px; display:grid; grid-template-columns: 220px 1fr; gap:18px; padding:14px 16px; background:rgba(28,39,64,0.4); border:1px solid var(--grid); border-radius:8px; }
+  .pulse { width:6px; height:6px; border-radius:50%; background:var(--ok); display:inline-block; box-shadow:0 0 8px var(--ok); animation: pulse 2s infinite; }
+  @keyframes pulse { 50% { opacity:0.3; transform:scale(0.85); } }
+</style>
+</head><body>
+<h1>Failure Heatmap</h1>
+<div class="sub">Last 24 hours · density-first view · brighter = more severe</div>
+<div class="grid-frame">
+  <div class="stats" id="stats"></div>
+  <div class="heatmap">
+    <div>
+      <div class="sub" style="margin-bottom:6px;">Severity over 24h (each cell = ~1 hour, brightest = highest severity event)</div>
+      <div class="hm-grid" id="hm"></div>
+    </div>
+    <div class="failure-list" id="rows"></div>
+  </div>
+  <div class="hermes-strip">
+    <div>
+      <div class="sub" style="font-size:10px;">HERMES-3 · 8B</div>
+      <div><span class="pulse"></span> <span style="font-weight:600;">online</span> · in mem · 4.3GB</div>
+      <div class="sub" style="font-size:10px; margin-top:4px;" id="h-summary"></div>
+    </div>
+    <div>
+      <div class="sub" style="font-size:10px; margin-bottom:6px;">RECENT CALLS</div>
+      <div id="recent" style="display:flex; gap:6px; overflow-x:auto;"></div>
+    </div>
+  </div>
+</div>
+<script>
+fetch('/data').then(r=>r.json()).then(d => {
+  // build a fake 24-cell heatmap weighted by events (illustrative)
+  const cells = Array.from({length: 24}, (_, i) => {
+    const r = Math.random();
+    if (r < 0.05) return 'h';
+    if (r < 0.18) return 'm';
+    if (r < 0.4) return 'l';
+    return '';
+  });
+  // tweak a few to known points
+  cells[14]='h'; cells[15]='m'; cells[18]='m'; cells[20]='m'; cells[21]='m';
+  document.getElementById('hm').innerHTML = cells.map(c => `<div class="cell ${c}"></div>`).join('');
+  const sev = d.hermes.last24h.bySev;
+  document.getElementById('stats').innerHTML = `
+    <div class="stat high"><div class="label">High</div><div class="val">${sev.high}</div></div>
+    <div class="stat med"><div class="label">Medium</div><div class="val">${sev.medium}</div></div>
+    <div class="stat low"><div class="label">Low</div><div class="val">${sev.low}</div></div>
+    <div class="stat"><div class="label">Open</div><div class="val">${d.failures.filter(f=>f.status==='triage'||f.status==='investigating').length}</div></div>
+    <div class="stat"><div class="label">Events 24h</div><div class="val">${d.failures.reduce((s,f)=>s+(f.eventCount||0),0)}</div></div>
+    <div class="stat"><div class="label">Hermes p50</div><div class="val">${(d.hermes.last24h.avgLatencyMs/1000).toFixed(1)}<span style="font-size:11px;color:var(--muted);">s</span></div></div>`;
+  const sevClass = s => s==='high'?'h':s==='medium'?'m':s==='low'?'l':'u';
+  document.getElementById('rows').innerHTML = d.failures.map(f => `
+    <div class="failure-row">
+      <div><span class="sev-dot ${sevClass(f.severity)}"></span> <span style="color:var(--muted); font-size:10px; text-transform:uppercase;">${f.source}</span></div>
+      <div class="name">${f.processName}</div>
+      <div class="ev">×${f.eventCount}</div>
+    </div>`).join('');
+  const h = d.hermes.last24h;
+  document.getElementById('h-summary').textContent = `${h.total} calls · ${h.byType.classify} cls · ${h.byType.chat} chat · ${h.byType['idea-chat']} idea · ${h.fail} failed`;
+  document.getElementById('recent').innerHTML = d.hermes.recent.slice(0,8).map(c => `
+    <div style="flex:0 0 auto; padding:8px 10px; background:rgba(255,255,255,0.04); border:1px solid var(--grid); border-radius:6px; font-size:11px; min-width:140px;">
+      <div style="color:var(--muted); font-size:9px; text-transform:uppercase; letter-spacing:1px;">${c.type}</div>
+      <div style="font-weight:500; margin-top:2px;">${c.target.slice(0,18)}</div>
+      <div style="color:${c.ok?'var(--ok)':'var(--hi)'}; font-size:10px; margin-top:2px;">${c.ok?'✓':'✗'} ${(c.latencyMs/1000).toFixed(1)}s</div>
+    </div>`).join('');
+});
+</script>
+</body></html>
diff --git a/public/v4-kanban.html b/public/v4-kanban.html
new file mode 100644
index 0000000..8521f9c
--- /dev/null
+++ b/public/v4-kanban.html
@@ -0,0 +1,90 @@
+<!doctype html><html><head>
+<meta charset="utf-8"><title>v4 — Kanban board</title>
+<style>
+  :root { --bg:#f4f5f7; --ink:#172b4d; --muted:#5e6c84; --col-bg:#ebecf0; --card:#fff; --hi:#de350b; --med:#ff8b00; --lo:#0052cc; --ok:#36b37e; --shadow: 0 1px 0 rgba(9,30,66,0.25); }
+  body { margin:0; background:var(--bg); color:var(--ink); font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif; padding:0; min-height:100vh; }
+  header { padding:14px 24px; border-bottom:1px solid #dfe1e6; background:#fff; display:flex; justify-content:space-between; align-items:center; }
+  header h1 { font-size:18px; font-weight:600; margin:0; }
+  header .meta { font-size:12px; color:var(--muted); }
+  .board { display:grid; grid-template-columns: repeat(4, 1fr); gap:14px; padding:18px 24px; align-items:start; }
+  .col { background:var(--col-bg); border-radius:6px; padding:10px; min-height: 65vh; }
+  .col h2 { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:1px; color:var(--muted); margin:0 0 10px; display:flex; justify-content:space-between; }
+  .col h2 .count { background:#dfe1e6; padding:2px 8px; border-radius:10px; font-size:11px; }
+  .card { background:var(--card); border-radius:4px; padding:10px 12px; box-shadow:var(--shadow); margin-bottom:8px; font-size:13px; cursor:grab; transition: box-shadow 0.15s; }
+  .card:hover { box-shadow: 0 2px 8px rgba(9,30,66,0.16); }
+  .card .src { display:inline-block; font-size:10px; text-transform:uppercase; letter-spacing:1px; padding:2px 6px; border-radius:3px; color:#fff; }
+  .card .src.pm2{ background:#0052cc; }
+  .card .src.launchd{ background:#6554c0; }
+  .card .src.agent{ background:#0747a6; }
+  .card .src.build{ background:#974f0c; }
+  .card .name { font-family:ui-monospace,Menlo,monospace; font-weight:600; margin-top:6px; font-size:12.5px; word-break:break-all; }
+  .card .last { color:var(--muted); font-size:11px; margin-top:6px; line-height:1.4; max-height:42px; overflow:hidden; }
+  .card .action { color:var(--lo); font-size:11px; font-style:italic; margin-top:6px; }
+  .card .footer { display:flex; justify-content:space-between; align-items:center; margin-top:8px; }
+  .sev { width:6px; height:6px; border-radius:50%; display:inline-block; vertical-align:middle; }
+  .sev.h { background:var(--hi); }
+  .sev.m { background:var(--med); }
+  .sev.l { background:var(--lo); }
+  .sev.u { background:#a5adba; }
+  .pill { background:#dfe1e6; padding:2px 7px; border-radius:10px; font-size:11px; color:var(--muted); }
+  .hermes-bar { background:#fff; border:1px solid #dfe1e6; border-radius:6px; padding:12px 16px; margin: 0 24px 12px; display:grid; grid-template-columns: auto 1fr auto; gap:18px; align-items:center; }
+  .hermes-bar .pulse { width:8px; height:8px; border-radius:50%; background:var(--ok); display:inline-block; animation: pulse 2s infinite; }
+  @keyframes pulse { 50% { opacity:0.3; } }
+  .hermes-bar .stat-line { font-size:12px; color:var(--muted); }
+  .hermes-bar .stat-line b { color:var(--ink); font-weight:600; }
+  .hermes-bar .recent { font-size:11px; color:var(--muted); }
+</style>
+</head><body>
+<header>
+  <h1>CNCP Failures · Board</h1>
+  <div class="meta">drag cards between lanes · auto-classify on triage</div>
+</header>
+
+<div class="hermes-bar">
+  <div><span class="pulse"></span> <b>Hermes 3</b> · 8B · in mem</div>
+  <div class="stat-line" id="h-stats"></div>
+  <div class="recent" id="h-recent"></div>
+</div>
+
+<div class="board" id="board"></div>
+
+<script>
+fetch('/data').then(r=>r.json()).then(d => {
+  const lanes = [
+    { id:'triage', title:'Triage', sub:'classified, awaiting action' },
+    { id:'investigating', title:'Investigating', sub:'someone is on it' },
+    { id:'resolved', title:'Resolved', sub:'fix verified' },
+    { id:'dismissed', title:'Dismissed', sub:'transient · auto-resolved' }
+  ];
+  const sevClass = s => s==='high'?'h':s==='medium'?'m':s==='low'?'l':'u';
+  const byLane = Object.fromEntries(lanes.map(l => [l.id, []]));
+  for (const f of d.failures) (byLane[f.status] || (byLane[f.status]=[])).push(f);
+
+  document.getElementById('board').innerHTML = lanes.map(l => `
+    <div class="col">
+      <h2>${l.title}<span class="count">${(byLane[l.id]||[]).length}</span></h2>
+      ${(byLane[l.id]||[]).map(f => `
+        <div class="card">
+          <div><span class="src ${f.source}">${f.source}</span> <span class="sev ${sevClass(f.severity)}"></span> <span class="pill">×${f.eventCount}</span></div>
+          <div class="name">${f.processName}</div>
+          <div class="last">${(f.lastLines||'').split('\\n').filter(Boolean)[0] || ''}</div>
+          ${f.suggestedAction ? `<div class="action">→ ${f.suggestedAction}</div>` : ''}
+          <div class="footer">
+            <span style="font-size:10px; color:var(--muted);">${new Date(f.lastSeen).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</span>
+            <span style="font-size:10px; color:var(--muted);">#${f.id.slice(0,7)}</span>
+          </div>
+        </div>
+      `).join('')}
+    </div>`).join('');
+
+  const h = d.hermes.last24h;
+  const sev = h.bySev;
+  document.getElementById('h-stats').innerHTML = `
+    <b>${h.total}</b> calls · <b>${h.byType.classify}</b> cls · <b>${h.byType.chat}</b> chat · <b>${h.byType['idea-chat']}</b> idea ·
+    sev <b style="color:var(--hi)">${sev.high}H</b> <b style="color:var(--med)">${sev.medium}M</b> <b style="color:var(--lo)">${sev.low}L</b> ·
+    p50 <b>${(h.avgLatencyMs/1000).toFixed(1)}s</b>`;
+  document.getElementById('h-recent').innerHTML = d.hermes.recent.slice(0,3).map(c => `
+    <div>${c.ok?'✓':'✗'} ${c.type}: ${c.target.slice(0,28)} (${(c.latencyMs/1000).toFixed(1)}s)</div>`).join('');
+});
+</script>
+</body></html>
diff --git a/public/v5-timeline.html b/public/v5-timeline.html
new file mode 100644
index 0000000..0cb42bf
--- /dev/null
+++ b/public/v5-timeline.html
@@ -0,0 +1,120 @@
+<!doctype html><html><head>
+<meta charset="utf-8"><title>v5 — Timeline / APM trace</title>
+<style>
+  :root { --bg:#06070a; --ink:#e4e7ec; --muted:#7d8696; --grid:#16191f; --lane-bg:#0c0e13; --hi:#ff4757; --med:#ffa726; --lo:#42a5f5; --ok:#26d07c; --accent:#7c3aed; }
+  body { margin:0; background:var(--bg); color:var(--ink); font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif; padding:18px; min-height:100vh; }
+  h1 { font-size:16px; font-weight:600; margin:0 0 2px; letter-spacing:-0.01em; }
+  .sub { color:var(--muted); font-size:11px; margin-bottom:18px; }
+  .legend { display:flex; gap:18px; font-size:11px; margin-bottom:14px; }
+  .legend span { display:inline-block; width:10px; height:10px; border-radius:2px; margin-right:5px; vertical-align:middle; }
+  .timeline { background:var(--lane-bg); border:1px solid var(--grid); border-radius:8px; overflow:hidden; }
+  .ruler { display:grid; grid-template-columns: 200px 1fr; font-size:10px; color:var(--muted); }
+  .ruler .label { padding:8px 12px; text-transform:uppercase; letter-spacing:1px; border-right:1px solid var(--grid); }
+  .ruler .ticks { display:grid; grid-template-columns: repeat(24, 1fr); padding:8px 0; border-left:1px solid var(--grid); }
+  .ruler .ticks div { text-align:left; padding-left:4px; border-left:1px solid var(--grid); font-variant-numeric:tabular-nums; }
+  .lane { display:grid; grid-template-columns: 200px 1fr; border-top:1px solid var(--grid); min-height:48px; align-items:center; }
+  .lane .info { padding:10px 12px; border-right:1px solid var(--grid); font-size:12px; }
+  .lane .info .nm { font-family:ui-monospace,Menlo,monospace; font-weight:500; word-break:break-all; font-size:11.5px; }
+  .lane .info .meta { color:var(--muted); font-size:10px; margin-top:3px; display:flex; gap:6px; align-items:center; }
+  .lane .track { position:relative; padding:6px 0; }
+  .span { position:absolute; height:22px; top:50%; margin-top:-11px; border-radius:3px; padding:0 8px; display:flex; align-items:center; font-size:10px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:pointer; transition:transform 0.15s; }
+  .span:hover { transform:scaleY(1.15); z-index:5; }
+  .span.h{ background:var(--hi); box-shadow: 0 0 12px rgba(255,71,87,0.5); color:#fff; }
+  .span.m{ background:var(--med); color:#1c1c1c; }
+  .span.l{ background:var(--lo); color:#fff; }
+  .span.u{ background:#4a5163; color:#fff; }
+  .src-pill { display:inline-block; font-size:9px; padding:1px 4px; border-radius:2px; text-transform:uppercase; letter-spacing:0.5px; font-weight:600; }
+  .src-pill.pm2{ background:#1e3a5f; color:#90caf9; }
+  .src-pill.launchd{ background:#3a2c5e; color:#bb86fc; }
+  .src-pill.agent{ background:#3a4a1e; color:#aed581; }
+  .src-pill.build{ background:#5e3a1e; color:#ffcc80; }
+  .hermes-pane { margin-top:18px; background:var(--lane-bg); border:1px solid var(--grid); border-radius:8px; padding:14px 16px; }
+  .hermes-pane h2 { font-size:13px; font-weight:600; margin:0 0 10px; display:flex; align-items:center; gap:8px; }
+  .hermes-pane .row { display:grid; grid-template-columns: 100px 1fr; gap:10px; font-size:12px; padding:4px 0; align-items:center; }
+  .hermes-pane .bar { height:6px; border-radius:2px; background:var(--accent); }
+  .pulse { width:8px; height:8px; border-radius:50%; background:var(--ok); display:inline-block; box-shadow:0 0 8px var(--ok); animation: pulse 2s infinite; }
+  @keyframes pulse { 50% { opacity:0.3; } }
+  .trace-row { display:grid; grid-template-columns: 80px 50px 1fr 80px; gap:8px; font-size:11px; padding:3px 0; align-items:center; }
+  .trace-bar { background:var(--accent); height:6px; border-radius:2px; opacity:0.6; }
+</style>
+</head><body>
+<h1>Failure Timeline · 24h trace</h1>
+<div class="sub">Each lane = one process · spans show event windows · severity colors</div>
+<div class="legend">
+  <div><span style="background:var(--hi)"></span>high</div>
+  <div><span style="background:var(--med)"></span>medium</div>
+  <div><span style="background:var(--lo)"></span>low</div>
+  <div><span style="background:#4a5163"></span>unclassified</div>
+</div>
+
+<div class="timeline">
+  <div class="ruler">
+    <div class="label">Process</div>
+    <div class="ticks" id="ticks"></div>
+  </div>
+  <div id="lanes"></div>
+</div>
+
+<div class="hermes-pane">
+  <h2><span class="pulse"></span> Hermes 3 · 8B · in mem (4.3GB)</h2>
+  <div id="h-rows"></div>
+  <h2 style="margin-top:16px;">Recent Call Trace</h2>
+  <div id="h-trace"></div>
+</div>
+
+<script>
+fetch('/data').then(r=>r.json()).then(d => {
+  // 24-hour window ending now
+  const now = Date.now();
+  const span24h = 24 * 3600 * 1000;
+  const start = now - span24h;
+  // Ticks: every 3 hours
+  document.getElementById('ticks').style.gridTemplateColumns = 'repeat(8, 1fr)';
+  document.getElementById('ticks').innerHTML = Array.from({length:8}, (_, i) => {
+    const t = new Date(start + i*3*3600*1000);
+    return `<div>${t.getHours().toString().padStart(2,'0')}:00</div>`;
+  }).join('');
+  const sevClass = s => s==='high'?'h':s==='medium'?'m':s==='low'?'l':'u';
+  document.getElementById('lanes').innerHTML = d.failures.map(f => {
+    const created = Math.max(start, f.created);
+    const last = Math.min(now, f.lastSeen || f.created);
+    const leftPct = 100 * (created - start) / span24h;
+    const widthPct = Math.max(2, 100 * (last - created) / span24h);
+    return `
+      <div class="lane">
+        <div class="info">
+          <div class="nm">${f.processName}</div>
+          <div class="meta"><span class="src-pill ${f.source}">${f.source}</span> ×${f.eventCount} ${f.exitCode!=null?'· exit '+f.exitCode:''}</div>
+        </div>
+        <div class="track">
+          <div class="span ${sevClass(f.severity)}" style="left:${leftPct}%; width:${widthPct}%;" title="${f.suggestedAction || ''}">
+            ${f.severity || '—'} · ${(f.lastLines||'').split('\\n')[0].slice(0,40)}
+          </div>
+        </div>
+      </div>`;
+  }).join('');
+
+  const h = d.hermes.last24h;
+  const sev = h.bySev;
+  const max = Math.max(1, sev.high, sev.medium, sev.low);
+  document.getElementById('h-rows').innerHTML = `
+    <div class="row"><span style="color:var(--muted)">total 24h</span><span><b>${h.total}</b> calls · ${h.byType.classify} cls · ${h.byType.chat} chat · ${h.byType['idea-chat']} idea</span></div>
+    <div class="row"><span style="color:var(--muted)">latency</span><span><b>${(h.avgLatencyMs/1000).toFixed(1)}s</b> avg · ${h.fail} failed</span></div>
+    <div class="row"><span style="color:var(--muted)">severity</span><span>
+      <span style="display:inline-block; vertical-align:middle; margin-right:8px;"><span style="color:var(--hi)">${sev.high} H</span> <div class="bar" style="width:${100*sev.high/max}px; background:var(--hi); display:inline-block; height:8px; vertical-align:middle;"></div></span>
+      <span style="display:inline-block; vertical-align:middle; margin-right:8px;"><span style="color:var(--med)">${sev.medium} M</span> <div class="bar" style="width:${100*sev.medium/max}px; background:var(--med); display:inline-block; height:8px; vertical-align:middle;"></div></span>
+      <span style="display:inline-block; vertical-align:middle;"><span style="color:var(--lo)">${sev.low} L</span> <div class="bar" style="width:${100*sev.low/max}px; background:var(--lo); display:inline-block; height:8px; vertical-align:middle;"></div></span>
+    </span></div>`;
+  document.getElementById('h-trace').innerHTML = d.hermes.recent.slice(0,8).map(c => {
+    const w = Math.min(100, c.latencyMs/150);
+    return `
+      <div class="trace-row">
+        <span style="color:var(--muted);">${new Date(c.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</span>
+        <span style="color:${c.ok?'var(--ok)':'var(--hi)'}">${c.ok?'✓':'✗'} ${c.type.slice(0,5)}</span>
+        <span style="font-family:ui-monospace,Menlo,monospace; font-size:10.5px;">${c.target.slice(0,46)}</span>
+        <span><div class="trace-bar" style="width:${w}px;"></div> <span style="color:var(--muted); font-size:10px;">${(c.latencyMs/1000).toFixed(1)}s</span></span>
+      </div>`;
+  }).join('');
+});
+</script>
+</body></html>
diff --git a/public/v6-brutalist.html b/public/v6-brutalist.html
new file mode 100644
index 0000000..2423ea9
--- /dev/null
+++ b/public/v6-brutalist.html
@@ -0,0 +1,120 @@
+<!doctype html><html><head>
+<meta charset="utf-8"><title>v6 — Brutalist data-dense</title>
+<style>
+  body { margin:0; background:#fff; color:#000; font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-weight:500; padding:0; min-height:100vh; }
+  * { box-sizing: border-box; }
+  .top { display:grid; grid-template-columns: 1fr auto; padding:16px 24px; border-bottom:3px solid #000; }
+  .top h1 { margin:0; font-size:28px; font-weight:900; letter-spacing:-0.02em; text-transform:uppercase; }
+  .top .stamp { font-size:11px; text-transform:uppercase; letter-spacing:2px; align-self:center; padding:4px 10px; border:2px solid #000; }
+  .stats { display:grid; grid-template-columns: repeat(8, 1fr); border-bottom:3px solid #000; }
+  .stat { padding:14px 12px; border-right:1px solid #000; }
+  .stat:last-child { border-right:none; }
+  .stat .v { font-size:30px; font-weight:900; letter-spacing:-0.02em; line-height:1; }
+  .stat .l { font-size:9px; text-transform:uppercase; letter-spacing:1.5px; margin-top:4px; color:#000; }
+  .stat.h .v { color:#d00; }
+  .stat.m .v { color:#b56300; }
+  .stat.l-low .v { color:#005bbb; }
+  table.dense { width:100%; border-collapse:collapse; font-size:11.5px; font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; }
+  table.dense th { text-align:left; background:#000; color:#fff; padding:6px 8px; font-weight:700; text-transform:uppercase; letter-spacing:1px; font-size:10px; border-right:1px solid #555; }
+  table.dense td { padding:6px 8px; border-right:1px solid #ccc; border-bottom:1px solid #ddd; vertical-align:top; }
+  table.dense tr:nth-child(even) td { background:#f4f4f4; }
+  table.dense .mono { font-family:ui-monospace,Menlo,monospace; }
+  table.dense .sev-h { background:#d00; color:#fff; padding:2px 6px; font-weight:700; text-transform:uppercase; font-size:9px; letter-spacing:1px; }
+  table.dense .sev-m { background:#b56300; color:#fff; padding:2px 6px; font-weight:700; text-transform:uppercase; font-size:9px; letter-spacing:1px; }
+  table.dense .sev-l { background:#005bbb; color:#fff; padding:2px 6px; font-weight:700; text-transform:uppercase; font-size:9px; letter-spacing:1px; }
+  table.dense .src { font-weight:700; text-transform:uppercase; font-size:10px; letter-spacing:1px; }
+  table.dense .status-triage { background:#fff3cd; }
+  table.dense .status-resolved { background:#d4edda; }
+  table.dense .status-investigating { background:#cce5ff; }
+  table.dense .status-dismissed { background:#e2e3e5; color:#666; }
+  table.dense .lines { font-family:ui-monospace,Menlo,monospace; color:#555; font-size:10.5px; line-height:1.4; max-width:300px; white-space:pre-wrap; word-break:break-word; }
+  table.dense .action { color:#005bbb; font-style:italic; font-size:11px; }
+  .hermes-block { padding:14px 24px; border-top:3px solid #000; }
+  .hermes-block h2 { margin:0 0 8px; font-size:14px; font-weight:900; text-transform:uppercase; letter-spacing:2px; }
+  .hermes-block table { width:100%; border-collapse:collapse; font-size:11px; }
+  .hermes-block table td { padding:4px 8px; border-bottom:1px solid #ddd; }
+  .hermes-block table td:first-child { text-transform:uppercase; letter-spacing:1px; font-size:10px; color:#666; width:120px; }
+  .recent-grid { display:grid; grid-template-columns: repeat(3, 1fr); gap:0; border:1px solid #000; margin-top:8px; }
+  .recent-grid > div { padding:8px 10px; border-right:1px solid #ddd; border-bottom:1px solid #ddd; font-size:10.5px; }
+  .recent-grid > div:nth-child(3n) { border-right:none; }
+</style>
+</head><body>
+<div class="top">
+  <h1>Failures · Brutalist Index</h1>
+  <div class="stamp" id="stamp"></div>
+</div>
+
+<div class="stats" id="stats"></div>
+
+<table class="dense">
+  <thead>
+    <tr>
+      <th>#</th>
+      <th>SEV</th>
+      <th>SRC</th>
+      <th>STATUS</th>
+      <th>PROCESS</th>
+      <th>EV</th>
+      <th>EXIT</th>
+      <th>CATEGORY</th>
+      <th>SUGGESTED ACTION</th>
+      <th>LAST OUTPUT (first line)</th>
+      <th>SEEN</th>
+    </tr>
+  </thead>
+  <tbody id="tbody"></tbody>
+</table>
+
+<div class="hermes-block">
+  <h2>Hermes 3 · 8B · in mem · 4.3GB · 127.0.0.1:11434 · online</h2>
+  <table id="h-stats"></table>
+  <h2 style="margin-top:14px;">Recent Calls</h2>
+  <div class="recent-grid" id="h-recent"></div>
+</div>
+
+<script>
+fetch('/data').then(r=>r.json()).then(d => {
+  document.getElementById('stamp').textContent = new Date().toUpperCase ? new Date().toISOString().slice(0,16).replace('T',' ') : '';
+  document.getElementById('stamp').textContent = new Date().toISOString().slice(0,16).replace('T',' ') + ' UTC';
+  const sev = d.hermes.last24h.bySev;
+  const open = d.failures.filter(f=>f.status==='triage'||f.status==='investigating').length;
+  const eventsTotal = d.failures.reduce((s,f)=>s+(f.eventCount||0),0);
+  document.getElementById('stats').innerHTML = `
+    <div class="stat h"><div class="v">${sev.high}</div><div class="l">High</div></div>
+    <div class="stat m"><div class="v">${sev.medium}</div><div class="l">Medium</div></div>
+    <div class="stat l-low"><div class="v">${sev.low}</div><div class="l">Low</div></div>
+    <div class="stat"><div class="v">${open}</div><div class="l">Open</div></div>
+    <div class="stat"><div class="v">${eventsTotal}</div><div class="l">Events 24h</div></div>
+    <div class="stat"><div class="v">${d.hermes.last24h.total}</div><div class="l">Hermes Calls</div></div>
+    <div class="stat"><div class="v">${(d.hermes.last24h.avgLatencyMs/1000).toFixed(1)}<span style="font-size:13px;color:#666">s</span></div><div class="l">Avg Latency</div></div>
+    <div class="stat"><div class="v">${d.hermes.last24h.fail}</div><div class="l">Failed Calls</div></div>`;
+  const sevCls = s => s==='high'?'sev-h':s==='medium'?'sev-m':s==='low'?'sev-l':'';
+  document.getElementById('tbody').innerHTML = d.failures.map((f,i) => `
+    <tr class="status-${f.status}">
+      <td class="mono">${i+1}</td>
+      <td><span class="${sevCls(f.severity)}">${f.severity || '—'}</span></td>
+      <td class="src">${f.source}</td>
+      <td>${f.status}</td>
+      <td class="mono"><b>${f.processName}</b></td>
+      <td class="mono">${f.eventCount}</td>
+      <td class="mono">${f.exitCode != null ? f.exitCode : '—'}</td>
+      <td>${f.category || '—'}</td>
+      <td class="action">${f.suggestedAction || '—'}</td>
+      <td class="lines">${(f.lastLines||'').split('\\n').filter(Boolean)[0] || '—'}</td>
+      <td class="mono" style="white-space:nowrap;">${new Date(f.lastSeen).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</td>
+    </tr>`).join('');
+  const h = d.hermes.last24h;
+  document.getElementById('h-stats').innerHTML = `
+    <tr><td>Total 24h</td><td><b>${h.total}</b></td><td>p50 latency</td><td><b>${(h.avgLatencyMs/1000).toFixed(1)}s</b></td></tr>
+    <tr><td>Classify</td><td><b>${h.byType.classify}</b></td><td>Refinement chat</td><td><b>${h.byType.chat}</b></td></tr>
+    <tr><td>Idea-chat</td><td><b>${h.byType['idea-chat']}</b></td><td>Failed</td><td><b>${h.fail}</b></td></tr>
+    <tr><td>Severity Dist</td><td colspan="3"><span class="${sevCls('high')}">${sev.high}H</span> <span class="${sevCls('medium')}">${sev.medium}M</span> <span class="${sevCls('low')}">${sev.low}L</span></td></tr>`;
+  document.getElementById('h-recent').innerHTML = d.hermes.recent.slice(0,9).map(c => `
+    <div>
+      <div style="font-family:ui-monospace,Menlo,monospace; color:#666; font-size:9px;">${new Date(c.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})} · ${c.type}</div>
+      <div style="font-weight:600; margin-top:2px; word-break:break-word;">${c.target}</div>
+      <div style="color:${c.ok?'#005bbb':'#d00'}; margin-top:2px; font-size:10px;">${c.ok?'✓':'✗'} ${(c.latencyMs/1000).toFixed(1)}s${c.severity?(' · '+c.severity):''}</div>
+    </div>`).join('');
+});
+</script>
+</body></html>
diff --git a/sample-data.json b/sample-data.json
new file mode 100644
index 0000000..e30d845
--- /dev/null
+++ b/sample-data.json
@@ -0,0 +1,116 @@
+{
+  "failures": [
+    {
+      "id": "mp1ka4pwqdza",
+      "source": "pm2",
+      "processName": "contact-mailer-ideas-viewer",
+      "exitCode": 0,
+      "severity": "medium",
+      "category": "needs-investigation",
+      "status": "triage",
+      "eventCount": 5,
+      "created": 1778500000000,
+      "lastSeen": 1778530281000,
+      "suggestedAction": "Restart process and monitor for stability",
+      "lastLines": "2026-05-11T12:51:21: TypeError: bad regexp at viewer/server.js:312\n  const re = new RegExp('('+q.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g,'\\\\\\\\$&')+')','gi');\n  SyntaxError: Invalid regular expression"
+    },
+    {
+      "id": "mp1k4yawqgae",
+      "source": "launchd",
+      "processName": "big-red-tunnel",
+      "exitCode": null,
+      "severity": "medium",
+      "category": "transient",
+      "status": "triage",
+      "eventCount": 166,
+      "created": 1778490000000,
+      "lastSeen": 1778530300000,
+      "suggestedAction": "Monitor and restart process if it continues to fail",
+      "lastLines": "2026-05-11T20:32:05Z ERR failed to run the datagram handler error=\"context canceled\"\n2026-05-11T20:32:05Z ERR failed to serve tunnel connection error=\"control stream encountered a failure while serving\"\n2026-05-11T20:32:21Z INF Retrying connection in up to 1m4s"
+    },
+    {
+      "id": "mp1klu8wzqee",
+      "source": "launchd",
+      "processName": "microsite-hawk",
+      "exitCode": null,
+      "severity": "low",
+      "category": "transient",
+      "status": "triage",
+      "eventCount": 10,
+      "created": 1778520000000,
+      "lastSeen": 1778530100000,
+      "suggestedAction": "Monitor for persistent issues",
+      "lastLines": "[hawk] Cycle complete — 2026-05-11T20:27:13Z | failures: 0 | log: /Users/stevestudio2/Projects/_microsite-hawk/logs/hawk-2026-05-11.jsonl"
+    },
+    {
+      "id": "synthetic-high-1",
+      "source": "agent",
+      "processName": "shopify-bulk-importer",
+      "exitCode": 137,
+      "severity": "high",
+      "category": "needs-rebuild",
+      "status": "investigating",
+      "eventCount": 3,
+      "created": 1778525000000,
+      "lastSeen": 1778530000000,
+      "suggestedAction": "Increase node --max-old-space-size and re-run with smaller batches",
+      "lastLines": "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory\nKilled (SIGKILL exit 137)\nLast batch: 4,217 / 8,500 products processed"
+    },
+    {
+      "id": "synthetic-build-1",
+      "source": "build",
+      "processName": "novasuede-deploy",
+      "exitCode": 1,
+      "severity": "medium",
+      "category": "needs-investigation",
+      "status": "triage",
+      "eventCount": 2,
+      "created": 1778526000000,
+      "lastSeen": 1778529500000,
+      "suggestedAction": "Check Kamatera disk space and pm2 env vars before redeploy",
+      "lastLines": "rsync: failed to write to /var/www/novasuede/dist (No space left on device)\npm2 reload failed: ENOENT no such file\ndeploy aborted at stage 4/6"
+    },
+    {
+      "id": "synthetic-pm2-1",
+      "source": "pm2",
+      "processName": "pd-enrich",
+      "exitCode": 2,
+      "severity": "low",
+      "category": "transient",
+      "status": "resolved",
+      "eventCount": 1,
+      "created": 1778450000000,
+      "lastSeen": 1778451000000,
+      "suggestedAction": "Watch for repeat; restart if it recurs",
+      "lastLines": "warn: gemini rate-limit hit, backed off 30s\ninfo: resumed enrichment, batch 11/40 ok"
+    }
+  ],
+  "hermes": {
+    "online": true,
+    "model": "hermes3:8b",
+    "modelSizeGB": 4.3,
+    "modelLoaded": true,
+    "queueDepth": 0,
+    "classifierBusy": false,
+    "url": "127.0.0.1:11434",
+    "last24h": {
+      "total": 9,
+      "byType": { "classify": 5, "chat": 1, "idea-chat": 3 },
+      "bySev": { "high": 1, "medium": 3, "low": 5 },
+      "ok": 8,
+      "fail": 1,
+      "avgLatencyMs": 6418
+    },
+    "recent": [
+      { "ts": 1778530200000, "type": "classify",  "target": "contact-mailer-ideas-viewer", "ok": true, "latencyMs": 7104, "severity": "medium" },
+      { "ts": 1778530000000, "type": "idea-chat", "target": "DNS-failure watcher integration",  "ok": true, "latencyMs": 8240 },
+      { "ts": 1778529800000, "type": "chat",      "target": "big-red-tunnel",     "ok": true, "latencyMs": 5301, "refined": true },
+      { "ts": 1778529500000, "type": "classify",  "target": "big-red-tunnel",     "ok": true, "latencyMs": 4146, "severity": "medium" },
+      { "ts": 1778529300000, "type": "classify",  "target": "microsite-hawk",     "ok": true, "latencyMs": 6300, "severity": "low" },
+      { "ts": 1778529000000, "type": "classify",  "target": "shopify-bulk-importer", "ok": true, "latencyMs": 8800, "severity": "high" },
+      { "ts": 1778528000000, "type": "idea-chat", "target": "per-day failure-rate trend", "ok": true, "latencyMs": 7929 },
+      { "ts": 1778527500000, "type": "idea-chat", "target": "hello world script", "ok": true, "latencyMs": 6038 },
+      { "ts": 1778527000000, "type": "classify",  "target": "queue-test-3",       "ok": false, "latencyMs": 15000, "error": "timeout" }
+    ]
+  }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..e4fa3cf
--- /dev/null
+++ b/server.js
@@ -0,0 +1,29 @@
+// CNCP Failures Mockups — 6-variant web viewer.
+// Serves /public statically + a /data endpoint returning sample-data.json
+// so every mockup can render against the same realistic seed.
+
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+
+const PORT = parseInt(process.env.PORT, 10) || 9771;
+const BIND = process.env.BIND || '127.0.0.1';
+
+const app = express();
+app.use(express.static(path.join(__dirname, 'public')));
+
+app.get('/data', (req, res) => {
+  try {
+    const raw = fs.readFileSync(path.join(__dirname, 'sample-data.json'), 'utf-8');
+    res.set('Cache-Control', 'no-store');
+    res.type('application/json').send(raw);
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/health', (_, res) => res.json({ ok: true, variants: 6 }));
+
+app.listen(PORT, BIND, () => {
+  console.log(`cncp-failures-mockups listening at http://${BIND}:${PORT}/`);
+});

(oldest)  ·  back to Cncp Failures Mockups  ·  live-data toggle: /data proxies real CNCP failures + hermes 5d00c00 →