More Projects
59 in total
MediLink
Contributor · 2024
Doctor-appointment site with a patient front end, an admin dashboard and a separate chatbot service. The MERN core handles users, appointments and messages with JWT auth and Cloudinary uploads; the chatbot is a Flask app that tokenises input with NLTK and maps symptoms to conditions and treatments from CSV tables, writing bookings back to CSV.
Project Details
Contributor
2024
Product · Web
Two systems live in this repo that do not know about each other. One is a conventional MERN appointment site with a patient app and an admin dashboard. The other is a Flask service that turns a sentence like "I have had a fever for six days" into a condition, a treatment and a booked slot, using nothing but token overlap and four CSV files.
That split is the interesting thing about the project, because both halves are solving the same problem — get a patient in front of a doctor at a time that is free — and they reach opposite conclusions about where the truth lives. The MERN side puts it in a database with schema validation, role checks and unique constraints. The chatbot puts it in a file it appends to. One of those two still parses.
What the site does
- Three roles in one user model — Admin, Doctor, Patient — with the password field excluded from
queries by default and bcrypt hashing on save, so a controller has to opt in with
.select("+password")before it can even attempt a comparison. - Role-partitioned sessions. The cookie is named by role,
adminTokenorpatientToken, behind two separate guards that verify the token and then re-check the role, so a patient session is unusable on an admin route even when the JWT is valid. Logout works by overwriting the correctly named cookie with an expiry in the past. - Booking that refuses to guess. Doctors resolve by first name, last name and department; zero matches is a 404 and more than one match errors out with a conflict message rather than picking the first. The resolved id and the authenticated patient's id are what get written, not the names.
- Validation that lives in the schema. The appointment model enforces a ten-digit phone, a twelve-digit national ID, a validated email and a status confined to Pending, Accepted or Rejected; the user model carries the same fields with most of the length checks commented out, which is a visible seam between what was designed and what was demoed.
- Cloudinary avatars behind a png/jpeg/webp mimetype allowlist, uploaded from a temp file rather than buffered in memory.
- Two Vite SPAs against one API. The patient app has home, appointment, about, register and login
routes; the dashboard has an overview, doctor and admin creation forms, a doctor list and a message
inbox. Both bootstrap by calling
/user/patient/meor/user/admin/meon mount and treating a failure as logged-out, so the role split reaches all the way to which endpoint the client asks. - A public contact endpoint feeding that admin inbox — the only unauthenticated write in the API.
- A symptom chatbot with staged conversation state, a 24-symptom vocabulary mapping to 79 conditions, and a separate treatment table.
- Slot booking from the chatbot: 13 hourly slots from 08:00 to 20:00, taken times subtracted, past times rejected, nearest-slot suggestion when the request is gone.
The API surface
Three routers under /api/v1. user carries patient registration, login, logout for each role, the
two me endpoints, a public doctor list, and admin-only creation of admins and doctors.
appointment has a patient-only post, and admin-only getall, status update and delete. message has
a public send and an admin-only getall. Every admin route sits behind the same guard, so the
authorisation story is one middleware read rather than a scan of nine controllers.
How the chatbot actually decides
There is no model. NLTK downloads punkt and stopwords at import, the input is lowercased, tokenised and stripped of stopwords, and each remaining token is checked against the tokenised form of every symptom label. Matched symptoms pull their candidate conditions from the CSV and the answer is the mode of that list. Duration drives the recommendation: five days or more and it says see a doctor.
The consequences are visible in the data. Matching is single-token, so "back pain" also fires Chest
Pain, Joint Pain and Abdominal Pain. There is no negation handling, so "I do not have a fever"
matches Fever. The symptom table names 79 conditions while the treatment table covers 61, so the
rest fall through to a generic line. Symptoms accumulate on the session and are never cleared, so a
long conversation drifts toward whichever condition appears most often across everything mentioned.
Every booking goes to the first doctor in the file regardless of specialisation, with a comment in
the source saying so — and the file's other two doctors carry their own availability windows that
nothing reads. The clinic name and campus in the confirmation text are hardcoded to somewhere else
entirely, which is the tell that this half started as a template. requirements.txt pulls torch and
transformers; neither is imported.
Where it breaks, honestly
The chatbot appends bookings to appointments.csv. The field list in code has eight columns, the
header on disk has six and no trailing newline, so the first appended row fused onto the header and
the file no longer parses — the two rows sitting in it have the doctor's name where the title should
be and the string yes where the date should be. The in-memory copy is read once at import, so
slots booked during the process lifetime are not excluded from the next check, and two concurrent
requests will both see the same slot free and both append.
Every one of those is a thing a database gives you for free. A column count that cannot silently drift, because the schema is declared once and enforced on write. A uniqueness constraint that makes double-booking a failed insert rather than a second row. A read that sees what the last write did. That is the argument for the database the rest of the project already had: the chatbot was never wired to Mongo, so its bookings never reach the appointment collection and web bookings are invisible to it. The same slot can be sold twice by the two halves of the same product, and neither one is wrong from where it is standing.
The rest of the honest list is smaller. CORS is open to * with credentials explicitly disabled to
make that legal, which means the cookie-based auth only works because both SPAs run on the same
origin in development; the API base URL is hardcoded to localhost:8000 in the components rather
than read from the config file that exists for exactly that. Flask runs with debug=True.
This was a team build and I contributed rather than owned it. What is worth taking from it is that CSV failure — the clearest small demonstration of why append-only flat files with no schema, no locking and no transaction boundary stop working the moment two things write.
Project Details
Contributor
2024
Product · Web