diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f6bbe80
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,32 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+
+This is a Kubernetes HPA validation app: sustained request traffic should scale its deployment, and the UI must clearly identify the pod handling each request. The Express application lives in `app.js`; `index.js` starts the server and owns graceful shutdown. The auto-refreshing hostname display is in `views/index.ejs`; static assets are in `public/`; tests are in `__tests__/`. Container configuration is in `Dockerfile` and `docker-compose.yaml`; the Helm chart is under `chart/`.
+
+## Build, Test, and Development Commands
+
+Use Node 26 and npm 11 (see `package.json`). Install the locked dependency set with `npm ci`.
+
+- `npm run dev` starts the app with file watching and the Node inspector on port 9229.
+- `npm start` runs the production-style server locally.
+- `npm run lint` checks all JavaScript with ESLint; `npm run lint:fix` applies safe fixes.
+- `npm test` runs the built-in Node test suite; `npm run test:watch` reruns it during development.
+- `docker compose up --build` starts the development image with source mounts.
+- `helm lint chart` validates the bundled Helm chart.
+
+## Coding Style & Naming Conventions
+
+Use CommonJS (`require`/`module.exports`), two-space indentation, semicolons, and single quotes, matching the existing source. Keep route handlers and middleware in `app.js`; expose startup or lifecycle helpers from `index.js`. Use descriptive camelCase JavaScript names and lowercase, hyphenated Kubernetes/Helm resource names. Run ESLint before committing rather than hand-formatting around its rules.
+
+## Testing Guidelines
+
+Write tests with `node:test`, `node:assert/strict`, and Supertest where HTTP behavior is involved. Place files in `__tests__/` with a `.test.js` suffix, and name tests as observable outcomes (for example, `Health check should return app status`). Preserve coverage for pod hostname headers/UI output, health checks, and shutdown behavior. Run `npm run lint && npm test` before opening a pull request.
+
+## Commit & Pull Request Guidelines
+
+Recent history uses short, imperative subjects such as `Upgrade runtime to Node 26`, `Fix Helm chart non-root runtime`, and `Harden container runtime and CI validation`; keep that style. Keep changes focused, explain the behavioral or deployment impact in the PR description, and include screenshots for UI-visible changes. Do not commit generated dependencies, credentials, or local environment files.
+
+## Security & Configuration
+
+Treat `package-lock.json` as authoritative and use `npm ci`; do not bypass engine checks. Preserve the non-root production image and `tini` signal handling. HPA is disabled by default; test scaling with an explicit Helm values override and keep its CPU request/target settings meaningful.
diff --git a/public/css/index.css b/public/css/index.css
index e2fc40a..49a2a0a 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -1,90 +1,253 @@
-#body {
- background-color: #DEF1F9;
- font-family: 'Lato', sans-serif;
+:root {
+ --ink: #f1f8fc;
+ --muted: #9eb2c1;
+ --line: rgba(221, 240, 250, 0.16);
+ --blue: #2496ed;
+ --navy: #071b2a;
}
-#container {
- max-width: 500px;
- margin: auto;
- padding-top: 20px;
+* {
+ box-sizing: border-box;
}
-#logo {
- display:block;
- margin:auto;
- padding-bottom: 5px;
+html {
+ background: var(--navy);
}
-#content {
- display:block;
- margin:auto;
- text-align: center;
+body {
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+ color: var(--ink);
+ background: var(--navy);
+ font-family: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
-table {
- border-collapse: inherit;
- border-spacing: 0 28px;
- text-align: center;
+.shell {
+ width: min(100%, 1120px);
+ min-height: 100vh;
+ margin: 0 auto;
+ padding: 0 48px;
}
-span {
- font-size: 17px;
+.topbar,
+.service-status,
+.request-details {
+ border-bottom: 1px solid var(--line);
}
-sub {
- vertical-align: sub;
- font-size: 12px;
- font-weight: 900;
- display: inline-block;
- padding-top: 5px;
+.topbar {
+ display: flex;
+ min-height: 76px;
+ align-items: center;
+ justify-content: space-between;
}
-.spinner {
- margin: 2px auto;
- width: 20px;
- height: 20px;
- position: relative;
+.brand,
+.repo-link {
+ display: inline-flex;
+ align-items: center;
+ color: var(--ink);
+ text-decoration: none;
}
-.cube1, .cube2 {
- background-color: #333;
- width: 7px;
- height: 7px;
- position: absolute;
- top: 0;
- left: 0;
+.brand {
+ font-size: 14px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.brand-mark {
+ display: grid;
+ width: 27px;
+ height: 27px;
+ place-items: center;
+ margin-right: 10px;
+ overflow: hidden;
+ border-radius: 7px;
+ background: #fff;
+}
+
+.brand-mark img {
+ width: 20px;
+ height: 20px;
+}
+
+.repo-link {
+ gap: 8px;
+ color: var(--muted);
+ font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 12px;
+}
+
+.repo-link:hover {
+ color: var(--blue);
+}
+
+.status-dot,
+.pulse {
+ display: inline-block;
+ border-radius: 50%;
+ background: var(--blue);
+ box-shadow: 0 0 0 0 rgba(36, 150, 237, 0.5);
+ animation: ping 2s infinite;
+}
+
+.status-dot {
+ width: 7px;
+ height: 7px;
+}
+
+.service-status {
+ display: flex;
+ min-height: 220px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 40px;
+ padding: 50px 0;
+}
+
+.section-label {
+ margin: 0;
+ color: var(--blue);
+ font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+h1,
+p {
+ margin-top: 0;
+}
+
+h1 {
+ margin: 10px 0;
+ font-size: clamp(2rem, 4.5vw, 3.6rem);
+ font-weight: 700;
+ letter-spacing: -0.065em;
+ line-height: 1;
+}
+
+.health-state {
+ display: flex;
+ min-width: 185px;
+ align-items: center;
+ gap: 11px;
+ padding: 18px 0 18px 18px;
+ border-left: 1px solid var(--line);
+}
+
+.pulse {
+ width: 10px;
+ height: 10px;
+ background: #32b96a;
+ box-shadow: 0 0 0 0 rgba(50, 185, 106, 0.5);
+ animation-name: health-ping;
+}
+
+.health-state strong,
+.health-state span {
+ display: block;
+}
- -webkit-animation: sk-cubemove 1.8s infinite ease-in-out;
- animation: sk-cubemove 1.8s infinite ease-in-out;
+.health-state strong {
+ font-size: 14px;
}
-.cube2 {
- -webkit-animation-delay: -0.9s;
- animation-delay: -0.9s;
+.health-state span {
+ margin-top: 4px;
+ color: var(--muted);
+ font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 10px;
}
-@-webkit-keyframes sk-cubemove {
- 25% { -webkit-transform: translateX(21px) rotate(-90deg) scale(0.5) }
- 50% { -webkit-transform: translateX(21px) translateY(21px) rotate(-180deg) }
- 75% { -webkit-transform: translateX(0px) translateY(21px) rotate(-270deg) scale(0.5) }
- 100% { -webkit-transform: rotate(-360deg) }
+.request-details {
+ display: grid;
+ grid-template-columns: 210px 1fr;
+ gap: 55px;
+ padding: 34px 0;
}
-@keyframes sk-cubemove {
- 25% {
- transform: translateX(21px) rotate(-90deg) scale(0.5);
- -webkit-transform: translateX(21px) rotate(-90deg) scale(0.5);
- } 50% {
- transform: translateX(21px) translateY(21px) rotate(-179deg);
- -webkit-transform: translateX(21px) translateY(21px) rotate(-179deg);
- } 50.1% {
- transform: translateX(21px) translateY(21px) rotate(-180deg);
- -webkit-transform: translateX(21px) translateY(21px) rotate(-180deg);
- } 75% {
- transform: translateX(0px) translateY(21px) rotate(-270deg) scale(0.5);
- -webkit-transform: translateX(0px) translateY(21px) rotate(-270deg) scale(0.5);
- } 100% {
- transform: rotate(-360deg);
- -webkit-transform: rotate(-360deg);
- }
-}
\ No newline at end of file
+dl {
+ margin: -10px 0;
+}
+
+.detail-row {
+ display: grid;
+ grid-template-columns: 145px 1fr;
+ gap: 22px;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--line);
+}
+
+.detail-row:last-child {
+ border-bottom: 0;
+}
+
+dt,
+dd,
+footer {
+ font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 12px;
+}
+
+dt {
+ color: var(--muted);
+}
+
+dd {
+ min-width: 0;
+ margin: 0;
+ overflow-wrap: anywhere;
+ line-height: 1.45;
+}
+
+footer {
+ padding: 22px 0 28px;
+ color: var(--muted);
+}
+
+#refresh-countdown {
+ display: inline-block;
+ min-width: 1ch;
+ text-align: right;
+}
+
+.refresh-indicator {
+ display: inline-block;
+ width: 13px;
+ height: 13px;
+ margin: 0 8px -3px 0;
+ border: 1px solid var(--blue);
+ border-left-color: transparent;
+ border-radius: 50%;
+ animation: turn 1.8s linear infinite;
+}
+
+@keyframes turn {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes ping {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(36, 150, 237, 0.45); }
+ 45% { box-shadow: 0 0 0 7px rgba(36, 150, 237, 0); }
+}
+
+@keyframes health-ping {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(50, 185, 106, 0.45); }
+ 45% { box-shadow: 0 0 0 7px rgba(50, 185, 106, 0); }
+}
+
+@media (max-width: 680px) {
+ .shell { padding: 0 22px; }
+ .topbar { min-height: 70px; }
+ .service-status { min-height: 0; align-items: flex-start; flex-direction: column; gap: 28px; padding: 42px 0; }
+ .health-state { padding: 0; border-left: 0; }
+ .request-details { grid-template-columns: 1fr; gap: 30px; padding: 32px 0; }
+ .detail-row { grid-template-columns: 1fr; gap: 7px; padding: 13px 0; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
+}
diff --git a/views/index.ejs b/views/index.ejs
index 4ccca20..5f4ecf9 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -1,77 +1,67 @@
-
+
-
-
+
+
+
+
+
+
+
+
-
docker-node-app
-
-
-
-
-
-
-
-
- |
- <% if (hostname) { %>
- Hello from <%= hostname %>!
- <% } %>
- |
-
-
-
- <% if (ip) { %>
- <%= ip %>
-
- Request IP
- <% } %>
- |
-
-
-
- <% if (user_agent) { %>
- <%= user_agent %>
-
- User Agent
- <% } %>
- |
-
-
-
- <% if (timestamp) { %>
- <%= timestamp %>
-
- Timestamp
- <% } %>
- |
-
-
- |
- This page will automatically reload and connect to new
- hosts...
- |
-
-
-
-
-
+
+
+
+
+
+
+
Hostname
+ <% if (hostname) { %>
+
<%= hostname %>
+ <% } %>
+
+
+
+
HealthyGET /healthz ยท 200 OK
+
+
+
+
+
+
+ <% if (ip) { %>- Client address
- <%= ip %>
<% } %>
+ <% if (user_agent) { %>- User agent
- <%= user_agent %>
<% } %>
+ <% if (timestamp) { %>- Rendered at
- <%= timestamp %>
<% } %>
+
+
+
+
+
+