Answers to the most common questions and error scenarios for Chronos Ledger.
- Setup & First Boot
- Authentication & Accounts
- Onboarding Wizard
- CSV Import & Timetable
- Attendance & Geofencing
- Absences & Proxy
- Guest Kiosk
- Notifications & Web Push
- WebSocket / Live Updates
- Docker & Deployment
- CI/CD & GHCR
- New Academic Year Rollover
- Data & Privacy
Make sure the script is executable and you are running it with bash, not sh:
chmod +x setup.sh
bash setup.shOn some distributions sh is dash, which does not support the local keyword used in the script.
The backend container is still starting. Wait 10–15 seconds and refresh. You can watch readiness:
docker compose logs -f backendThe backend prints Application startup complete. when it is ready. If it never prints this, check for a database connection error (see below).
PostgreSQL is not ready yet, or the DATABASE_URL is misconfigured.
- Verify the postgres container is running:
docker compose ps - Check
.env—DB_PASSWORDmust match the password embedded inDATABASE_URL(or use the flat variable substitution indocker-compose.yml). - If postgres crashed, inspect its logs:
docker compose logs db
The most common cause is a password mismatch. Run docker compose down -v to wipe volumes, fix .env, and run docker compose up again.
You are running migrations against a database that already has tables from a previous partial run. Options:
# Wipe the database and start clean (dev only)
docker compose down -v
docker compose up
# Or stamp the current state and skip the failed migration
cd backend
uv run alembic stamp headsetup.sh is designed for Linux/macOS. On Windows, use WSL2:
wsl bash setup.shOr manually generate secrets and copy them into .env:
# PowerShell equivalent
[System.Web.Security.Membership]::GeneratePassword(64,0)The NEXT_PUBLIC_API_URL was built with http://localhost instead of the server's LAN IP.
setup.sh auto-detects the LAN IP. If it detected incorrectly, rebuild with the correct IP:
NEXT_PUBLIC_API_URL=http://192.168.1.10/api/v1 \
NEXT_PUBLIC_WS_URL=ws://192.168.1.10/ws \
docker compose up --buildAlso ensure APP_CORS_ORIGINS in .env includes http://192.168.1.10.
| Field | Value |
|---|---|
admin@college.internal |
|
| Password | ChronosAdmin2026! |
Change this immediately — the admin is prompted to do so on first login via the Onboarding Wizard.
The seed migration creates the admin account during alembic upgrade head. If migrations did not run (check docker compose logs backend), the account does not exist.
Force migrations:
docker compose exec backend uv run alembic upgrade headSuper Admins can reset any user's password via the Admin dashboard. If the Super Admin account itself is locked, reset directly in the database:
docker compose exec db psql -U chronos_admin -d chronos_ledger -c \
"UPDATE users SET hashed_password = crypt('NewTempPassword!', gen_salt('bf')) WHERE email = 'admin@college.internal';"Requires the pgcrypto extension, which is enabled by the seed migration.
Chronos Ledger uses HS256 JWTs with 8-hour expiry. If the server clock jumped forward, existing tokens become invalid immediately. Users must log in again.
To change the expiry window, set JWT_ACCESS_TOKEN_EXPIRE_MINUTES in .env.
The banner is driven by initial_login_state on the user record. If the Onboarding Wizard password step did not complete successfully, this flag was not cleared.
Fix: complete the password step in the Onboarding Wizard (Admin Dashboard → Setup Guide), or clear it directly:
docker compose exec db psql -U chronos_admin -d chronos_ledger -c \
"UPDATE users SET initial_login_state = false WHERE email = 'admin@college.internal';"Automatically on first login when initial_login_state is true for a SUPER_ADMIN or DEPT_ADMIN account. It can also be reopened at any time from Admin Dashboard → Setup Guide button (top-right of the tab bar).
| Step | Required | Can skip? |
|---|---|---|
| 1. Change password | Yes | No |
| 2. Create academic cycle | Yes | No |
| 3. Import CSV | Yes | No |
| 4. Generate ledger | Recommended | Yes |
| 5. Done | — | — |
The ledger is not generated automatically after import. Go to Step 4 — Generate Ledger in the wizard (or Admin Dashboard → Import → Generate Daily Ledger). The nightly cron runs at midnight, but you can trigger it manually from the UI.
Use the Setup Guide link from the Admin Dashboard. On Step 2, create a new academic cycle (leave the old one — historical data is preserved under the previous cycle). Then re-upload the new semester's CSV.
The new cycle becomes active immediately for ledger generation.
The ingestion engine expects a student-centric format. Required columns:
| Column | Example |
|---|---|
student_roll |
22CS001 |
student_name |
Alice Sharma |
student_email |
22cs001@college.internal |
faculty_email |
prof.kumar@college.internal |
faculty_name |
Dr. Kumar |
course_code |
CS301 |
course_title |
Operating Systems |
section |
A |
day_of_week |
Monday |
time_start |
09:00 |
time_end |
10:00 |
room |
LH-3 |
dept_code |
CS |
Column names are case-insensitive. Extra columns are ignored.
The import is idempotent — re-running it with the same data is safe. "Duplicate key" errors suggest the CSV has internal duplicates (the same student+course+slot appears twice). Remove duplicates and re-upload.
The API response includes a skipped count with reasons. Download the response JSON from the browser developer tools (Network tab → the POST /api/v1/ingestion/upload request → Response). Common skip reasons:
invalid_email— faculty or student email does not match expected formatunknown_day—day_of_weekis not a full English day nametime_parse_error— time is notHH:MM24-hour format
The browser geolocation API requires HTTPS or localhost. If the app is served over plain HTTP, the location prompt will be blocked by the browser.
Options:
- Recommended: Set up TLS on nginx (see
docs/deployment.md). - Testing only: In Chrome, go to
chrome://flags/#unsafely-treat-insecure-origin-as-secureand add your server IP.
The server-side check uses the room's configured lat/lon plus a 30-metre radius. If the room coordinates in the database are wrong, every mark attempt will fail.
Update room coordinates:
docker compose exec db psql -U chronos_admin -d chronos_ledger -c \
"UPDATE master_slots SET room_lat = 12.9716, room_lon = 77.5946 WHERE target_room_identifier = 'LH-3';"The altitude delta threshold is |Δalt| < 4 metres. GPS altitude accuracy is typically ±10–20m on mobile devices, making this check unreliable outdoors. The check only fires when the device reports altitude — if the device does not expose it, the check is skipped.
If you want to widen the threshold, it is a constant in backend/app/services/geo_fence.py:
ALT_DELTA_THRESHOLD_M = 4.0 # change to 10.0 for looser enforcementThe service worker's Background Sync fires when the browser decides to — usually within a few seconds of going online. If it is not firing:
- Make sure the PWA is installed (added to home screen), not just open in a tab.
- Check
chrome://serviceworker-internalsto confirm the SW is registered and active. - Force a sync in DevTools: Application → Service Workers → Sync → push the
attendance-synctag.
WebSocket notifications are only delivered to connected clients. The line manager must have the app open. If they are offline, the absence request will still appear in their pending queue when they next log in.
For email notifications, the current release does not include an email transport. This is a planned enhancement.
The student timeline reads from the live ledger. After approving a proxy, trigger a ledger refresh: Admin Dashboard → Import → Generate Daily Ledger (or wait for the midnight cron).
The ledger is a materialized daily snapshot. Approved absences cascade ON_LEAVE only when the ledger is regenerated. Use the manual Generate button in the Admin Dashboard.
The faculty member must be online with the app open. The notification arrives via WebSocket to /faculty/dashboard. If they are offline, the request stays pending in the database and will appear when they next log in.
Check that the faculty member's email in the guest form exactly matches their account email — the lookup is case-insensitive but the email must exist in the system.
Yes. The guest kiosk (/guest/kiosk) is explicitly public. It does not expose any internal data — it only allows submitting a visit request and viewing the faculty notification status. The underlying API endpoints (/api/v1/guest/*) are similarly unauthenticated by design.
- Confirm the user has granted notification permission (browser prompt when first logging in).
- Verify VAPID keys are set in
.env—VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY, andVAPID_CONTACT_EMAIL. - Check that the frontend was built with the matching
NEXT_PUBLIC_VAPID_PUBLIC_KEY. - VAPID public key must be the same value in both places. Regenerate with
npx web-push generate-vapid-keysif unsure, then rebuild.
Reminders fire 15 minutes before the slot's time_window_start via periodicsync in the service worker. The accuracy depends on when the browser chooses to fire the periodic sync — browsers enforce a minimum interval of ~1 hour for battery reasons.
For more reliable reminders, the user must keep the tab open (the service worker runs JavaScript timers when the tab is active).
The WebSocket connects to NEXT_PUBLIC_WS_URL. In the GHCR image this defaults to /ws (same-origin). If you are running the frontend dev server and the backend separately, ensure NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws.
Also check nginx is proxying /ws correctly — see nginx/nginx.conf.
Nginx has a default proxy read timeout of 60 seconds. The Chronos Ledger nginx config sets proxy_read_timeout 3600s on the /ws location. If you see 60-second drops, nginx.conf may not have been updated — verify:
docker compose exec nginx cat /etc/nginx/nginx.conf | grep proxy_read_timeoutAnother service on the host is using port 80. Either stop it (sudo systemctl stop apache2 / nginx) or change the host port in docker-compose.yml:
ports:
- "8080:80" # map host 8080 → container 80Redis is configured with --maxmemory 256mb. PostgreSQL can spike higher during bulk import. Minimum recommended RAM: 1 GB free after OS. On 512 MB machines, reduce Redis:
command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lrudocker compose exec db pg_dump -U chronos_admin chronos_ledger \
| gzip > chronos-backup-$(date +%Y%m%d).sql.gzTo restore:
gunzip -c chronos-backup-20260519.sql.gz \
| docker compose exec -T db psql -U chronos_admin chronos_ledger# Pull the latest docker-compose.prod.yml from the release assets, or:
export VERSION=v1.2.0
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -dChronos Ledger images are built with SBOM and provenance attestation — verify with:
docker buildx imagetools inspect ghcr.io/Life-Experimentalist/chronos-ledger-backend:v1.2.0Check if the CVE affects Chronos Ledger's actual usage (many CVEs in base images are in code paths that are never executed). If it does:
- Open an issue on GitHub with the CVE ID.
- If it's in the base image (
node:20-alpine,python:3.11-slim, ornginx:1.27-alpine), we will update theFROMline once the upstream image is patched. - If it's in a dependency, update via
uv add <package>@<fixed-version>ornpm install <package>@<fixed-version>.
Check the NEXT_PUBLIC_* environment variables in the CI step. The build requires them to be set (even to empty strings) to avoid undefined being baked into the JS bundle.
The CI workflow sets:
NEXT_PUBLIC_API_URL: /api/v1
NEXT_PUBLIC_WS_URL: /ws
NEXT_PUBLIC_VAPID_PUBLIC_KEY: ""
NEXT_PUBLIC_TELEMETRY_ENABLED: "false"
NEXT_PUBLIC_TELEMETRY_ENDPOINT: ""If you have added new NEXT_PUBLIC_ variables, add them to the CI env: block.
The GITHUB_TOKEN needs packages: write permission. This is declared in cd.yml:
permissions:
contents: read
packages: writeIf it still fails, check that the repository is in the Life-Experimentalist organization (not a personal fork). Personal forks cannot push to org packages.
Likely causes:
- Commits are not following Conventional Commits — only
feat:,fix:,perf:, andsecurity:prefixes create release PRs. release-please-config.jsonor.release-please-manifest.jsonis missing or malformed.- The
GITHUB_TOKENpermissions do not includepull-requests: write.
Check release.yml — it declares permissions: { contents: write, pull-requests: write, packages: write }.
- Open the Onboarding Wizard: Admin Dashboard → Setup Guide
- Skip to Step 2 — Create Cycle. Fill in the new semester's start and end dates.
- Move to Step 3 — Import CSV. Upload the new semester's timetable CSV.
- Click Generate Ledger to populate the first day's entries.
The old cycle is preserved in full — historical attendance records and ledger snapshots remain untouched. The new cycle is set as active.
No. The system maintains one active cycle at a time. Switching cycles makes the new one active for ledger generation; historical data remains queryable under the old cycle's ID.
- Prepare a CSV with only the new department's data.
- Import it via Admin Dashboard → Import Data → CSV Import Zone.
- The import is idempotent — existing records are not duplicated; new ones are created.
- Regenerate the ledger to include the new slots in today's schedule.
All data stays on your campus server. Nothing is sent to external services by default except:
- Telemetry (opt-in by default): Anonymous view counts sent to CFlair-Counter. No PII. Disable in Admin Dashboard → Overview → Privacy & Telemetry, or set
NEXT_PUBLIC_TELEMETRY_ENABLED=falseat build time. - Web Push: Push payloads are routed through the browser vendor's push service (Google FCM for Chrome, Mozilla for Firefox). Payload content is a short status string — no student names or sensitive data.
Two options:
Runtime (per-instance): Admin Dashboard → Overview → Privacy & Telemetry → toggle off. Stored in localStorage, persists across sessions for that browser.
Build-time (permanent, applies to all users): Add to your .env before building:
NEXT_PUBLIC_TELEMETRY_ENABLED=false
Then rebuild the frontend image. This removes all telemetry code paths at compile time.
Chronos Ledger is designed for on-premises deployment — the deploying institution is the data controller. There is no built-in automated retention or purge schedule. Administrators are responsible for implementing any required retention policies directly on the PostgreSQL database.