Synaplan
Open-source AI platform for companies: self-hosted AI agents, GDPR-compliant chatbot widget, RAG knowledge base, WhatsApp and Outlook AI, MCP, multi-model routing. Docker, Kubernetes or hosted in Germ
Client, provider and admin dashboards over one slot engine, with email that is sent exactly once and a guest booking flow that needs no account.
BusinessPython · JavaScriptGitHub @luke-zhang-cs-pyOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Almanac - multi-role appointment booking. Flask + JWT, live availability from recurring hours, automatic email notifications, and coffee-chat invites that need no account.
Taken from the repository README
Loading the full README.
Read the full README ↗Booking, from both directions: clients book a provider's free slots, and providers can email someone an invite that turns into a booking without the guest ever making an account.
Set weekly hours, add a block or a booking, pick a date — and the board recomputes in the tab. Each example shows the engine's own arithmetic: how many windows it read, how many slots it tiled, how many it dropped and why.
This is the slot engine only, not the platform — JWT auth, a relational
database, transactional email and a background reminder thread genuinely can't
run in a browser tab, so none of them are mimed. js/slots.js is a port of
domain/calendar_logic.py, and tools/build_static.py runs the two against
each other in a real browser over 21 scenarios before publishing, refusing to
write a single file if they disagree about one slot.
Read the full write-up → — what a free slot has to survive, how five kinds of email get sent exactly once, and every bug this has had. There's also an architecture map.
pip install -r requirements.txt
python seed_data.py # creates [email protected] / admin12345
python app.py # http://127.0.0.1:5003
No .env needed to start — SQLite and a dev JWT secret by default. It binds
127.0.0.1; set HOST=0.0.0.0 only when you mean it. Running with
FLASK_DEBUG=0 and no real SECRET_KEY is refused outright, because the dev
secret is committed and therefore public, and it signs every session token.
Switching to managed Postgres (Supabase, Neon, Render, RDS) is one environment
variable, DATABASE_URL. No code changes.
The root holds the things you run; everything you import lives in a package
named for its layer. The arrows only point one way —
core ← accounts/domain ← notify ← routes ← app.py — so an import
pointing back up is a cycle and says so immediately.
app.py Flask app factory, blueprint registration
core/ config.py, database.py settings; SQLite or Postgres
accounts/ auth.py JWT creation, RBAC decorators
domain/ calendar_logic.py the free-slot engine
coffee_chats.py, offerings.py,
pounds.py, schedule.py the rules: no HTTP, no email
notify/ mailer.py, notifications.py, transport, occasions, layout,
coffee_notifications.py, and the one background timer
email_render.py, scheduler.py
routes/ nine blueprints, one per area of the API
docs/app/ the slot engine, ported to JS and published on its own
tools/ build_static.py, build_planner.py, refresh_figures.py
app.py stays at the root because Flask(__name__) resolves templates/ and
static/ relative to its own directory. login_page.py, seed_data.py and
seed_luke.py stay because they're run as scripts — which puts their
directory on sys.path, not the project's.
python tools/build_planner.py --blank --out calendar.html
Blank, then yours. Nothing is seeded and nothing leaves the browser — the entries above were typed into the page while the frames were captured.
The planner is one self-contained HTML file you double-click — no server, no
assets, everything kept in that browser. It ships two ways. planner.html is
seeded with an invented term, which shows what it does and is the wrong place
to start a real one from: you'd be deleting somebody else's lectures first.
calendar.html is the same engine with nothing in
it — no term, no diary, no fixtures, no to-dos, no countdown.
Your own timetable is not in this repository and should not be. A term
timetable says where somebody is at every hour of every weekday; .gitignore
blocks the real seed and tests/test_planner.py fails if one appears beside
the sample. Build yours with --data and keep it outside the repo.
Where the full-stack half comes in. A file:// page can show a
notification while its tab is open and nothing at all once it's closed — it
can't register a service worker, so there's no background to run in. That's
the gap Almanac closes: export a backup from the calendar, import it, and the
server's sweep emails you before each event whether or not anything is open.
domain/schedule.py owns that, keyed on the planner's own event ids so
re-importing updates rather than duplicates. A lecture is deliberately not
an appointment — no second party, no provider to notify, no slot to hold — so
it lives in its own table with no booking semantics.
A free slot has to survive a lot. domain/calendar_logic.py turns
recurring weekly hours, one-off blocks and existing bookings into bookable
times — dropping anything that overlaps something busy at all (not just an
exact match), anything already past on today's date, and anything too short for
the requested session. A partial-unique index makes race-condition
double-booking impossible at the database level rather than in application code.
Five kinds of email, each sent exactly once. Welcome, confirmation,
cancellation, completion and a 24-hour reminder, all without anyone pressing a
button. Delivery runs on a background thread so a slow mail server never slows
a booking; every attempt is recorded in an email_log; and a unique index over
that log is what guarantees nobody is mailed the same thing twice — the
guarantee lives in the schema, not in a code path that could be missed.
Coffee chats run the other way. An email that produces a booking: send an invite, they click, they see real availability, they pick a time — with no account. A coffee chat is usually first contact, and asking an alum to register before picking a slot loses most of them. Follow-ups go out after three days, capped at two, because a third is pestering.
Money is an integer in minor units, and 0 means free — a real answer, not
a missing one.
pytest -q --cov --cov-branch --cov-fail-under=100
629 tests, 100% of 2,708 statements, and of every branch, over every module in
the repository -- the seed scripts, the standalone login page and tools/
included. CI fails a push below that. 14 of those guard the published slot-engine
demo: every copied file byte-identical to its source, no fetch() anywhere so
file:// keeps working, and every clause of the "this is not the platform"
banner still present.
The build's own check is the other half, and it's the one that can't run in CI:
it needs a real browser. (tests/test_build_static.py runs the build's logic in
CI against a stand-in browser -- the comparison, the refusals, the sabotage
list -- but only a real one can say the port agrees.) python tools/build_static.py --prove sabotages the
published port twelve ways — shifting every weekday by one, treating a
whole-day block as zero length, dropping the provider_id filter — and
requires each to be caught.
MIT — see CONTRIBUTING.md for setup and conventions.