A proof-of-concept full-stack application for hotel shift handover notes.
The app helps front-desk staff keep handover tasks consistent across shifts, especially when notes come from a mix of structured system events and free-text notes written during downtime.
- Staff signup and login.
- View open handover tasks first, sorted by computed priority.
- Add structured handover notes manually.
- Reconcile free-text shift notes into structured event records using OpenRouter.
- Normalize multilingual notes into English during reconciliation.
- Select the source-of-truth shift for reconciled notes using:
- shift start date:
yyyy-mm-dd - shift kind:
day,night, ormorning - resulting shift ID:
yyyy-mm-dd-day,yyyy-mm-dd-night, oryyyy-mm-dd-morning
- shift start date:
- Override task priority when AI/computed priority is wrong.
- Add task thread history for follow-up, resolution, or unresolved updates.
- Resolve a task with a short message and optional image attachment.
- Store thread images in AWS S3.
- Protect the API with configurable sliding-window rate limiting backed by Redis/Valkey.
- TypeScript
- Hono
- PostgreSQL
- Redis/Valkey
- OpenRouter for free-text reconciliation
- AWS S3 for thread image attachments
- SvelteKit static site
- TypeScript
backend/ Hono API, migrations, services, tests
frontend/ SvelteKit static frontend
shared/ Shared Zod schemas and TypeScript types
sample/ Sample structured events and free-text night log
- Node.js and npm
- Docker
- Local Postgres and Redis/Valkey containers
- Optional: AWS credentials for testing image upload to S3
- Optional: OpenRouter API key for testing reconciliation
npm install --package-lock-only=falseThis repository intentionally includes a restrictive .npmrc to reduce supply-chain risk during day-to-day work:
allow-git=none
allow-remote=none
save-exact=true
min-release-age=5
prefer-offline=true
prefer-dedupe=true
init-private=true
ignore-scripts=true
package-lock-only=trueThe important consequence is package-lock-only=true: a plain npm install updates/checks the lockfile but does not install node_modules.
Use this command when you actually need local dependencies installed:
npm install --package-lock-only=falseFor a clean CI/deploy install, use:
npm ci --package-lock-only=falseKeep ignore-scripts=true unless a dependency genuinely requires lifecycle scripts. If you must allow scripts for a one-off trusted install, make it explicit:
npm ci --package-lock-only=false --ignore-scripts=falseDo not remove the restrictive defaults casually; prefer explicit per-command overrides so risky install behavior is visible in shell history and deployment config.
Copy the example environment file:
cp .env.example .envImportant backend variables:
PORT=8787
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
REDIS_URL=redis://localhost:6379
JWT_SECRET=change-this-in-production
CORS_ORIGIN=http://localhost:5173
HOTEL_ID=lumen-sg
HOTEL_TIMEZONE=+08:00
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW_SECONDS=60
RATE_LIMIT_MAX_REQUESTS=120
OPENROUTER_API_KEY=
OPENROUTER_MODEL=openai/gpt-4o-mini
AWS_REGION=ap-southeast-1
AWS_S3_BUCKET=handover-shift-notes-ky64-poc
S3_PUBLIC_BASE_URL=Important frontend variable:
VITE_API_BASE_URL=http://localhost:8787OPENROUTER_API_KEY can be left empty unless you are testing free-text reconciliation.
The expected local Docker container names are postgres and redis:
docker start postgres redisCheck containers if needed:
docker ps -aRun migrations:
npm run db:migrateSeed sample events:
npm run db:seedThe seed data comes from sample/events.json.
Start backend:
npm run dev:backendStart frontend in another terminal:
npm run dev:frontendThen open the local frontend shown by Vite, usually:
http://localhost:5173
- Start Postgres and Redis/Valkey.
- Run migrations and seed data.
- Start backend and frontend.
- Open the frontend.
- Signup or login.
- Confirm the handover page shows seeded open tasks first.
- Change a task priority from the dropdown.
- Click Resolve or Add thread.
- Add a short thread message.
- Optionally attach an image if AWS credentials and S3 bucket are configured.
- Confirm the thread appears in task history.
- Open a resolved task and confirm its thread is still viewable.
To test free-text reconciliation:
- Set
OPENROUTER_API_KEYin the backend environment. - Start the backend.
- Open
/reconcilein the frontend. - Enter:
- shift start date, e.g.
2026-05-27 - shift kind, e.g.
night - free-text notes from
sample/night-logs.md
- shift start date, e.g.
- Submit the form.
- Confirm structured events are auto-written and visible in the handover task list.
The staff-selected shift ID is the source of truth for reconciled notes. Model-inferred timestamps are stored on the event, but shift_id and business_date come from the reconciliation form.
Fetch the current handover task list:
curl http://localhost:8787/eventsFilter by status or room:
# Only unresolved tasks
curl "http://localhost:8787/events?status=unresolved"
# Tasks for a specific room
curl "http://localhost:8787/events?room=205"
# Combine filters
curl "http://localhost:8787/events?status=pending&room=310"Valid status values: resolved, unresolved, pending.
The response returns all matching events sorted by priority (critical > high > medium > low), with tie-breaks by timestamp. No authentication is required for this endpoint.
Build everything:
npm run buildRun tests:
npm testBackend-only build:
npm run build --workspace @handover/backendFrontend-only build:
npm run build --workspace @handover/frontend- The app is a POC and uses simple email/password authentication with JWT bearer tokens.
- There are no roles yet; every authenticated staff user can create/update tasks and threads.
- Free-text reconciliation auto-writes extracted events, but raw notes and model output are preserved for auditability.
- The UI is intentionally light-mode only, minimal, and formal for institutional use.