← Back to Punch John Eric
Table of Contents

Frontend — Core Interaction

12 features

Features that define the primary user-facing experience: viewing the counter, incrementing it, and receiving visual feedback.

F1

Counter Display

3 requirements

When a user visits the application, the current punch count is prominently displayed in the center of the page. The counter element must be immediately visible without scrolling, must render a numeric value (not a loading spinner or placeholder), and the displayed number must faithfully reflect the value returned by the server's API. This is the first thing users see and serves as the application's primary piece of state.

Requirements

F1.R1 Counter element is visible on page load

The DOM element containing the punch count (identified by #counter-value) must be present and visible in the viewport as soon as the page finishes its initial render. It must not be hidden, collapsed, or obscured by other elements.

F1.R2 Counter shows a numeric value

The counter must display a number — not an empty string, a placeholder like '...', or an error state. On a healthy page load, the user should immediately see a meaningful integer value representing the total number of punches recorded.

F1.R3 Displayed value matches the API response

The number shown in the counter must exactly match the count value returned by GET /api/counter. There must be no discrepancy caused by stale caches, race conditions, or rendering bugs. If the API returns { count: 42 }, the page must display '42'.

F2

Counter Increment

3 requirements

Users increment the shared punch counter by clicking the boxing glove button. Each click triggers a network request to the server, which atomically increments the stored count and returns the new value. The frontend then updates the display to reflect the change. This is the core interaction loop of the entire application.

Requirements

F2.R1 Click sends POST /api/increment

When the user clicks the boxing glove button, the frontend must issue an HTTP POST request to the /api/increment endpoint. The request should be sent immediately upon click, without debouncing or batching. This is the mechanism by which the client communicates the user's intent to punch.

F2.R2 Display updates to new count after click

After the POST /api/increment request completes successfully, the counter display must update to show the new count value returned in the response body. The user should see the number visually change, confirming that their punch was registered.

F2.R3 Multiple sequential clicks increment correctly

If a user clicks the glove multiple times in sequence (waiting for each request to complete), the counter must increment by exactly one for each click. Three clicks must result in a count that is exactly three higher than the starting value, with no missed or duplicate increments.

F4

Multi-User Synchronization

2 requirements

Because the punch counter is shared across all users, the application must keep each user's display in sync with the server's authoritative count. This is accomplished through periodic polling — the frontend regularly fetches the latest count from the API and updates the display if it has changed. This ensures that if User A punches, User B sees the updated count within a few seconds without needing to refresh the page.

Requirements

F4.R1 Polls GET /api/counter every 5 seconds

The frontend must set up a repeating interval that calls GET /api/counter approximately every 5,000 milliseconds. This polling mechanism runs continuously in the background as long as the page is open, ensuring the client stays reasonably up-to-date with the server's state.

F4.R2 Display updates when server value changes externally

When the polling request returns a count value that differs from what is currently displayed, the frontend must update the counter display to reflect the new value. This handles the case where another user (or another tab) has incremented the counter — the current user sees the change appear automatically.

F5

Double-Click Protection

2 requirements

To prevent accidental double-counting, the application implements a client-side lock that blocks concurrent increment requests. When a punch is in flight (the POST request has been sent but the response has not yet arrived), additional clicks are ignored. This ensures that even rapid or accidental double-clicks result in exactly one increment, not two.

Requirements

F5.R1 isUpdating lock prevents concurrent submits

The frontend maintains a boolean flag (isUpdating) that is set to true when a POST /api/increment request begins and reset to false when the response is received. While this flag is true, any subsequent click on the punch button must be silently ignored — no additional network request should be sent.

F5.R2 Rapid clicking results in only one increment

If a user clicks the boxing glove multiple times in rapid succession (faster than the server can respond), the counter must increment by exactly one. The double-click protection mechanism must reliably prevent all concurrent duplicate requests.

F6

Number Formatting

1 requirement

Large punch counts are formatted with locale-appropriate thousands separators to improve readability. Instead of displaying '1000000', the counter shows '1,000,000' (or the equivalent separator for the user's locale). This is a small but important usability detail that makes large numbers scannable at a glance.

Requirements

F6.R1 Uses toLocaleString for thousands separators

The frontend must format the counter value using JavaScript's Number.prototype.toLocaleString() method before rendering it to the DOM. This ensures that numbers above 999 display with appropriate grouping separators (e.g., commas in en-US) based on the user's browser locale.

F27

Shield

7 requirements

Users can add shields to protect against punches. Each shield absorbs one incoming punch — when someone punches while shields are active, a shield is consumed instead of incrementing the counter. The shield button displays a blue shield icon and triggers a pulse animation on click. A shield counter is displayed showing the current number of active shields.

Requirements

F27.R1 Shield button visible on page

The application must display a shield button alongside the punch button. The button must contain an SVG shield icon, have an aria-label of 'Shield button', and be visually distinct (blue theme) from the punch and heal buttons.

F27.R2 Click sends POST /api/shield

Clicking the shield button must send a POST request to /api/shield. The server must respond with 200 OK and a JSON body containing both count and shields fields. The shields value must be incremented by 1.

F27.R3 Shield count displayed

The page must display the current shield count in a dedicated shield display area. The display must show a shield icon and the numeric shield count. When shields are active (greater than 0), the display must visually indicate this with distinct styling.

F27.R4 Shield animation on click

When the shield button is clicked, it must apply a '.shielding' CSS class that triggers a pulse animation (scale up then back to normal over 300ms). The class must be removed after the animation completes.

F27.R5 Punch with active shields consumes a shield instead

When a user punches while shields are active (shields > 0), the punch must consume one shield (shields decremented by 1) instead of incrementing the counter. The counter value must remain unchanged. This is the core defensive mechanic.

F27.R6 Shields decay by 1 every 10 seconds (server-side)

Shields are not permanent. The server tracks a shieldTimestamp and on every read, calculates how many 10-second intervals have elapsed since the last update. For each interval, one shield is removed. This decay happens server-side so shields expire even when no one is on the page. Partial intervals are preserved — if 15 seconds have passed, only 1 shield decays (not 2).

F27.R7 Shield maximum is 10

The total number of active shields is capped at 10. When a user clicks shield and the count is already at 10, it remains at 10. The server enforces this limit via Math.min(10, shields + 1).

F28

Heal

6 requirements

Users can heal to decrement the punch counter. Healing reduces the counter by 1 with a minimum of 0. The heal button displays a green cross icon and triggers a glow animation on click. Healing has a 3-second cooldown per user, tracked in the browser via localStorage.

Requirements

F28.R1 Heal button visible on page

The application must display a heal button alongside the punch and shield buttons. The button must contain an SVG cross icon, have an aria-label of 'Heal button', and be visually distinct (green theme) from the other buttons.

F28.R2 Click sends POST /api/heal

Clicking the heal button must send a POST request to /api/heal. The server must respond with 200 OK and a JSON body containing both count and shields fields. The count value must be decremented by 1.

F28.R3 Heal decrements counter (minimum 0)

The heal action must decrement the counter by 1. If the counter is already at 0, it must remain at 0 — the counter must never go negative. The server enforces this minimum.

F28.R4 Heal animation on click

When the heal button is clicked, it must apply a '.healing' CSS class that triggers a glow animation (scale up with green glow then back to normal over 300ms). The class must be removed after the animation completes.

F28.R5 Heal has 3-second cooldown per user (localStorage)

Each user can only heal once every 3 seconds. The cooldown is tracked client-side via localStorage (key: lastHealTime). If a user attempts to heal while on cooldown, a toast message is shown indicating the remaining time. This prevents spam-healing and adds strategic depth.

F28.R6 Cooldown timer displayed near heal button

A small countdown timer is displayed near the heal button showing the remaining cooldown time in seconds (e.g., '7s'). The timer updates in real-time and disappears when the cooldown expires. The timer persists across page reloads since it reads from localStorage.

F29

Current Users Display

5 requirements

The application displays the number of currently active users at the top of the page. Active users are tracked server-side by IP address — any client that has made a request within the last 15 seconds is considered active. The count updates on every poll cycle (every 5 seconds).

Requirements

F29.R1 Current user count displayed on page

The page must display a 'Current Users:' label with a numeric count of active users. The element must be visible at the top of the container, above the title. The count is sourced from the server's GET /api/counter response.

F29.R2 Count updates on poll

The user count must update every time the frontend polls GET /api/counter (every 5 seconds). The displayed value reflects the server's current active user count at each poll interval.

F29.R3 Server tracks users by IP with 15s window

The server maintains an in-memory Map of IP addresses to last-seen timestamps. On each GET /api/counter request, the requesting IP is recorded. The active user count is computed by counting entries with a last-seen time within the past 15 seconds. Stale entries are cleaned up on each count request.

F29.R4 GET /api/users returns users grouped by location

The server exposes a GET /api/users endpoint that returns active users grouped by geolocation. Each entry contains a location string (city, country code) and a count. The response shape is { users: [{ location, count }], total }. IP addresses are never exposed in the response. Users from unresolvable IPs (localhost, private ranges) are grouped under 'Unknown'.

F29.R5 User modal displays location breakdown

Clicking the user count in the footer opens a modal that fetches GET /api/users and displays a list of locations with user counts. Each location is shown as a styled row with the location name and a badge showing the count. A total is displayed at the bottom. If there are no active users, it shows 'No active users'. If the fetch fails, it shows an error message.

F40

Punch Specific Body Parts

9 requirements

Instead of punching a generic boxing glove, users can now target specific body parts on a character figure ('John Eric'). The boxing glove is replaced by an SVG stick figure with 6 clickable zones: head, torso, left shoulder, right shoulder, legs, and keister. Each punch still increments the unified counter, but the server also tracks which body part was hit. Visual feedback includes a zone-specific hit animation, a floating '+1' indicator, and hover tooltips showing per-part hit counts.

Requirements

F40.R1 Character figure with 6 clickable body-part zones visible on page

The main punch target is an SVG character figure of John Eric with 6 distinct clickable zones: head, torso, leftShoulder, rightShoulder, legs, and keister. Each zone is a <g> element with data-part attribute, role='button', aria-label, and tabindex for keyboard accessibility. The character replaces the boxing glove button.

F40.R2 Clicking a body part sends POST /api/increment with bodyPart field

When a user clicks a body-part zone, the frontend sends POST /api/increment with { bodyPart: 'head' | 'torso' | 'leftShoulder' | 'rightShoulder' | 'legs' | 'keister' } in the request body. If no bodyPart is provided (backward compatibility), the server defaults to 'torso'. Invalid bodyPart values also default to 'torso'.

F40.R3 Server tracks bodyPartHits in counter.json alongside unified counter

The counter.json file includes a bodyPartHits object: { head, torso, leftShoulder, rightShoulder, legs, keister } with integer hit counts. When a punch lands (shields are zero), both the unified counter and the specific body part counter are incremented. The bodyPartHits field defaults to all zeros for new or legacy counter files.

F40.R4 Shields absorb body-part punches without recording a body part hit

When shields are active (shields > 0), a punch decrements the shield count but does NOT increment the unified counter or the bodyPartHits for any body part. This is consistent with the existing shield mechanic where shields absorb damage.

F40.R5 Body part hit animation plays on the specific zone when punched

When a body part is punched, that specific SVG zone receives a 'hit' CSS class that triggers a shake+flash animation (300ms). A floating '+1' indicator appears near the hit zone and fades upward. The animation is removed after completion so it can replay on subsequent hits.

F40.R6 Body part hit counts shown on hover (stats tooltip)

Hovering over a body-part zone displays a tooltip showing the part name and its total hit count (e.g., 'Head: 47 hits'). The tooltip appears above the character figure and disappears on mouse leave.

F40.R7 Character figure is responsive on mobile viewports

The character SVG scales down on mobile viewports (120px wide on screens <= 600px vs 180px on desktop). All body-part zones remain large enough to tap accurately on touch devices.

F40.R8 WebSocket broadcasts include bodyPartHits data

When state is broadcast via WebSocket (after punches, shields, heals, admin actions), the message includes the bodyPartHits object so all connected clients can display up-to-date per-part statistics.

F40.R9 GET /api/counter includes bodyPartHits in response

The GET /api/counter endpoint response includes bodyPartHits alongside count, shields, users, and disabledActions. The bodyPartHits object contains exactly 6 keys with non-negative integer values.

F49

Punch Reasons Sidebar

6 requirements

A sidebar on the main page displays selectable reasons for punching John Eric. Five predefined reasons are provided: 'Mad at JE', 'Annoyed at the world', 'Funny', 'Thinking of JE', and 'Hungry'. Users can also add custom reasons via a form in the sidebar. All reasons are stored server-side so they persist across sessions and are shared with all users. The currently selected reason is visually highlighted.

Requirements

F49.R1 Sidebar with predefined punch reasons visible on main page

The main page must display a sidebar panel (id='punch-reasons-sidebar') containing a list of selectable punch reasons. The five predefined reasons are: 'Mad at JE', 'Annoyed at the world', 'Funny', 'Thinking of JE', and 'Hungry'. Each reason is displayed as a clickable item with class 'reason-item'.

F49.R2 GET /api/reasons returns all reasons

The server exposes GET /api/reasons which returns an array of all reasons (predefined defaults + custom user-added). Each entry has { id, text, isDefault } fields. Default reasons always appear first, custom reasons follow. Returns 200 with application/json.

F49.R3 POST /api/reasons adds a new custom reason

POST /api/reasons accepts a JSON body with a 'text' field and creates a new custom reason. The new reason is assigned a unique id, stored with isDefault=false, and persisted to reasons.json. Returns 201 with the created reason object. Rejects empty or missing text with 400.

F49.R4 Reasons persist across sessions for all users

Custom reasons are stored server-side in reasons.json using atomic writes (tmp + rename pattern). When the page loads, reasons are fetched from GET /api/reasons and displayed. All users see the same set of reasons including any custom reasons added by other users.

F49.R5 Sidebar has form to add custom reasons

The sidebar includes an input field (id='new-reason-input') and a submit button (id='add-reason-submit') for adding custom reasons. After successful submission, the new reason appears in the sidebar list and the input is cleared.

F49.R6 Selected reason is visually highlighted

When a user clicks a reason item, it receives the 'selected' CSS class and all other reason items lose it. The selected reason remains highlighted until another reason is selected or the page is reloaded.

F50

JohnEric Character Image

6 requirements

The SVG character figure can be replaced with an actual uploaded image of JohnEric. A dedicated non-admin user account ('johneric') allows JE to log in to a panel at /je/ and upload a photo of himself in any attire or pose. When an image is uploaded, all visitors see the real photo instead of the SVG character. The SVG remains as a fallback when no image has been uploaded. Body-part click zones overlay the image so punching still works.

Requirements

F50.R1 JohnEric user account with login/logout (non-admin)

A separate user account exists for JohnEric with credentials configured via JE_USER (default: 'johneric') and JE_PASSWORD or JE_PASSWORD_HASH environment variables (no default — must be set). POST /api/je/login validates credentials and sets an httpOnly je_token cookie. POST /api/je/logout clears the session. GET /api/je/status returns { authenticated: bool }. Sessions use the same 24-hour expiration as admin sessions but are tracked in a separate session map.

F50.R2 POST /api/je/upload-image accepts and saves image data

Authenticated JE user can upload an image via POST /api/je/upload-image with a JSON body containing { image: 'data:image/...;base64,...' }. The server validates the data URL format, checks the image is under 5MB, strips the base64 prefix, and saves the raw bytes to a file using atomic writes (tmp + rename). Supported formats: PNG, JPEG, GIF, WebP. Returns { success: true, ext, size, uploadedAt } on success.

F50.R3 GET /api/je/image returns the uploaded image or 404

GET /api/je/image serves the uploaded JE image with the correct Content-Type header (image/png, image/jpeg, etc.). If no image has been uploaded, returns 404 with { error: 'No image uploaded' }. The image metadata (extension, size, upload timestamp) is stored in a separate .meta JSON file.

F50.R4 Main page displays uploaded JE image with SVG fallback

On page load, the frontend fetches GET /api/je/image. If the image exists, it is displayed in an <img> element overlaying the character area, and the SVG character is hidden. If no image exists (404), the SVG character remains visible. The image is styled to match the SVG dimensions (180px desktop, 120px mobile) and the body-part click zones overlay the image.

F50.R5 JohnEric panel page at /je/ with login and image upload

A dedicated panel page at /je/ (served from public/je/index.html) provides a login form for the JE account. After authentication, the page shows the current uploaded image (if any), a file input for selecting a new image, a preview of the selected image, and an upload button. Successful uploads refresh the displayed image.

F50.R6 Uploaded image persists across server restarts

The uploaded image is stored as a file on disk (configurable via JE_IMAGE_FILE env var, default: je-image.dat in the project root). The image metadata (.meta file) stores the file extension, byte size, and upload timestamp. Both files survive server restarts and redeployments.

F51

Punch Reason Counts

5 requirements

Tracks how many times each punch reason is selected when punching. When a user punches with a reason selected, the server increments a count for that reason. A toggle link at the top of the reasons section reveals a summary of reason counts. The reasons list area is dynamically scrollable with a max-height so it does not overflow the page when many reasons exist.

Requirements

F51.R1 POST /api/increment tracks selected reason count

When POST /api/increment is called with an optional 'reason' field (the reason id string), the server increments a counter for that reason in reason-counts.json. If no reason is provided, no reason count is tracked. The reason field is validated against known reason ids.

F51.R2 GET /api/reason-counts returns all reason counts

GET /api/reason-counts returns a JSON object mapping reason ids to their punch counts. Returns 200 with application/json. If no counts exist, returns an empty object {}.

F51.R3 Reason counts persist in reason-counts.json via atomic writes

Reason counts are stored in reason-counts.json using the same atomic write pattern (tmp + rename) as other data files. The file is read on each increment and written back atomically. Missing or corrupt files default to an empty object.

F51.R4 Toggle link at top of reasons section shows/hides counts

A clickable link (id='reason-counts-toggle') appears at the top of the punch reasons sidebar, after the title. Clicking it toggles the visibility of a reason counts summary panel (id='reason-counts-panel'). The panel displays each reason and its count, sorted by count descending. The panel is hidden by default.

F51.R5 Reasons list area is dynamically scrollable with max-height

The reasons list (ul.reasons-list) has a CSS max-height and overflow-y: auto, making it scrollable when the list of reasons exceeds the visible area. This prevents the sidebar from growing unbounded as custom reasons are added.

Frontend — Visual Design & Animation

8 features

Features that define the visual identity, animations, and interactive feedback that make the punching experience satisfying and delightful.

F7

Boxing Glove SVG

2 requirements

The punch button is not a plain HTML button — it is a detailed, hand-crafted SVG illustration of a boxing glove. The glove serves as both the visual centerpiece and the interactive target. It features gradient fills, stitching details, knuckle lines, a thumb, and a wrist cuff, creating a realistic and inviting click target.

Requirements

F7.R1 SVG element is visible with nonzero dimensions

The boxing glove SVG must be rendered in the viewport with a width and height greater than zero. It must not be hidden by CSS, clipped to zero size, or otherwise invisible to the user. The glove is the primary call-to-action, so its visibility is critical to the application's usability.

F7.R2 SVG has role="img" and aria-label

The SVG element must include role='img' and an aria-label attribute (e.g., 'Boxing glove') so that screen readers can identify it as a meaningful image rather than ignoring it or reading out its internal SVG markup. This is essential for accessibility compliance.

F8

Punch Animation

2 requirements

When the user clicks the boxing glove, a satisfying 'punch' animation plays. The glove visually lunges forward (scales and translates) to simulate a punching motion, then returns to its resting position. This animation provides immediate tactile feedback that the user's click was registered, even before the network request completes.

Requirements

F8.R1 .punching class added on click

When the user clicks the punch button, the frontend must immediately add the CSS class 'punching' to the button element. This class triggers a CSS animation (or transition) that visually moves the glove forward. The class must be added synchronously in the click handler, before any asynchronous operations begin.

F8.R2 .punching class removed after approximately 300ms

After the punch animation has played (approximately 300 milliseconds), the 'punching' class must be removed from the button element. This resets the glove to its resting position and allows subsequent clicks to trigger the animation again. The timing should match the CSS animation duration.

F9

Counter Bump Animation

1 requirement

When the counter value changes after a successful increment, a subtle 'bump' animation plays on the counter display. The number briefly scales up and then returns to normal size, drawing the user's eye to the updated value and reinforcing that the punch was counted.

Requirements

F9.R1 .bump class fires on counter after increment

After the POST /api/increment response is received and the counter display is updated with the new value, the frontend must add the 'bump' CSS class to the counter element. This class triggers a scale animation. The class should be removed after the animation completes so it can be re-triggered on the next increment.

F41

Larger Punch Count Font

1 requirement

Increases the font size of the punch counter display to make it more prominent and easier to read. The counter number is styled with a larger font-size value to improve visibility and visual hierarchy on the page.

Requirements

F41.R1 Counter displays with increased font size

The punch count number must be rendered with a font size larger than the current implementation, making it more prominent and easier to read for users across different devices and screen sizes.

F42

Improved Leg Styling

3 requirements

Visual improvements to the character's leg rendering to make them appear more proportional and aesthetically pleasing. This includes adjusting the SVG path geometry, stroke width, styling, and positioning to create a better visual balance with other body parts.

Requirements

F42.R1 Legs have improved proportions and styling

The legs body part SVG should be visually improved with better proportions, appropriate stroke width, and styling that matches the overall character design aesthetic. The geometry should look natural and proportional to the rest of the body.

F42.R2 Legs maintain clickable hit area

Despite visual changes, the legs body part must remain fully functional as a clickable target for the 'Punch Specific Body Parts' feature (F40), maintaining appropriate hit detection area.

F42.R3 Legs styling is consistent across viewports

The improved leg styling should render consistently across different viewport sizes and devices, maintaining visual quality in responsive layouts.

F43

Abs Visual Detail

1 requirement

Add subtle abdominal muscle definition lines to the character's torso, visible through the shirt as faint styling details that enhance the character design.

Requirements

F43.R1 Abs lines visible on torso

The torso body part displays subtle abs definition lines rendered as SVG paths with low opacity, giving the impression of muscle definition under the shirt.

F44

Festive Party Hat

1 requirement

A large, colorful party hat on the character's head with multiple stripes, a decorated pom-pom, confetti dots, and a star on top for maximum festive flair.

Requirements

F44.R1 Party hat visible on character head

A large triangular party hat with three colored stripes, a decorated pom-pom, scattered confetti dots, and a star at the tip is rendered above the character's hair on the head body part.

F48

Human-like Character Proportions

3 requirements

Redesign the character doll SVG to use smoother, more organic shapes instead of blocky rectangles and simple geometric primitives. The torso, arms, and legs use curved SVG paths that create a more natural, human-like silhouette while maintaining all existing body-part clickable zones and interactive features.

Requirements

F48.R1 Character uses curved organic body shapes

The character SVG uses smooth curved paths and organic shapes for the torso, arms, and legs instead of sharp-cornered rectangles. The silhouette should appear natural and human-like rather than blocky or robotic.

F48.R2 All 6 body part hit zones remain functional

All 6 clickable body-part zones (head, torso, leftShoulder, rightShoulder, legs, keister) remain present with their data-part attributes and continue to function correctly for punching.

F48.R3 Character proportions are anatomically balanced

The character's body proportions are anatomically reasonable — the head, torso, arms, and legs are sized and positioned relative to each other in a way that looks like a natural human figure rather than disjointed geometric shapes.

Frontend — Error Handling & Resilience

2 features

Features that ensure the application degrades gracefully when things go wrong — network failures, server errors, or unexpected conditions.

F10

Toast Notifications

4 requirements

When an error occurs (such as a failed network request during a punch attempt), the application displays a non-blocking toast notification at the bottom of the screen. The toast provides a brief, human-readable description of the error, then automatically disappears after a few seconds. This keeps the user informed without disrupting the overall experience or requiring manual dismissal.

Requirements

F10.R1 Toast becomes visible on error

When a network request fails or the server returns an error response, the toast container (#toast) must become visible to the user. It must transition from a hidden or off-screen state to a visible position, typically at the bottom center of the viewport.

F10.R2 Toast shows a descriptive error message

The toast must display a meaningful, human-readable error message — not a raw HTTP status code or a generic 'Something went wrong'. The message should give the user enough context to understand what happened (e.g., 'Could not connect to the server').

F10.R3 Toast auto-dismisses after 4 seconds

After appearing, the toast must automatically fade out or slide away after approximately 4,000 milliseconds. The user should not need to manually close or dismiss the notification. This prevents toast messages from accumulating on screen during repeated errors.

F10.R4 Toast has error styling class

When the toast is displaying an error message, it must have a CSS class (such as 'error' or 'show') applied that provides distinctive error styling — typically a red or orange background that visually communicates 'something went wrong' at a glance.

F11

Error Recovery UI

2 requirements

If the initial page load fails to connect to the API (for example, the server is down), the application must clearly communicate this to the user rather than displaying a broken or empty state. The counter shows the word 'Error' and an explanatory message is appended to the page, informing the user that the server is unreachable.

Requirements

F11.R1 Counter shows "Error" on load failure

If the initial GET /api/counter request fails (network error, server unreachable, or non-2xx response), the counter display element must show the text 'Error' instead of a number. This immediately signals to the user that the application is not functioning normally.

F11.R2 Error message div appended with connection failure text

In addition to showing 'Error' in the counter, the frontend must append a div with the class 'error-message' to the page. This div should contain a descriptive message such as 'Unable to connect to the server' that helps the user understand why the application is not working.

Frontend — Responsive Design & Accessibility

4 features

Features that ensure the application works well across different device sizes and is usable by people with disabilities.

F12

Responsive Design

2 requirements

The application's layout adapts to different screen sizes using CSS media queries. On desktop, the layout has more generous spacing and larger elements. On mobile devices (screens narrower than 600 pixels), the layout compresses — the container narrows, fonts scale down, and the boxing glove image shrinks to fit comfortably on a small screen without horizontal scrolling.

Requirements

F12.R1 Layout adapts at 600px breakpoint

The CSS must include a media query at max-width: 600px that adjusts the layout for mobile devices. Key changes include reducing container padding, scaling down headings, and reorganizing any flex layouts that would overflow on narrow screens. The page must remain fully usable at mobile widths.

F12.R2 Glove image is smaller on mobile

At viewport widths below the 600px breakpoint, the boxing glove SVG must render at a smaller size than it does on desktop. This prevents the glove from dominating the screen on mobile devices and ensures the overall layout remains balanced and usable.

F13

Accessibility

5 requirements

The application follows web accessibility best practices to ensure it is usable by people who rely on screen readers, keyboard navigation, or other assistive technologies. Interactive elements have appropriate ARIA attributes, focus states are clearly visible, and all functionality is available without a mouse.

Requirements

F13.R1 Punch button has aria-label

The boxing glove button element must include an aria-label attribute (e.g., 'Punch button') that provides a text description for screen readers. Without this, a screen reader user would have no way to know what the button does, since the visual boxing glove SVG is not interpretable as text.

F13.R2 SVG has role="img" and aria-label

The inline SVG element for the boxing glove must include role='img' and an aria-label attribute. This tells assistive technologies to treat the SVG as a single image with a text alternative, rather than attempting to parse and announce its internal graphical elements.

F13.R3 Toast has aria-live="polite"

The toast notification container must include the aria-live='polite' attribute. This instructs screen readers to announce the toast's content when it appears, but to wait until the user's current activity (such as reading or typing) is complete. This ensures that error messages are communicated to screen reader users without being disruptive.

F13.R4 Button has visible focus ring

When the punch button receives keyboard focus (via the Tab key), a clearly visible focus ring or outline must appear around it. This visual indicator is essential for keyboard-only users to know which element is currently focused. The focus ring should use the :focus-visible pseudo-class to avoid showing on mouse clicks.

F13.R5 Button is activatable via keyboard

The punch button must be fully functional using only the keyboard. Pressing Enter or Space while the button is focused must trigger the same increment behavior as a mouse click. Users who cannot use a mouse must have complete access to the application's core functionality.

F46

Back to Game Navigation

3 requirements

Adds a back button at the top of the features, tests, and other secondary pages to navigate back to the main game page (index.html). This improves navigation and user experience by providing a clear way to return to the core application from informational pages.

Requirements

F46.R1 Back button on features page navigates to main game

The features.html page displays a back button that, when clicked, navigates the user to index.html (the main game page)

F46.R2 Back button on test report page navigates to main game

The test-report.html page displays a back button that, when clicked, navigates the user to index.html (the main game page)

F46.R3 Back button styling is consistent and accessible

The back button has clear, accessible styling with appropriate hover states, focus indicators for keyboard navigation, and is positioned prominently at the top of the page

F47

Test Report By Release View

4 requirements

Adds a 'By Release' tab to the test report page that groups tests by release version from releases.json. Releases are displayed in reverse chronological order with the most recent release first. Also moves the back-to-home link to the top of all pages, right under the navigation tabs.

Requirements

F47.R1 By Release tab visible on test report page

The test report page displays a third tab labeled 'By Release' alongside the existing 'By Layer' and 'By Feature' tabs. Clicking the tab switches the view to show tests grouped by release version.

F47.R2 Tests grouped by release version

Each release section shows the version number, date, title, and all tests associated with features included in that release. Feature IDs are extracted from the release's features array in releases.json.

F47.R3 Releases in reverse chronological order

Releases are displayed newest first. The most recent release appears at the top of the By Release view, with older releases following in descending order.

F47.R4 Back link at top of pages under nav tabs

The back-to-home link is positioned at the top of all secondary pages (features, tests, releases, feature requests, admin), right under the main navigation tabs, instead of at the bottom before the footer.

Backend — API Endpoints

4 features

Features that define the server-side HTTP API that the frontend consumes. The API is built with Express.js and provides endpoints for reading and incrementing the counter.

F14

Homepage HTML

2 requirements

The server serves the main application page at the root URL (/). This is the entry point for all users — navigating to the site in a browser loads the HTML document that contains the counter display, boxing glove button, and all associated CSS and JavaScript.

Requirements

F14.R1 GET / returns 200 with HTML content

An HTTP GET request to the root path (/) must return a 200 OK status code with a Content-Type of text/html. The response body must be a complete HTML document that the browser can render into the functional application.

F14.R2 HTML contains required elements and title

The served HTML document must include the essential DOM elements: an element with id='counter-value' for the counter display, an element with id='punch-button' for the boxing glove, and a <title> tag. These elements are the minimum required for the JavaScript to initialize and for the application to function.

F15

GET /api/counter

4 requirements

This endpoint returns the current value of the punch counter. It is called by the frontend on initial page load and during periodic polling to keep the display synchronized with the server's authoritative count. The endpoint is read-only and has no side effects.

Requirements

F15.R1 Returns 200 with application/json Content-Type

The endpoint must respond with HTTP status 200 and a Content-Type header of application/json. This ensures the browser and frontend JavaScript can correctly parse the response as JSON data.

F15.R2 Response body has exactly { count: N }

The JSON response body must contain a single property named 'count' whose value is the current counter number. The response must not include extraneous properties, metadata, or wrapper objects — just the clean { count: N } shape that the frontend expects.

F15.R3 count is a non-negative integer

The count value must be a non-negative integer (0 or greater). It must not be null, undefined, a string, a floating-point number, or a negative number. The counter starts at zero and only increases, so negative values would indicate a data corruption bug.

F15.R4 Reflects count after increments

After one or more POST /api/increment requests have been processed, a subsequent GET /api/counter must return the updated count. The read endpoint must always reflect the latest persisted state — there must be no stale reads or caching that would return an outdated value.

F16

POST /api/increment

5 requirements

This endpoint increments the punch counter by exactly one and returns the new value. It is called each time a user clicks the boxing glove. The increment is atomic — the server reads the current count, adds one, writes the new value to disk, and returns it in a single request-response cycle.

Requirements

F16.R1 Returns 200 with application/json Content-Type

A successful increment must respond with HTTP status 200 and a Content-Type header of application/json. The 200 status confirms that the increment was processed and persisted.

F16.R2 Response body has exactly { count: N }

The JSON response body must contain a single property named 'count' whose value is the new (post-increment) counter value. This allows the frontend to immediately update the display with the authoritative new count without making a separate GET request.

F16.R3 count is incremented by exactly 1

Each call to POST /api/increment must increase the counter by exactly one. If the counter was at 5 before the request, it must be at 6 after. The increment must never skip values, add more than one, or fail to increment at all.

F16.R4 Increment persists to disk

The new counter value must be written to the counter.json file on disk before the response is sent. This ensures that if the server restarts, the count is not lost. Persistence is the mechanism by which the counter survives across server restarts and deployments.

F16.R5 Extra body fields are ignored

If the POST request includes additional fields in the JSON body beyond what the endpoint expects (or even an empty body), those fields must be silently ignored. The endpoint should not reject requests with unexpected properties — it simply reads the current count, increments, and responds.

F45

Edit Feature Suggestions

3 requirements

Users can edit the text of pending feature request suggestions before they are approved or denied by an admin. Once a suggestion has been approved or denied, it becomes immutable.

Requirements

F45.R1 PUT endpoint updates pending request text

PUT /api/feature-requests/:id accepts a text field and updates the matching feature request. Returns the updated request on success, 404 if not found, 400 if validation fails.

F45.R2 Cannot edit approved or denied requests

The PUT endpoint returns 403 Forbidden if the request has already been approved or denied, preventing modification of requests that have entered the review pipeline.

F45.R3 Edit button and form on feature requests page

Pending feature requests display an Edit button that reveals an inline edit form. The form has a textarea pre-filled with the current text, Save and Cancel buttons, and error display.

Backend — Data Persistence & Recovery

2 features

Features that ensure the counter value survives across server restarts, handles missing or corrupted data files, and recovers gracefully from filesystem issues.

F3

Counter Persistence

4 requirements

The punch counter is stored in a JSON file (counter.json) on the server's filesystem. This file-based persistence means the counter value survives server restarts, redeployments, and process crashes. The file is read on every API request and written on every increment, ensuring the on-disk state is always current.

Requirements

F3.R1 Value written to counter.json on increment

Each time POST /api/increment is called, the server must write the new counter value to the counter.json file before sending the response. The file must contain valid JSON in the shape { "count": N } where N is the updated integer value.

F3.R2 Counter file created on initialization if missing

When the server starts up, it checks for the existence of counter.json. If the file does not exist (first run, or the file was deleted), the server must create it with an initial value of { "count": 0 }. This ensures the application can always start cleanly.

F3.R3 Existing counter file not overwritten on initialization

If counter.json already exists when the server starts, the server must not overwrite or reset it. The existing count must be preserved. This is critical for deployments — restarting the server must never reset the punch count to zero.

F3.R4 Write handles zero, negative, and large numbers

The writeCounter function must correctly serialize any integer value, including zero, negative numbers (even though they shouldn't occur in normal operation), and very large numbers. The JSON serialization must not truncate, round, or corrupt the value.

F17

Counter File Recovery

5 requirements

The application must handle corrupted, missing, or malformed counter files gracefully at runtime — not just at startup. If the counter.json file is deleted while the server is running, or if its contents become invalid JSON, the server must recover by returning a count of zero and recreating the file, rather than crashing or returning an error to the user.

Requirements

F17.R1 Returns { count: 0 } when file is missing

If counter.json does not exist when a read is attempted (e.g., it was deleted after the server started), the server must return { count: 0 } to the API caller rather than throwing an error. The user should experience a reset counter, not a broken application.

F17.R2 Recreates the file when missing

When the server detects that counter.json is missing during a read operation, it must automatically recreate the file with { "count": 0 }. This self-healing behavior ensures that subsequent reads and writes will succeed without manual intervention.

F17.R3 Returns { count: 0 } for invalid JSON

If counter.json exists but contains invalid JSON (e.g., corrupted data, truncated writes), the server must catch the JSON.parse error, return { count: 0 }, and reinitialize the file. The application must never crash or return a 500 error due to a malformed counter file.

F17.R4 Returns { count: 0 } for empty file

If counter.json exists but is completely empty (zero bytes), the server must treat this the same as invalid JSON — return { count: 0 } and reinitialize. Empty files can occur due to interrupted writes or filesystem issues.

F17.R5 Passes through unexpected JSON shapes

If counter.json contains valid JSON but with an unexpected structure (e.g., { "value": 5 } instead of { "count": 5 }), the readCounter function passes through whatever it parses. The API layer then accesses .count, which would be undefined, and the behavior depends on the calling code. This is an edge case that tests document rather than strictly define.

Backend — Security & Cross-Origin Support

3 features

Features related to CORS headers, input validation, and handling of malformed or hostile client requests.

F18

CORS (Cross-Origin Resource Sharing)

3 requirements

The API enables Cross-Origin Resource Sharing by including the appropriate CORS headers in all responses. This allows the frontend to be served from any origin and still make API calls to the server. The CORS middleware is applied globally, covering GET, POST, and preflight OPTIONS requests.

Requirements

F18.R1 Access-Control-Allow-Origin: * on GET requests

All GET responses from the API (including /api/counter) must include the header Access-Control-Allow-Origin: *. This wildcard value permits any origin to read the response, which is appropriate for a public API with no authentication.

F18.R2 CORS headers present on POST requests

POST responses (including /api/increment) must also include CORS headers. Without these, browsers would block the frontend from reading the increment response if the page is served from a different origin than the API.

F18.R3 OPTIONS preflight returns CORS headers

The server must correctly respond to OPTIONS preflight requests with the appropriate CORS headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, etc.) and a 2xx status code. Browsers send preflight requests for POST calls with JSON content types, so this is essential for the increment functionality to work cross-origin.

F22

Malformed Body Handling

4 requirements

The server must handle malformed, unexpected, or edge-case request bodies without crashing or exposing internal error details. This includes broken JSON payloads, non-JSON content types, unusually large bodies, and requests to nonexistent endpoints. Robust input handling prevents accidental server crashes and provides appropriate error responses.

Requirements

F22.R1 Broken JSON returns 400, not 500

If a client sends a POST request with a Content-Type of application/json but the body is not valid JSON (e.g., '{broken'), the server must respond with a 400 Bad Request status, not a 500 Internal Server Error. The 400 status correctly communicates that the problem is with the client's request, not the server.

F22.R2 Non-JSON content-type still succeeds

If a POST /api/increment request is sent with a Content-Type other than application/json (e.g., text/plain), the server must still process the increment successfully. The endpoint does not depend on the request body for its logic — it simply increments the counter — so the content type is irrelevant.

F22.R3 Large body still succeeds

If a POST /api/increment request includes an unusually large request body (e.g., a JSON object with thousands of fields), the server must still process the increment successfully. Express's default body size limit is generous enough for normal use, and the endpoint ignores the body contents entirely.

F22.R4 404 response does not contain a count property

When a client requests a nonexistent API endpoint (e.g., GET /api/nonexistent), the server returns a 404 response. This response must not accidentally include a 'count' property in its body, which could confuse a client into thinking it received valid counter data.

F26

Security Scanning & Hardening

6 requirements

The server applies security-hardening HTTP headers via helmet.js to protect against common web vulnerabilities including clickjacking, MIME-type sniffing, and cross-site scripting. The CI pipeline performs automated dependency vulnerability scanning to catch known security issues before deployment.

Requirements

F26.R1 Security headers middleware applied to all responses

The server must use helmet.js middleware to apply a comprehensive set of security headers to every HTTP response. This includes Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, Referrer-Policy, and other protective headers. The middleware must be applied before route handlers and static file serving.

F26.R2 Content-Security-Policy allows only same-origin and inline resources

The Content-Security-Policy header must restrict resource loading to same-origin by default (default-src 'self'). Scripts and styles are allowed from 'self' and 'unsafe-inline' because the generated test report and features pages use inline scripts and styles. Images are allowed from 'self' and data: URIs for inline SVG support.

F26.R3 X-Content-Type-Options set to nosniff

Every HTTP response must include the X-Content-Type-Options: nosniff header. This prevents browsers from MIME-type sniffing responses away from the declared content-type, mitigating drive-by download attacks and reducing the risk of content-type confusion vulnerabilities.

F26.R4 X-Frame-Options prevents clickjacking

Every HTTP response must include an X-Frame-Options header (DENY or SAMEORIGIN) to prevent the application from being embedded in iframes on malicious sites. This protects against clickjacking attacks where an attacker overlays invisible frames to trick users into clicking unintended targets.

F26.R5 Strict-Transport-Security enforces HTTPS

Every HTTP response must include a Strict-Transport-Security header with a max-age value. This instructs browsers to only communicate with the server over HTTPS for the specified duration, preventing protocol downgrade attacks and cookie hijacking on insecure connections.

F26.R6 CI pipeline runs npm audit and fails on high/critical vulnerabilities

The GitHub Actions CI pipeline must include an npm audit step that checks all dependencies for known security vulnerabilities. The step must fail the build if any high or critical severity vulnerabilities are found, preventing deployment of code with known security risks.

Backend — Static Assets & Routing

3 features

Features that govern how the server delivers static files (CSS, JavaScript, images) and handles URL routing for special pages.

F19

Static File Serving

3 requirements

The server uses Express's built-in static file middleware to serve all files in the public/ directory. This includes the application's CSS stylesheet, client-side JavaScript, and any other assets. The static middleware handles content-type detection, caching headers, and 404 responses for missing files automatically.

Requirements

F19.R1 CSS and JS files load successfully

The application's stylesheet (style.css) and client-side script (script.js) must be served with 200 OK responses and correct content types. If these files fail to load, the application will appear unstyled and non-functional. The static middleware must correctly resolve paths relative to the public/ directory.

F19.R2 Favicon link element exists in HTML

The HTML document must include a <link> element that references a favicon. The favicon is an inline SVG data URI of a boxing glove, embedded directly in the HTML. This provides a recognizable icon in the browser tab without requiring a separate file request.

F19.R3 404 for nonexistent assets

Requests for files that do not exist in the public/ directory must receive a 404 Not Found response. The server must not return the index.html for all paths (SPA-style catch-all) — only explicitly defined routes and existing static files should return successful responses.

F20

Test Report Page

7 requirements

The application hosts an auto-generated test report at /test/. This report is produced during the CI/CD pipeline by running all test suites and aggregating their results into a self-contained HTML page. It provides three views: By Layer (Unit, Integration, Contract, Functional), By Feature (mapping tests to requirements), and By Release (grouping tests by release version).

Requirements

F20.R1 /test/ returns 200 when report exists

When the test report has been generated and the file public/test/index.html exists, a GET request to /test/ must return a 200 OK status with the report's HTML content. The report page must be fully self-contained with inline CSS and no external dependencies.

F20.R2 Report has correct title

The generated test report HTML must include a <title> element containing 'Test Report' (or similar). This ensures the browser tab displays a meaningful title when the report is open.

F20.R3 Summary cards are shown

The report must display summary cards at the top showing the total number of tests, number passed, number failed, and number of uncovered requirements. These cards provide an at-a-glance overview of the test suite's health.

F20.R4 Test layers are shown

The report's 'By Layer' view must display tests grouped into their respective layers: Unit, Integration, Contract, and Functional. Each layer shows its own pass/fail count and lists individual test results.

F20.R5 Back link to homepage

The report page must include a visible link that navigates the user back to the main application (the root URL /). This allows users to easily return to the punch counter after reviewing test results.

F20.R6 404 when report has not been generated

If the test report file does not exist (e.g., on a fresh server without a CI build), a GET request to /test/ must return a 404 response with an informative message (e.g., 'Test report not yet generated') rather than crashing or returning a blank page.

F20.R7 Hash navigation highlights target for 5 seconds

When a user navigates to the test report page with a URL hash (e.g., /test/#feature-F1-R1), the page must automatically switch to the 'By Feature' view, scroll to the matching requirement element, and apply a visual highlight that fades out over 5 seconds. This enables clickable test indicators on the features page to deep-link directly to the relevant test results.

F21

Test Report Redirect

1 requirement

For convenience and URL consistency, the server redirects requests to /test (without a trailing slash) to /test/ (with a trailing slash). This prevents 404 errors when users type the URL without the trailing slash and ensures the report's relative asset paths resolve correctly.

Requirements

F21.R1 GET /test returns 301 redirect to /test/

A GET request to /test (no trailing slash) must receive a 301 Moved Permanently response with a Location header pointing to /test/. The 301 status indicates a permanent redirect, which browsers and search engines will cache for future requests.

Infrastructure — Deployment & Operations

13 features

Features related to the production hosting environment, SSL/TLS security, and the automated deployment pipeline.

F23

SSL/HTTPS

1 requirement

The production site is served exclusively over HTTPS, ensuring all communication between the user's browser and the server is encrypted. SSL/TLS is provided by the hosting environment (DreamHost) and is configured in the CI/CD deployment workflow. Users who visit the site via HTTP are automatically redirected to HTTPS by the hosting platform.

Requirements

F23.R1 Production site is accessible over HTTPS

The production URL (https://punchjohneric.com) must be accessible over HTTPS with a valid SSL/TLS certificate. The certificate must not be expired, self-signed, or misconfigured. Browsers must not display any security warnings when visiting the site. The deployment workflow must configure the production URL with the https:// scheme.

F24

CI/CD Pipeline

1 requirement

The application uses GitHub Actions to automate its entire delivery lifecycle. Every push to the main branch triggers a three-stage pipeline: (1) Test — runs all unit, integration, contract, and functional tests; (2) Deploy — uploads files to the DreamHost VPS via SFTP and restarts the server via SSH; (3) Verify — runs post-deployment smoke tests against the live production site. This automation ensures that every change is tested before deployment and verified after.

Requirements

F24.R1 Automated test, deploy, and verify jobs

The GitHub Actions workflow (.github/workflows/deploy.yml) must define three sequential jobs: 'test', 'deploy', and 'verify'. The deploy job must depend on the test job (only deploys if tests pass), and the verify job must depend on the deploy job (only runs post-deployment checks after a successful deployment). This dependency chain ensures that broken code is never deployed and that deployments are always verified.

F25

Features & Requirements Page

11 requirements

The application hosts a self-contained documentation page at /features/ that enumerates every feature and requirement of the application. The page is generated from a static JSON data file (features.json) by a build script (scripts/generate-features.js) and deployed alongside the application via the CI/CD pipeline. It serves as a living specification — the authoritative reference for what the application does, organized by category, with verbose human-readable descriptions for each feature and each of its requirements.

Requirements

F25.R1 GET /features/ returns 200 when page exists

When the features page has been generated and the file public/features/index.html exists on disk, an HTTP GET request to /features/ must return a 200 OK status with HTML content. The page must be fully self-contained — all styles are inline, with no external CSS or JavaScript dependencies — so it renders correctly even if other assets fail to load.

F25.R2 GET /features redirects to /features/

A GET request to /features (without a trailing slash) must return a 301 Moved Permanently redirect with a Location header pointing to /features/. This matches the behavior of the /test route and prevents broken relative paths. The 301 status ensures browsers and search engines cache the redirect permanently.

F25.R3 Page has correct title

The generated HTML must include a <title> element containing both 'Features' and the application name. This ensures the browser tab displays a meaningful title when the page is open, making it easy to identify among multiple tabs.

F25.R4 Page lists all features from features.json

The generated page must display every feature defined in features.json. Each feature must show its feature ID (e.g., F1), its name, and its full verbose description. No features may be omitted or truncated. The count of features displayed must match the count in the source data file.

F25.R5 Page lists all requirements for each feature

For every feature displayed, all of its requirements must be listed. Each requirement must show its combined ID (e.g., F1.R1), its summary, and its full verbose description. The total count of requirements on the page must match the total across all features in features.json.

F25.R6 Page includes table of contents

The page must include a table of contents section near the top that lists all categories and their features as clickable anchor links. Clicking a feature in the table of contents must scroll the page to that feature's detailed section. This makes the page navigable for users looking for a specific feature.

F25.R7 Page includes navigation links

The page must include navigation links that allow the user to return to the main application (/) and to navigate to the test report (/test/). These links ensure the features page is connected to the rest of the site and does not feel like a dead end.

F25.R8 404 when page has not been generated

If the features page file does not exist on disk (e.g., the generate-features.js script has not been run), a GET request to /features/ must return a 404 response with an informative message such as 'Features page not yet generated.' The server must not crash or return a blank page.

F25.R9 Generator reads features.json and produces valid HTML

The scripts/generate-features.js script must read features.json from the project root, parse its contents, and produce a complete, valid HTML file at public/features/index.html. The output must include all categories, features, and requirements from the source data with no data loss. The script must exit with code 0 on success.

F25.R10 Hash navigation highlights target for 5 seconds

When a user navigates to a requirement or feature using a hash fragment identifier (e.g., /features/#F1-R1), the targeted element must be highlighted with a distinct background color and border for 5 seconds before fading back to normal. The highlight animation uses a 5-second ease-out transition to ensure users have sufficient time to identify the referenced requirement, especially when following links from test reports or external documentation.

F25.R11 Each requirement shows test coverage indicator

Every requirement listed on the features page must display a test coverage indicator showing whether it has passing tests. A green checkmark (✓) indicates the requirement has at least one passing test and links to the test report. An orange circle (◯) indicates stub tests exist. A gray X (✗) indicates no test coverage. Clicking a green checkmark or orange circle navigates to /test/ and highlights the corresponding requirement tests using the same 5-second highlight animation as R10.

F30

Test Report Accuracy

3 requirements

The generated test report must accurately reflect the actual state of testing. The report is built from JSON result files (test-results/jest.json, test-results/playwright.json) and the feature registry (tests/helpers/features.js). If these sources become stale, the report will show incorrect coverage data. This feature adds self-verification tests that catch discrepancies between the report and reality.

Requirements

F30.R1 Test result files contain tests for all features in the registry

The Jest and Playwright JSON result files must contain tagged tests for every feature defined in the feature registry (tests/helpers/features.js). Missing features indicate that test results are stale and need to be regenerated before building the report.

F30.R2 Every requirement has at least one passing test in the result files

For every requirement in the feature registry, the test result JSON files must contain at least one passing test tagged with that feature-requirement pair. Uncovered requirements indicate either missing tests or stale result files.

F30.R3 Feature registry and features.json are in sync

Every feature ID in tests/helpers/features.js must have a matching entry in features.json, and vice versa. Mismatches indicate that one file was updated without the other, which would cause the report or features page to be inconsistent.

F31

Rate Limiting

3 requirements

The server applies rate limiting to all API endpoints to prevent abuse. When a client exceeds the allowed number of requests within a time window, the server responds with HTTP 429 Too Many Requests. The frontend handles 429 responses by displaying a descriptive toast notification asking the user to slow down.

Requirements

F31.R1 Rate limiter middleware applied to /api/* routes

The server must use express-rate-limit middleware on all /api/* routes. The limiter enforces a maximum number of requests per IP address within a configurable time window (default: 100 requests per 15 seconds). Requests exceeding the limit receive a 429 status code.

F31.R2 429 response returned when limit exceeded

When a client exceeds the rate limit, the server must respond with HTTP 429 Too Many Requests. The response body must contain an error message. The response must not contain a count property to avoid confusion with valid counter responses.

F31.R3 Frontend shows toast on 429 response

When the frontend receives a 429 response from any API endpoint, it must display a toast notification with a message like 'Too many requests, please slow down.' The toast must use error styling to alert the user.

F32

Server-Side Heal Cooldown

4 requirements

The server enforces a 3-second heal cooldown per IP address, preventing users from bypassing the client-side localStorage cooldown. When a user attempts to heal while on server-side cooldown, the server responds with 429 and includes the remaining cooldown time. This ensures heal rate limiting cannot be circumvented by clearing browser storage.

Requirements

F32.R1 Server tracks heal cooldown per IP

The server maintains an in-memory Map of IP addresses to last-heal timestamps. When a heal request is processed successfully, the requesting IP's timestamp is updated to the current time.

F32.R2 Heal rejected with 429 when on cooldown

If a client sends POST /api/heal while their IP is within the 3-second cooldown window, the server must respond with HTTP 429. The response must include a retryAfter field indicating the remaining cooldown in seconds.

F32.R3 First heal succeeds, immediate second is rejected

The first POST /api/heal request from an IP must succeed with 200. An immediate subsequent request from the same IP (within 3 seconds) must be rejected with 429. After the cooldown expires, the next request must succeed again.

F32.R4 Frontend handles server-side cooldown rejection

When the frontend receives a 429 response from /api/heal, it must display a toast showing the remaining cooldown time. It must also sync the client-side cooldown timer with the server's retryAfter value to keep the UI accurate.

F33

WebSocket Sync

5 requirements

The application uses WebSocket connections for real-time counter synchronization between users. When any user punches, shields, or heals, all connected clients receive an instant update via WebSocket broadcast. HTTP polling is retained as a fallback — the poll interval increases to 30 seconds when WebSocket is active and reverts to 5 seconds when disconnected.

Requirements

F33.R1 WebSocket server attached to HTTP server

The server must create a WebSocket server (using the ws library) attached to the same HTTP server that serves the Express app. The WebSocket server must accept connections on the same port as the HTTP server.

F33.R2 State broadcast on every mutation

After every state-changing API call (POST /api/increment, POST /api/shield, POST /api/heal), the server must broadcast the current state ({ count, shields, users }) to all connected WebSocket clients.

F33.R3 Client receives initial state on WebSocket connect

When a client establishes a WebSocket connection, the server must immediately send the current counter state to that client. This ensures the client has up-to-date data without waiting for a poll cycle.

F33.R4 Frontend connects WebSocket with auto-reconnect

The frontend must establish a WebSocket connection on page load. If the connection closes, the client must automatically attempt to reconnect after 3 seconds. The reconnection must be transparent to the user.

F33.R5 Polling interval adapts to WebSocket status

When the WebSocket connection is active, the HTTP polling interval must increase to 30 seconds (reduced server load). When the WebSocket is disconnected, the interval must revert to 5 seconds (fast fallback). This ensures users always have reasonably fresh data regardless of WebSocket availability.

F34

Atomic File Writes

3 requirements

Counter state is written to disk using an atomic write pattern: data is first written to a temporary file, then renamed to the target file. Since rename is atomic on POSIX filesystems, this prevents data corruption if the process crashes mid-write. The temporary file is automatically cleaned up on success.

Requirements

F34.R1 writeState uses tmp file + rename pattern

The writeState function must write the JSON data to a temporary file (counter.json.tmp) first, then use fs.renameSync to atomically move it to the target file (counter.json). This ensures the target file is never in a partially-written state.

F34.R2 No .tmp file remains after successful write

After a successful writeState call, the temporary file (counter.json.tmp) must not exist on disk. The rename operation moves (not copies) the file, so no cleanup is needed.

F34.R3 Data integrity preserved through atomic write

After writing state via the atomic pattern, reading the file back must return exactly the same data that was written. The JSON must be valid and contain the correct count, shields, and shieldTimestamp values.

F35

Progressive Web App (PWA)

5 requirements

The application is a Progressive Web App that can be installed on mobile devices and desktops. A web app manifest provides metadata for the install experience (app name, icons, theme color, standalone display mode). A service worker caches static assets for faster loading. The app is installable from the browser without an app store.

Requirements

F35.R1 Web app manifest served at /manifest.json

The server must serve a valid manifest.json file at the /manifest.json path. The manifest must include name, short_name, start_url, display, background_color, theme_color, and icons fields. The HTML must include a <link rel='manifest'> tag referencing it.

F35.R2 Manifest has required PWA fields

The manifest.json must contain all fields required for PWA installability: name (string), icons (array with at least 192x192 and 512x512 sizes), start_url (string, '/'), and display (string, 'standalone'). These fields enable the browser's install prompt.

F35.R3 Service worker registered on page load

The frontend must register a service worker (/sw.js) when the page loads. Registration must be wrapped in a feature check ('serviceWorker' in navigator) to avoid errors in unsupported browsers. Registration failures must be silently caught.

F35.R4 Service worker caches static assets

The service worker must cache the application's static assets (HTML, CSS, JS, favicon) during the install event. Subsequent requests for cached assets must be served from the cache (cache-first strategy). API requests must not be cached and should always go to the network.

F35.R5 HTML includes PWA meta tags

The index.html must include a <meta name='theme-color'> tag for the browser chrome color, a <link rel='manifest'> tag pointing to manifest.json, and a <link rel='apple-touch-icon'> tag for iOS home screen icons.

F36

Versioning & Navigation

6 requirements

The application includes a semantic versioning system that displays the current version on all pages and provides access to release notes. A version.json endpoint serves the current version from package.json. Release notes are stored in releases.json and displayed on a dedicated /releases/ page. A tabbed navigation bar on the features, test report, and releases pages provides cohesive navigation between these pages.

Requirements

F36.R1 Version number displayed on all pages

All pages (main app, test report, features, releases) must display the current version number in a footer at the bottom of the page. The version is fetched dynamically from /version.json and falls back to a hardcoded value if the fetch fails. The footer must also include a link to the /releases/ page.

F36.R2 Version number links to /releases/ page

The version footer on all pages must include a clickable 'Release Notes' link that navigates to the /releases/ page. This allows users to see what changed in each version.

F36.R3 /releases/ page shows chronological release notes

The /releases/ page must display a list of all releases from releases.json, sorted chronologically (newest first). Each release must show the version badge, date, title, and categorized lists of changes (Features, Improvements, Fixes). The page must use the same purple gradient styling as /features/ and /test/.

F36.R4 Tabbed navigation on features, tests, and releases pages

The /features/, /test/, and /releases/ pages must include a tabbed navigation bar with three tabs: Features, Tests, and Releases. The currently active page must be highlighted. The main app page does NOT include these tabs.

F36.R5 Version.json endpoint provides current version

The server must serve a /version.json endpoint (static file) that contains the current version from package.json. The JSON response must have the shape { version: string } where version is a valid semantic version (e.g., '1.0.0').

F36.R6 Release notes stored in releases.json

Release notes must be stored in a releases.json file at the project root. Each release entry must include version, date, title, and arrays for features, improvements, and fixes. The /releases/ page is generated from this data by scripts/generate-releases.js.

F37

User Feature Requests

4 requirements

Users can submit and view feature requests through a dedicated Feature Requests tab. The tab appears in the navigation alongside Features, Tests, and Releases. Users can submit new requests via a form and see all previously submitted requests displayed in a list. Requests are persisted server-side so all users see the same requests. Implemented requests are marked with a badge linking to the specific release notes.

Requirements

F37.R1 Request a New Feature link visible at top of features page

The /features/ page must display a 'Request a New Feature' link prominently at the top of the page, above the feature list. Clicking this link must reveal a text input field (initially hidden) where users can type their feature request. The link must be clickable and the text input must become visible after clicking.

F37.R2 Feature Requests tab with form and request history

A 'Feature Requests' tab must appear in the navigation on all tabbed pages (Features, Tests, Releases, Feature Requests). The tab links to /feature-requests/ which displays a form for submitting new requests (textarea and submit button) and a list showing all previously submitted requests with timestamps. The active tab is highlighted when viewing the Feature Requests page.

F37.R3 Server-side persistence shared across all users

Feature requests must be stored server-side in a feature-requests.json file using atomic writes (tmp + rename). The GET /api/feature-requests endpoint returns all requests. The POST /api/feature-requests endpoint accepts a JSON body with a text field and creates a new request with id, text, timestamp, implemented status, and implementedVersion fields.

F37.R4 Implemented requests marked with release version link

When a feature request is marked as implemented, it displays a green 'Implemented in vX.Y.Z' badge with a link to the specific release notes anchor at /releases/#X.Y.Z. Implemented requests have a green left border and light green background to visually distinguish them from pending requests.

F38

Admin Panel

5 requirements

An authenticated admin panel at /admin/ allows authorized users to manage the application. Admins can log in with a username and password, reset or set the punch counter, enable or disable individual actions (punch, shield, heal), and approve or deny user-submitted feature requests. The admin tab is hidden from navigation unless the user is authenticated.

Requirements

F38.R1 Admin authentication with login/logout

The admin panel requires authentication via username and password (from ADMIN_USER and ADMIN_PASSWORD environment variables). POST /api/admin/login validates credentials using crypto.timingSafeEqual and returns an httpOnly session cookie. POST /api/admin/logout clears the session. GET /api/admin/status returns { authenticated: bool }. Sessions expire after 24 hours.

F38.R2 Counter management (reset and set)

Authenticated admins can reset the counter to 0 via POST /api/admin/reset-counter, or set it to any non-negative integer via POST /api/admin/set-counter with { value }. Both endpoints broadcast the updated state to all connected WebSocket clients.

F38.R3 Action toggle (disable/enable punch, shield, heal)

Authenticated admins can disable or enable individual actions (punch, shield, heal) via POST /api/admin/toggle-action with { action, enabled }. Disabled actions return 403 from their API endpoints. The disabled state is persisted in counter.json and broadcast via WebSocket. Disabled actions appear grayed out and non-interactable on the frontend.

F38.R4 Approve or deny feature requests

Authenticated admins viewing the /feature-requests/ page see Approve and Deny buttons inline next to each pending request. POST /api/admin/approve-request and POST /api/admin/deny-request accept { id } and update the request status. Approved requests show a green badge, denied requests show a red badge.

F38.R5 Hidden admin tab shown only when authenticated

The Admin tab in the navigation bar is hidden by default (display:none). On each tabbed page, a script checks GET /api/admin/status and shows the tab if authenticated. The admin panel page itself always shows the tab as active.

F39

Automated Feature Implementation

7 requirements

When an admin approves a user-submitted feature request, the server automatically triggers a GitHub Actions workflow via repository_dispatch. The workflow runs a Node.js script that calls the Claude API to generate an implementation plan, write tests, implement the feature, update registries, and bump the version. The result is a pull request on a new branch for human review — never auto-merged.

Requirements

F39.R1 Admin approval triggers GitHub Actions workflow via repository_dispatch

When POST /api/admin/approve-request succeeds, the server calls the GitHub API to send a repository_dispatch event of type 'implement-feature-request'. The dispatch includes the feature request ID, text, and approval timestamp as client_payload. The call is fire-and-forget: if the GitHub API is unreachable or the environment variables (GITHUB_TOKEN, GITHUB_REPO) are missing, the approval still succeeds and the error is logged.

F39.R2 Implementation script calls Claude API to generate plan and code

The scripts/implement-feature.js script reads the feature request text, builds project context (CLAUDE.md, features.json, test registry, sample tests, server source, frontend files), and makes multiple Claude API calls: first to generate a JSON implementation plan (feature ID, requirements, test plan, files to create/modify, version bump), then to generate each file's content. Generated test files follow existing test patterns with correct imports and annotations.

F39.R3 Pipeline creates a PR with generated implementation for human review

The GitHub Actions workflow creates a new branch (auto/feature-{id}), commits all generated files, and opens a pull request against main. The PR includes the original request text, implementation plan summary, and a review checklist. The PR is never auto-merged — a human must review the generated code, verify CI passes, and merge manually.

F39.R4 Manual workflow_dispatch fallback for direct triggering

The implement-feature.yml workflow also supports workflow_dispatch with a request_text input, allowing admins to trigger the implementation pipeline manually from the GitHub Actions UI without going through the feature request approval flow.

F39.R5 Webhook marks feature request as implemented after merge

After the implement-feature workflow successfully merges a PR, it calls a webhook on the production server to set implemented=true and implementedVersion on the corresponding feature request. The webhook is secured by a shared secret and retries up to 3 times to handle server restarts during deploy.

F39.R6 Self-healing fix loop retries failing tests with Claude API

When auto-generated code fails tests, the pipeline runs a fix script (scripts/fix-failing-tests.js) that parses the test output, identifies failing test files and their implementation files, and calls the Claude API to generate fixes. The pipeline retries up to 2 times before giving up. If fixes succeed, the corrected code is committed and the PR is auto-merged.

F39.R7 TDD pipeline: tests run inside script, PR only after all pass

The implementation script follows TDD: it generates tests first (RED phase), then generates implementation (GREEN phase), runs all tests internally via child_process, and uses a fix loop (up to 3 attempts) to resolve failures. The script only exits 0 when all tests pass. The workflow only creates a branch and PR after the script succeeds, ensuring no PR ever exists with failing tests.