Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
297 changes: 230 additions & 67 deletions public/css/index.css
Original file line number Diff line number Diff line change
@@ -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);
}
}
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; }
}
Loading