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... -
-
-
-
-
-
-
+ +
+
+ + + docker-node-app + +
+ +
+
+ + <% if (hostname) { %> +

<%= hostname %>

+ <% } %> +
+
+ +
HealthyGET /healthz ยท 200 OK
+
+
+ +
+
+
+ <% if (ip) { %>
Client address
<%= ip %>
<% } %> + <% if (user_agent) { %>
User agent
<%= user_agent %>
<% } %> + <% if (timestamp) { %>
Rendered at
<%= timestamp %>
<% } %> +
+
+ + +
+