Skip to content
acorjordanPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ACOR Library — Guest Check-in & Statistics

A self-service kiosk for library visitors plus a statistics dashboard for staff. Built to run on the existing ACOR Kubernetes cluster (Postgres operator, ingress-nginx, cert-manager) with a tiny dependency footprint.

The core idea: register once, count every visit

The librarian asked for two things that pull against each other:

  1. "The form should only appear the first time someone registers."
  2. "…display metrics and statistics, especially monthly visitor numbers."

If we only stored registrations, "monthly visitors" would go flat the moment everyone had signed up — a regular who comes weekly would generate no data. So the data model has two tables:

Table Written Holds
guest once, at first visit the profile (all the librarian's fields)
visit every check-in timestamp + that day's reason for visiting

This is what makes the dashboard real: monthly visits, unique visitors, new vs. returning, and every breakdown fall out of these two tables.

The kiosk flow follows from it:

Enter email ─▶ known guest? ─┬─ yes ─▶ "Welcome back" + reason ─▶ log visit ─▶ ✓
                             └─ no  ─▶ full registration form  ─▶ create guest + first visit ─▶ ✓

Fields

All questions live in one file — src/fields.js — which drives both the on-screen form and server-side validation, so they can't drift. To change a question, edit only that file.

Captured once (profile): name, email, phone, nationality, country (optional), gender, age, status, topic(s) of research, preferred medium(s), and the open "what else would you request?" prompt. Captured every visit: reason for today's visit.

Deliberate tweaks to the raw spec, all easy to revert in fields.js:

  • Age → age bands (Under 18 … 65+) instead of a raw number: better privacy and cleaner charts. (To collect an exact number, change the age_band field to type: 'text'.)
  • Gender includes Prefer to self-describe / Prefer not to say.
  • Reason / topic / medium are multi-select (people rarely have exactly one).
  • Added an optional country of residence to enrich the Jordanian / International / US-citizen split without changing it.
  • A required consent checkbox — this is personal data.

Run it locally

Option A — everything in Docker:

docker compose up --build      # → http://localhost:3000  (dashboard: librarian / change-me)

Option B — Node against a local Postgres:

cp .env.example .env           # edit DATABASE_URL if needed
createdb guestbook
npm install                    # also vendors chart.js into public/vendor
npm start                      # migrations run automatically on boot

Deploy

Build (GitHub Actions → GHCR)

.github/workflows/build.yml builds this repo's Dockerfile and pushes to ghcr.io/acorjordan/kiosk on every push to main (tags: latest, the short commit SHA, and semver on v* tags). Make the GHCR package public (or give the cluster a pull secret) so the operator can pull it — matches the other public ACOR images (e.g. ghcr.io/acorjordan/*).

Deploy to the ACOR cluster (Ansible)

Deployment lives in the acor/infrastructure repo, following the existing per-app pattern. Two playbooks, in order:

# 1. Provision the Postgres instance (Zalando operator) in the kiosk namespace
ansible-playbook playbooks/k8s-postgres-kiosk.yml

# 2. Deploy the app (Secret + Deployment + Service + Ingress)
ansible-playbook playbooks/k8s-kiosk.yml

Before the first run, add the dashboard credentials to the k8s vault:

ansible-vault edit group_vars/k8s/vault.yml
#   kiosk_dashboard_user: librarian
#   kiosk_dashboard_password: <a strong password>

The image/tag is set in group_vars/all/containers.yml (containers.kiosk). The app reads its DB password from the operator-managed Secret, composes DATABASE_URL, and runs its own migrations on startup (guarded by a Postgres advisory lock so the 2 replicas don't race). /healthz backs the k8s probes. Served internal-only at https://kiosk.k8s.acorjordan.org — resolves via the existing *.k8s.acorjordan.org wildcard to the private cluster nodes, so no DNS change is needed and the kiosk is not exposed publicly.

The generic manifests in k8s/ are kept for standalone / non-ACOR use. The Ansible playbooks above are the source of truth for the ACOR deployment.

Dashboard & Grafana

/dashboard gives the librarian headline KPIs, a 12-month visits chart, new-vs- returning, and breakdowns by nationality/status/topic/medium/gender/age, plus CSV export of guests and visits.

Because the numbers live in plain Postgres tables, the same data is directly queryable from Grafana (you already run it). Point a Postgres data source at the guestbook DB; e.g. monthly visitors:

select date_trunc('month', created_at) as t, count(*) as visits
from visit group by 1 order by 1;

Privacy notes

  • Personal data is collected with explicit consent (stored as consent_at).
  • The dashboard and CSV exports are behind basic auth; the app refuses to start in production without DASHBOARD_PASSWORD set.
  • Set container TZ=Asia/Amman so "month" boundaries match local time.
  • Consider a retention policy (e.g. delete visit rows older than N years) and who on staff may access exports.

Project layout

src/
  fields.js     one source of truth for the form (+ validation)
  db.js         pg pool + auto-migrations
  stats.js      all dashboard SQL + CSV
  routes/       checkin (kiosk) + dashboard
  views.js      server-rendered HTML shells
public/         kiosk.js, dashboard.js, styles, vendored chart.js
migrations/     001_init.sql
k8s/            postgres (Zalando), deployment, service, ingress, secret

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages