- Python 37.8%
- HTML 30.2%
- JavaScript 20.6%
- CSS 11%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| app | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| prompt.txt | ||
| README.md | ||
| requirements.txt | ||
| run.py | ||
Appointments 📅
A lightweight, self-hosted scheduling and calendar web application.
- Stack: Python 3 (Flask, SQLAlchemy, SQLite), Gunicorn, Vanilla JavaScript, Debian 13 (
trixie-slim). - Container First: Fully containerized with
Dockerfileanddocker-compose.yml. - Zero Frontend Dependencies: No npm, no external JS CDNs or frameworks.
- Vibe-coded: Built with Gemini Flash.
Key Features
-
Self-Hosted Public Calendar & Availability:
- Public-facing booking calendar.
- Admin month-view dashboard with individual slot creation and flexible recurring slot schedules.
- Recurring Slot Unrolling: Recurring availability schedules are unrolled into individual database rows at creation time for complete autonomy over each individual slot.
-
Visitor Timezone Detection & Translation:
- Detects the visitor's local timezone via vanilla JavaScript (
Intl.DateTimeFormat). - Translates all displayed slot times into the visitor's timezone.
- Displays a prominent notification informing the user that timezone translation is occurring.
- Allows visitors to manually change or inspect timezones.
- Detects the visitor's local timezone via vanilla JavaScript (
-
Proof-of-Work (PoW) CAPTCHA:
- In-browser SHA-256 hash-collision solver using the native Web Crypto API (
window.crypto.subtle.digest). - Invisible or near-instant for real human users (~100–300 ms), but computationally expensive for spam bots.
- Zero third-party CAPTCHA cookies or external network requests.
- In-browser SHA-256 hash-collision solver using the native Web Crypto API (
-
Optimistic Concurrency Control (OCC):
- Eliminates double-booking race conditions during simultaneous reservations.
- Implemented via a
versioncolumn and SQLAlchemy OCC (version_id_col). - If two visitors attempt to reserve the same slot concurrently, the second transaction is safely rolled back and rejected with an informative conflict notification.
-
Lazy Evaluation for Pending Reservations:
- Submitting a booking puts the slot into a
pendingstate for 10 minutes and emails a confirmation link to the guest. - If unconfirmed after 10 minutes, the backend automatically treats the slot as available again via lazy evaluation during database queries, without requiring external cron daemons.
- Submitting a booking puts the slot into a
-
iCalendar (.ics) Integration:
- Upon confirmation, both the administrator and the guest receive an email with an
.icsattachment for calendar import. - All
.icsevent timestamps strictly use UTC (Zformat). - Cancellations by either party trigger an
.icscancellation payload (METHOD:CANCEL,STATUS:CANCELLED) to remove the event from calendars.
- Upon confirmation, both the administrator and the guest receive an email with an
-
Cryptographically Secure Booking Management:
- Confirmation emails contain a unique, cryptographically secure token link to cancel or reschedule appointments.
- Rescheduling safely locks the new slot with OCC, releases the previous slot, and dispatches updated
.icsnotifications.
-
Admin-Only Invite Link Tracking:
- Administrators can generate unique invite links with custom descriptions (e.g. "Sent to Client X").
- Visiting via an invite link sets a cookie that expires upon booking or after 2 days (whichever is earlier).
- The invite link attribution is recorded on the booking and is visible exclusively to the administrator.
-
Security & Configuration:
- Passwords hashed with Argon2 (
argon2-cffi). - Flask secret key read from a 32-byte binary file in persistent storage or generated via
secrets.token_bytes(32). - Comprehensive in-app configuration UI for SMTP credentials, application URL, notification recipients, and site branding.
- Passwords hashed with Argon2 (
Data Persistence Definition
The application strictly defines what data needs to persist and what is ephemeral:
Persistent Data (Mounted to ./appointments_data):
appointments.db: SQLite database storing slots, bookings, invite links, and application configuration.secret_key: 32-byte secret key for Flask session signing.initial_otp.txt: Temporary record of the initial administrator one-time password generated on first deploy (deleted automatically when the administrator sets a permanent password).
Ephemeral Data (Inside container only):
- Python runtime, packages, and virtual system files.
- Application codebase and static assets.
- Temporary files and runtime cache.
Quick Start with Docker Compose
-
Clone the repository:
git clone https://git.vitapavlik.cz/vitapavlik/appointments.git cd appointments -
Start the application:
docker compose up --build -d -
Retrieve the One-Time Administrator Password (OTP): On first deployment, the backend outputs an OTP to stdout:
docker compose logs appointmentsNote: A copy is also saved to
./appointments_data/initial_otp.txtand is deleted automatically after your first password change. -
Log in as Administrator:
- Open:
http://localhost:8080/admin/login - Username:
admin - Password:
<your one-time password> - You will be prompted to set a permanent password immediately.
- Open:
-
Configure SMTP & URLs:
- Navigate to
/admin/settings. - Set your Application URL (e.g.,
http://localhost:8080or your reverse proxy domain). - Enter your SMTP Server Credentials to enable automated email confirmations and
.icsattachments. - Use the Send Test Email tool to verify your connection.
- Navigate to
-
Create Availability:
- Visit
/admin/recurringto generate recurring weekly time slots unrolled into individual records. - Or click Add Single Slot directly on the dashboard.
- Visit
Reverse Proxy Setup (e.g. Nginx, Caddy, Traefik)
The container serves pure HTTP over Gunicorn on internal port 8000 (mapped to host port 8080 by default):
server {
listen 80;
server_name appointments.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
License & Credits
- Vibe-coded with Gemini Flash.
- Source code: https://git.vitapavlik.cz/vitapavlik/appointments