No description
  • JavaScript 81.5%
  • CSS 10.8%
  • HTML 6.1%
  • Dockerfile 1.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
JButler 2380cf8644
All checks were successful
CI / test (push) Successful in 57s
CI / e2e (push) Successful in 3m5s
Release / release (push) Successful in 58s
Add ci
2026-08-22 01:10:46 -04:00
.forgejo/workflows Add ci 2026-08-22 01:10:46 -04:00
public Edit equip 2026-08-21 12:02:37 -04:00
server Edit equip 2026-08-21 12:02:37 -04:00
.dockerignore e2e testing 2026-08-21 11:24:19 -04:00
.gitignore e2e testing 2026-08-21 11:24:19 -04:00
docker-compose.test.yml Add ci 2026-08-22 01:10:46 -04:00
docker-compose.yml Add ci 2026-08-22 01:10:46 -04:00
Dockerfile e2e testing 2026-08-21 11:24:19 -04:00
README.md Edit equip 2026-08-21 12:02:37 -04:00
Taskfile.yml Add ci 2026-08-22 01:10:46 -04:00

Lab Equipment Reservation Manager

A small web app for tracking lab hardware and reserving it for up to 7 days at a time. No login required.

Run it

With Task installed, task lists everything available; the common ones:

task up      # build + start the app in the background
task logs    # tail the app server's logs
task down    # stop it

Without Task, the equivalent is:

docker compose up --build

Either way, then open http://localhost:3000. Data lives in Postgres, persisted in the pgdata Docker volume, so it survives restarts.

Demo data

To start with a pre-populated set of fake equipment and reservations (via @faker-js/faker):

task up:seed
# or: SEED_DEMO_DATA=true docker compose up --build

Seeding only happens once, the first time the equipment table is empty.

All tasks

Task What it does
task up Build + start the app in the background
task up:seed Same, pre-populated with demo data
task down Stop the app
task restart Rebuild + restart just the app server (after a code change)
task logs Tail the app server's logs
task build Build the production image
task test Run the backend/unit suite, isolated, self-cleaning
task test:e2e Run the Playwright E2E suite, isolated, self-cleaning
task test:all Both of the above, one after the other
task clean Stop everything and remove volumes (dev + test stacks)
task install npm install in server/, for local non-Docker development

How it's built

  • Backend: Node.js + Express, talking to Postgres via pg. Schema lives in server/schema.sql and is applied automatically on startup.
    • server/index.js — assembles the app and mounts the routers; no route logic of its own
    • server/routes/ — one file per resource: equipment.js, reservations.js, avatar.js, events.js
    • server/lib/ — small shared pieces used by more than one route: http.js (the async-route wrapper and date parsing), broadcast.js (the SSE client list), dicebear.js (avatar rendering)
    • server/db.js / server/seed.js — the Postgres pool/migrations and the demo-data seeder
  • Frontend: plain JS (public/js/) loaded as native ES modules — no build step, no bundler. index.html loads a single entry point (js/main.js); everything else is pulled in via import.
    • main.js — wires up view-tab switching and the one-time setup for each view, then does the initial load
    • refresh.js — the only module that knows about all three views; re-fetches equipment and re-renders List/Manage/Timeline after any change
    • list-view.js, manage-view.js, timeline-view.js — one per view (List, Manage, Timeline/Gantt)
    • reservation-modal.js, confirm-dialog.js — the two modals, shared across views
    • api.js — every fetch() call to the backend, in one place
    • date-utils.js, format.js — pure helpers with no DOM dependency (unit-tested directly, see below)
  • API:
    • GET /api/equipment — list equipment with availability status and current/upcoming reservations
    • POST /api/equipment — add new hardware
    • PATCH /api/equipment/:id — update a device's description
    • DELETE /api/equipment/:id — remove hardware
    • GET /api/categories — distinct categories currently in use, for the "Add Equipment" dropdown
    • POST /api/reservations — reserve equipment (max 7 days, no overlapping bookings)
    • DELETE /api/reservations/:id — cancel a reservation (soft delete — it moves to history)
    • GET /api/equipment/:id/reservations/history — a device's past reservations (expired or cancelled)
    • GET /api/reservations?from&to — reservations touching a time window, across all equipment (feeds the Timeline view)
    • GET /api/avatar/:seed — renders a DiceBear "bottts" avatar SVG for the given seed, entirely offline
    • POST /api/equipment/:id/regenerate-avatar — assigns a new random avatar seed
    • GET /api/events — Server-Sent Events stream; the frontend refreshes automatically whenever any browser reserves, cancels, or adds/removes equipment

Reservations expire naturally: once end_at passes, they stop counting toward availability and drop out of the equipment list on their own — no cleanup job needed. They remain visible under each device's "History". A periodic server heartbeat (every 60s) also nudges connected browsers to refresh, so a reservation expiring purely because time passed still updates the UI without a page reload.

The Dockerfile

Multi-stage, with production (the default build target) kept lean:

  • deps — installs production npm dependencies only (npm ci --omit=dev), cached independently of the app source so code changes don't bust this layer
  • production — the image docker-compose.yml actually runs: copies in deps' node_modules plus the app source, runs as the non-root node user, and exposes a HEALTHCHECK against /healthz. No test tooling of any kind ends up in here.
  • e2e — used only by docker-compose.test.yml (--target e2e), built from Microsoft's mcr.microsoft.com/playwright image (browsers preinstalled — Playwright doesn't run reliably on Alpine/musl) with the full npm ci including devDependencies

Running tests

There are two independent suites, both run fully containerized against a throwaway Postgres instance each.

Backend / unit tests (server/tests/), via Node's built-in test runner — no extra framework needed:

  • date-utils.test.js — pure date/time logic shared with the frontend (public/js/date-utils.js), including regression tests for two real bugs found during development (one rounding bug, caught by hand; one caught by the E2E suite below — see computeDefaultStart does not round "now" forward)
  • equipment.test.js — equipment CRUD, description updates, categories, and avatar generation/regeneration
  • reservations.test.js — reservation validation (7-day max, no past starts, no overlaps, back-to-back is allowed), cancellation, and history
  • events.test.js — the live-update (SSE) broadcast
task test
# or: docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test test

Frontend / E2E tests (server/e2e/), via Playwright, driving a real Chromium browser against a real running instance of the app:

  • equipment.spec.js — adding hardware, editing/cancelling a description edit, avatar regeneration, the remove-confirmation dialog, adding a new category
  • reservation.spec.js — reserving from the List view, the overlap-conflict error, cancelling
  • timeline.spec.js — reserving by clicking a day on the Gantt chart, Earlier/Later navigation
  • live-updates.spec.js — two separate browser contexts, confirming the SSE live-update actually reaches a second, idle browser
task test:e2e
# or: docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from e2e e2e

Run both suites with task test:all, or the two docker compose commands separately (not combined into one up) — combining them under --abort-on-container-exit would tear the whole run down as soon as whichever suite finishes first, rather than letting both run to completion. Either way, task test and task test:e2e clean up their own containers/volumes automatically, on both success and failure (task clean also does this by hand, plus wipes the dev stack).

Or locally, against a Postgres instance you already have running:

task install    # or: cd server && npm install
DATABASE_URL=postgres://reservemgr:reservemgr@localhost:5432/reservemgr_test npm test        # backend, from server/
DATABASE_URL=postgres://reservemgr:reservemgr@localhost:5432/reservemgr_test npm start &      # in one terminal
E2E_BASE_URL=http://localhost:3000 npx playwright test                                        # in another

Point DATABASE_URL at a database you don't mind being wiped — the backend suite truncates all tables between tests, and the E2E suite gives every piece of equipment it creates a unique, timestamped name rather than relying on a clean slate.

Local development (without Docker)

You'll need a Postgres instance reachable via DATABASE_URL.

cd server
npm install
DATABASE_URL=postgres://reservemgr:reservemgr@localhost:5432/reservemgr npm start

The server serves the frontend from ../public, so visiting http://localhost:3000 works the same way.