OdontoX v1.8 Stable — internal release notes
Audience: OdontoX engineering + ops. Window covered: 2026-05-22 → 2026-06-01 (~40 entries inRELEASES.md).
Login tag at cut: v1.8 (ui/src/components/ui/sign-in.tsx APP_VERSION).
Public name: v1.8.
Deploy targets: odontox-server Worker (production env), odontox-app Cloudflare Pages canonical, marketplace-app Cloudflare Pages.
Database: odontox-prod Neon project, schema app.
This document mirrors the public doc plus full technical depth. Sections marked “Superadmin” are internal-only and must never be cross-posted to q.odontox.io or any clinic-visible surface.
What shipped in v1.8 — feature headlines
New features- Ruby Reception Cockpit — Day Brief, per-appointment Prep Hints, End-of-Day Wrap-Up (§2)
- Ruby Insights reorganised into Plan ahead / Collect / Recover, shared per-clinic cache, 15/day limit (§2, §3)
- Payment reminders — Email + WhatsApp send, edit-before-send, send history, 3/day cap (§3)
- Owner dashboard rebuilt — dental KPIs, Today’s Schedule worklist, Action Required panel, Provider Performance (§3)
- Personalised dashboard — drag / resize / hide widgets + redesigned Revenue chart (§3)
- Todos planner — task fields, assignees, tags, voice dictation, two-pane bucketed planner (§3)
- Calendar v2 — redesigned Day/Week/Month, zoom, utilization bars + heat strips, smart overflow (§3)
- Visiting doctors + unassigned bookings + reassignment (§3)
- WhatsApp for doctors & receptionists; automations editor; quotation/receipt attachments (§3)
- Service Catalog procedure picker; per-clinic per-role module visibility (§3)
- Sidebar order + one-click presets (Front Desk / Chairside / Billing); appearance Live Preview (§3)
- Analytics DB read-replica routing — reporting/dashboard/stats reads offloaded to the
ep-icy-toothread replica viagetAnalyticsDb+ANALYTICS_DATABASE_URL(configured on prod) - App-wide caching migration (~13 modules + patient tabs + finance lists) — instant revisits, no skeleton flashing
- Client-side navigation rewrite — full-page reloads eliminated; app-wide context providers memoised
- Initial bundle ~1.03 MB → 807 KB (PostHog removed, Sentry lazy, framer-motion dropped); tenant files edge-cached
- Ruby per-clinic insight cache + the v4-flash model move (lower cost, 1M context)
deepseek-v4-flash.
1. Header — scope and deploy artifacts
- Branches merged: all v1.8 work on
main. No long-lived feature branches outstanding. - Versioning: v1.8 = +0.1 from the latest published GitHub release (v1.7). Login
APP_VERSIONset tov1.8this cut. - Worker version IDs: see Cloudflare dashboard → odontox-server → Deployments. Backend deploys were unblocked mid-window by provisioning the
BOOKING_RATE_LIMITERnamespace IDs (see §9). - Pages deploy IDs:
odontox-appcanonical promotion is enforced after every deploy perfeedback_commit_deploy.marketplace-appunchanged this window. - Bridge: no Bridge release in this window.
2. Headline milestone — Ruby Reception Cockpit + the shared AI insight-cache
The marquee of v1.8 is Ruby crossing from “an insights page admins open” to “a deterministic-first assistant woven into the front desk,” backed by a real server-side cache and a production model upgrade.Deterministic-first architecture
SQL decides who/what/when; Ruby only narrates. A new no-show risk score (0–100) combines per-patient 90-day miss rate (40%), lead time from booking (25%), first-visit flag (15%), outstanding balance (10%), and late arrivals (10%). It feeds both the Day Brief and per-appointment Prep Hints. Ruby never selects who to flag — the score does — which kills the class of “the LLM made up a list” bugs.Reception cockpit surfaces
- Day Brief —
GET /reception/day-brief,POST /reception/day-brief/refresh. Greets by name, surfaces confirm/collect/no-show actions, hides when nothing is actionable. - Prep Hints —
POST /reception/prep-hints. One-line per-appointment hint (missing doctor, first-visit consent, balance, no-show risk); routine rows render an em-dash. - EOD Wrap-Up —
GET /reception/eod,POST /reception/eod/close-confirmed. Ruby-summarised checklist of leftovers with one-click batch close. The reconcile guards against closing doctor-less rows (see §3 Receptionists).
DayBriefCard, PrepHintBadge, EodReconcileModal under ui/src/components/receptionist/; all quiet-fail on 403 (ai_insights off) or network error so the dashboard never breaks because Ruby is down. 9 reception unit/route tests.
Model upgrade — deepseek-v4-flash
DEEPSEEK_MODEL flipped at all env blocks in server/wrangler.toml. Input cost 0.14/M (cache miss) and 0.0028/M (cache hit, ~25× on the stable system prefix); context 128K → 1M. DICOM analysis untouched (separate GPT path). All 16 existing Langfuse prompts re-pushed with the v4 design-principles header (no body changes); 3 new reception prompts authored at production label (reception-day-brief, reception-prep-hint, reception-eod-reconcile).
Shared insight-cache layer
Newserver/src/lib/ai-cache.ts. ensureAiInsightCacheSchema() lazily creates app.ai_insight_cache (id, clinic_id, kind, data text, trace_id, computed_at, expires_at) with a (clinic_id, kind) unique index — no migration. getAiCache<T> treats expired rows as misses; setAiCache UPSERTs with computeAiCacheExpiry() = min(now + 24h, pktEndOfDay(now)). Cache is best-effort: read errors fall through to regenerate, write errors log but don’t fail the response. Four shared kinds (daily_brief, revenue_forecast, followups, churn_risk) plus per-invoice templated payment_reminder:<id> rows live on the same table. localStorage stays as a first-paint hint, overwritten within ~200 ms.
This makes the page shared per-clinic (first opener of the day pays the DeepSeek cost; everyone else gets the cached payload), which is what enabled moving the client rate limit from 3/hr to 15/day with page loads excluded (useAIRateLimit: MAX_USES 3→15, WINDOW_MS 1h→24h).
Why this matters operationally
Two admins on the same clinic no longer each burn quota opening the same insights, the front desk gets a daily action list grounded in real DB counts, and the cost ceiling drops sharply via the v4 cache-hit discount on a byte-stable system prompt.3. All public headlines, plus extra technical detail
Same headlines as the public doc, role-organised, with internal additions and the fixes that shipped alongside.Clinic Owners & Admins
Owner dashboard redesign (2026-05-31). Five dental KPIs (Today’s Appointments + status breakdown, Today’s Collections + outstanding, Chair Utilization, No-show Rate, Treatment Acceptance), chair-side Today’s Schedule worklist, an Action Required triage panel (missed appts, pending payments, overdue recalls, unaccepted plans, lab due, low stock — each deep-linked), Revenue by Treatment, Weekly Load, and Provider Performance (multi-dentist). Powered by a single additive, KV-cached (5-min)/stats/admin; shell+KPIs render instantly, the one Recharts trend chart lazy-loads, category/weekly bars are pure CSS. Chair Utilization = active chairs × opening hours; Treatment Acceptance = plan status over trailing 90d; Revenue by Treatment = per-clinic procedure categories billed this month.
Dashboard customisation (2026-05-31). @dnd-kit on the existing 12-column grid: drag, resize (half/full-width span presets), hide/show from a tray, reset to default; persisted per-user in localStorage (no DB). Drag surface + charting lib both lazy-loaded. Redesigned Revenue Overview chart (gradient fill, highlighted latest month, cleaner tooltip).
Ruby Insights reorganised (2026-05-26). AIInsightsAdminView rebuilt from tabs into Plan ahead / Collect / Recover sections with count badges; header rebranded “Ruby Insights” with a live status line. New SectionHeader/SubSectionHeader/SectionError helpers; sub-content components and data hooks unchanged. Stat-card row removed (counts now in section badges).
Payment reminders (2026-05-26). Per-channel Email/WhatsApp send with edit-before-send (EmailEditPanel/WhatsAppEditPanel). New app.payment_reminder_sends (lazy ensurePaymentReminderSendsSchema, indexed (invoice_id, sent_at DESC) + (clinic_id, sent_at DESC)). 24h, 3-per-channel resend cap enforced server-side (429 on exceed); UI badges + greyed buttons are belt-and-suspenders. Per-invoice generation cached (payment_reminder:<id>); routine Generate is a cache hit and costs nothing, explicit Regenerate spends 1 of 15. Agent tuned: maxTokens 512→280, temp 0.4→0.2, 2–3 sentence body. Fixes: Generate Reminder 400 (frontend sent amountOwed/hardcoded clinicName:"Clinic"; server now accepts { invoiceId } and derives the rest); reminder emails addressing patients by encrypted base64 (now decryptPatientPHI first); second-row Generate killing the first row’s spinner (generatingId singleton → Set); revenue-forecast prompt gained duplicate-invoice / single-patient-concentration / procedure-mix guards (the ssh & Associates six-duplicate-Rs3,000 case).
Sidebar order + presets (2026-06-01). Persisted per-user server-side via GET/PUT /sidebar-order (idempotent table on the writable primary, no migration); localStorage stays as instant-paint, DB is cross-device source of truth, hydrated once/session with debounced writes. One-click presets (Front Desk / Chairside / Billing / Reset) reorder only what plan+role already show (within-group only).
Appearance Live Preview (2026-06-01). Renders the selected scheme on a miniature real-app mock (sidebar with active row, summary cards, chart, highlighted list row).
Notifications (2026-05-31). “Saved” toast on every matrix toggle / bulk silence / reset. Fix: unchecked cells reverting after hard refresh — the persisted client cache hydrated before the refetch; saves now write the new state straight into the cache. EOD report email defaulted OFF (2026-05-23): eod-email-report handler COALESCE(emailMatrix.eod.report.admins, true) → false, mirrored in notification-defaults; audit showed zero clinics had it set explicitly, so the code flip alone restores OFF.
Staff permissions dialog shows the real effective values (2026-05-23). Settings → Staff → permissions now fetches the effective map from the server (GET /staff/:id/permissions → { permissions, defaults, effective, role, planTier }) instead of recomputing from a UI-side table that had drifted — toggles now match exactly what the runtime gate enforces. Website Leads keys (leads.view/manage/convert) were missing from the tree and are now exposed. New permissions-ui-parity.test.ts fails the build (with the exact diff) if the UI defaults ever drift from the server’s getDefaultsForPlan again.
Doctors
WhatsApp access (2026-05-31). Defaults addcomms.whatsapp.module.access + comms.whatsapp.templates.send for doctors across Free/Pro/Pro+. Doctor patient-scoping unchanged (own appointment book only).
Todos planner (2026-05-30 → 05-31). app.doctor_todos + title, assigned_to, tags (migration 0056). POST/PATCH /doctor-todos accept the new fields (admins edit any clinic row, doctors scoped to own; body-only legacy shape still valid, falls back to body-as-title). TodoFormDialog with voice dictation (EN/UR) + assignee picker; two-pane planner (time buckets + week calendar with due-date chips); TanStack Query with cross-tab invalidation. Fix: prescription “Save and export PDF” crash (undefined reference).
Instant module caching (2026-05-31). 11 more modules moved from per-mount useEffect to first-load-gated TanStack reads with mutation-invalidation: Prescriptions, Insurance, Reports, doctor Dashboard, Clinical Notes, Files/DICOM, IPD, Installments, Referrals, Daily Close (+ supporting query keys). Viewers/uploads/rendering untouched (fetch-layer only).
Receptionists (Calendar v2 + cockpit — 2026-05-22 / 05-23 / 05-26)
Calendar v2 (2026-05-22). Redesigned Day/Week/Month; per-user Day zoom (60/90/120/160 px/hr, persisted per role); Week day-header booked-vs-operating bar; Month utilization ring + hourly heat strip; smart overflow “+N at HH:00” popover; empty-day CTAs. NewuseDayIndex (indexes events by YYYY-MM-DD and hour once/render, kills per-cell filter loops) and useDayLayout (deterministic overlap clustering). Fixes: Week/Month empty on busy clinics — endpoint cap 100→2000 (default 50→500), client requests limit=2000 and recursively splits; two-month weeks dropping events (lookup keyed on currentDate month).
Procedure picker from Service Catalog (2026-05-23). New slim GET /procedures/picker (returns {id, procedureName, category, defaultPrice, usageCount}, ordered usage_count DESC, capped 500, 15-min TanStack cache, invalidated by qk.services.all()). ProcedurePicker.tsx (popover + search + grouped top-5/flat-top-30). Fixes: catalog rows illegible in dark mode (low-opacity teal → white in dark); new-service Internal Cost defaults to 0.
Reception cockpit + Ruby honesty (2026-05-23 / 05-26). Day Brief/Prep/EOD per §2. /appointment-nudges gains authoritative summary counts + missedYesterday; UI tiles read summary, nudges[].count only badges items; new missed_followup nudge type. Fixes: tile counts drifting from the visible list when categories overflowed LIMIT; stale 30-min localStorage on nudges → 5 min; receptionist AI items now deep-link (inventory low/expiring filters, multi-patient inline expansion, overdue-patient profile route that was a 404). EOD reconcile guard: closeConfirmedBatch → { updated, skippedNoDoctor }; UPDATE adds doctor_id IS NOT NULL; tri-state toast; generateReceptionEod payload gains unclosedWithoutDoctor.
Receptionist dashboard structure (2026-05-23). Four titled sections (At a glance / Needs attention / Quick actions / Today’s schedule); appointments paginate 15/page; “Ruby Insights” replaces “AI Action Items” with the Ruby mark (OdontoXAIIcon).
Visiting doctors, unassigned bookings & reassignment (2026-05-23). A major scheduling addition (migration 0052: users.account_type login|visiting + specialty).
- Visiting doctors — added without login (Settings → Staff → “Add visiting doctor”,
POST /staff/visiting-doctorbehindstaff.create.visiting): creates ausersrow withaccount_type='visiting',password_hash=null,is_onboarded=true, plus a doctoruser_clinic_assignmentsrow; no invitation email; sign-in short-circuits visiting accounts with a generic 401 (no email enumeration). They appear in every selector via the sharedDoctorPickerwith a “Visiting” badge;safeUserColumnsexposesaccountTypeon/clinic-users. - Unassigned bookings stack on a slot (the existing
if (doctorId)guard already alloweddoctor_id = NULL); an amber “Assign doctor” badge shows on calendar cards, queue rows, detail header, and right rail.PATCH /appointments/:id/assign-doctorruns full conflict detection (409 on clash) and writes anappointment.assign_doctoraudit entry{ from, to }. The Day view gains an “Unassigned” lane that renders only under the “All doctors” filter.scheduled → in_progressis rejected whiledoctor_idis null (UNASSIGNED_APPOINTMENT400; “Start appointment” greyed with a tooltip). - Tests: 31 new —
clinic-modulesrole-template merge,unassigned-bookingstacking,appointments-status-guard,appointments-assign-doctor(success + 409 + audit),staff(visiting create / 409 dup / receptionist with-without permission),auth(visiting sign-in rejection). Permission tree addsstaff.create.visiting(default true for admin + receptionist). API docs updated.
/clinic-modules/active now merges clinic_permission_templates[role].permissions as a fourth layer (order: ALWAYS_ON → clinic module rows → receptionist hardcoded blocklist → role template → per-user assignment override). Setting e.g. finance:false for receptionist on one clinic hides the Finance section, dashboard card, “View Invoice” buttons, and the appointment Billing tab (trigger and body, so URL manipulation can’t surface it) for receptionists at that clinic only — admins/doctors there and receptionists elsewhere are unaffected.
Internal staff/patient chat hub retired (2026-05-23). Removed in favour of WhatsApp v2 (the supported patient-messaging surface): the receptionist “Messages” quick-action card, the patient-detail “Message” button, the unread-toast “View Inbox” action, and the stale ?chat=1 URL strip handler are all gone. WhatsApp v2 untouched.
WhatsApp automations + chat (2026-05-26). Dispatcher fetches the approved Meta template, substitutes placeholders, logs the rendered body in-thread; routes BODY/button_N/header params; caches approved components 5 min per clinicId:name:language. Automations editor: APPROVED-only + category grouping, typed SlotDef extraction across body/header/URL-button, per-slot variable picker (body vs URL-friendly), full-template preview, save blocked while any slot unmapped; SUPPORTED_VARS 13→22 with slot/dependsOn. automation-templates.ts default-mapping fixes + 2 new entries so all 11 canonical triggers have a binding; recentAppointmentType resolver added. Chat sidebar overhaul: real balance math, working Book/Invoice/Call deep-links, avatar plinth, accurate window-state banner. Fixes: request_received no-op (missing-var → firstName+clinicName); appointment_scheduled missing date/time slots; light-mode thread dark-on-dark (--wa-thread-bg → theme-aware class); standalone /whatsapp redirects into the dashboard shell.
Reliable document attachments (2026-05-29). Browser renders via PdfExportService.renderPdf; POST /messages/attachments/stage mints a 15-min presigned R2 PUT (browser PUTs bytes directly, never through the Worker); send step pulls via the R2_STORAGE binding and sends by mediaId (never link). RecentInvoicesCard + new RecentQuotationsCard/RecentReceiptsCard. Authenticated stream-through GET /messages/attachments/:keyEnc for history previews (binding read, not a 302, to avoid bucket CORS). Fix: the red-triangle delivery class — Meta no longer fetches our URL.
Patients & Portal users
- Patient detail tabs (Overview/Appointments/Payments/Treatments) cached (first-load-gated TanStack); finance lists keep rows on screen during background refresh; Revenue Overview refreshes on payment/invoice/quotation via a shared finance-summary key.
- Document previews/downloads warm the PDF engine in the background (app + portal share links); custom-letterhead prescriptions reuse a cached letterhead; odontogram PDF renders as vectors (no ~1 s screenshot freeze).
- Tenant files (X-rays, scanned forms, signed invoices) hit the Cloudflare edge cache after first view (see §4.1).
- Module-load recovery: a stalled screen (typically right after a deploy) now shows “Updating to the latest version…” and refreshes once instead of an endless spinner; the refresh-loop guard was corrected so it can’t block the very refresh needed to recover, and stale URL params are stripped on section change.
- Clinic logos load-failure state is tracked per-image-URL rather than latched globally, so a one-off blip no longer drops branding to a text fallback for the rest of the session (the latch was exposed by the no-full-reload navigation change).
App-shell navigation (2026-05-31 → 06-01)
Standalone-page sidebar handler and several in-app links moved fromwindow.location.href (full reload) to React Router client-side navigation, so the in-memory query cache survives module switches. Impersonation, clinic-switch, and auth/logout intentionally keep a hard reload to re-establish identity. Six app-wide context provider values (clinic events, breadcrumbs, branding, theme, notifications, user profile) memoised to stop a full-dashboard re-render on every live clinic event. Startup trimmed (login shader + emoji picker lazy; an unused animation lib removed).
4. Performance, scale & data-flow
Performance and database scale were a primary focus of v1.8. The headline pieces:4.0 The v1.8 speed & DB-scale pass
- Analytics DB read-replica routing (2026-05-31). New
getAnalyticsDbread-routing layer for the reporting / analytics / dashboard-stats endpoints. Heavy reporting reads are routed to a Neon read replica (ANALYTICS_DATABASE_URL, pointed at theep-icy-toothreplica on production) so they no longer contend with live transactional traffic on the writable primary. Falls back to the primary when the secret is unset — behaviour is identical until configured, and it is configured on prod. No schema change. (When verifying analytics correctness against the replica, confirm a known revenue figure, since the replica can lag the primary slightly.) - App-wide caching migration. ~13 modules moved from per-mount
useEffectfetches to first-load-gated TanStack Query reads with mutation-invalidation: PatientDetails tabs (Overview/Appointments/Payments/Treatments), the finance lists (Invoice/Receipts/Quotation/Revenue, with a shared finance-summary key so cross-module money changes stay consistent), Prescriptions, Insurance, Reports, the doctor Dashboard, Clinical Notes, Files/DICOM, IPD, Installments, Referrals, and Daily Close. Revisits are instant; background refreshes keep current rows on screen instead of flashing skeletons. - Client-side navigation rewrite. Standalone-page sidebar handler and several in-app links moved from
window.location.href(full reload) to React Router navigation, so the in-memory query cache survives module switches. Impersonation, clinic-switch, and auth/logout intentionally keep a hard reload to re-establish identity. Six app-wide context provider values (clinic events, breadcrumbs, branding, theme, notifications, user profile) memoised to stop a full-dashboard re-render on every live clinic event. - Bundle & startup. Initial JS ~1.03 MB → 807 KB raw (315 → 240 KB gzip): PostHog removed entirely (
ph.odontox.iodropped from CSP), Sentry made lazy off the entry chunk (idle-init frommain.tsx),ScrollToToprewritten without framer-motion, dead demo files +@hugeicons/*/@number-flow/reactremoved. Login shader + emoji picker lazy-loaded. - Edge caching for tenant files.
tenants.odontox.io(X-rays, scanned forms, signed invoices) served from the Cloudflare edge cache after first view — no Worker invocation and no R2 read on repeat views (see §4.1). - Egress-aware endpoints. The new
GET /procedures/pickerreturns a minimal column set capped at 500 rows and cached 15 min per clinic, keeping Neon egress predictable when many sessions open the booking dialog. Ruby’s per-clinic insight cache (§2) means the first opener pays the DeepSeek cost and DB queries once per day, not per admin per page load. - Document & settings speed. Settings load only the active tab (no fan-out of panel requests; Billing/Permissions/Audit cached); the PDF engine warms in the background (app + portal); the odontogram PDF renders as vectors instead of a screenshot.
4.1 Edge caching for tenant files
server/src/worker.ts:handleR2Request for tenants.odontox.io checks caches.default first; on miss the R2 body is tee()’d (client gets bytes while the cached copy is written in ctx.waitUntil). Cache key is origin-neutral (CORS layered at response time) so one entry serves every clinic origin. Cache-Control: public, max-age=31536000, immutable preserved.
4.2 Ruby read path (cache-first)
4.3 WhatsApp document attachment (browser-render → R2 → Meta)
4.4 Analytics read routing
getAnalyticsDb routes reporting/analytics/dashboard-stats reads to ANALYTICS_DATABASE_URL when set, else falls back to the primary — identical behaviour until the replica secret is configured.
5. Schema changes
Migrations
- 0052 —
app.users+account_type(text NOT NULL default'login', CHECKlogin|visiting) +specialty(text null) + indexusers_account_type_idx. Idempotent (IF NOT EXISTS).phonepre-existed. - 0056 —
app.doctor_todos+title,assigned_to,tags. Existing body-only rows stay valid.
Lazy-ensured tables (no migration step)
app.ai_insight_cache—ensureAiInsightCacheSchema(), unique(clinic_id, kind).app.payment_reminder_sends—ensurePaymentReminderSendsSchema(),{ clinic_id, invoice_id, channel, sent_by, sent_to, body, status, error_message, sent_at }.- sidebar-order store — idempotent CREATE on the writable primary, per-user JSON.
server/src/schema/payment_reminder_sends.ts (re-exported from schema/index.ts); doctor_todos extended.
6. New backend modules / files (significant)
server/src/lib/ai-cache.ts— shared insight cache.server/src/routes/reception/*— day-brief, prep-hints, eod (+ refresh/close-confirmed).server/src/lib/invoice-pdf.ts—renderInvoicePdfByToken(db, token); shared by/public-documents/:token/pdfand the chat attachment resolver.server/src/lib/r2.ts—getSignedPutUrl/getSignedGetUrl/generateChatAttachmentKey.ui/src/components/appointments/ProcedurePicker.tsx,DoctorPicker.tsx;ui/src/components/receptionist/{DayBriefCard,PrepHintBadge,EodReconcileModal}.tsx;ui/src/components/todos/*(planner) +TodoFormDialog.scripts/whatsapp/—templates.json(11 canonical payloads),submit-templates.sh,test-fire-all.sh,backfill-template-renders.sql;scripts/deploy-cloudflare.sh.
7. API surface changes (selected)
GET/PUT /api/v1/protected/sidebar-orderGET /reception/day-brief,POST /reception/day-brief/refresh,POST /reception/prep-hints,GET /reception/eod,POST /reception/eod/close-confirmedPOST /payment-reminder/send-whatsapp,GET /payment-reminder/channels,GET /payment-reminder/history;POST /payment-reminderreduced to{ invoiceId }GET /public-documents/:token/pdf(invoice PDF; other types 415)POST /messages/attachments/stage,GET /messages/attachments/:keyEncPOST /staff/visiting-doctor(requirePermission('staff.create.visiting'), 409 on dup),PATCH /appointments/:id/assign-doctor(409 on conflict + audit),GET /staff/:id/permissions→{ permissions, defaults, effective, role, planTier }GET /procedures/picker;/stats/adminadditive dental fields;/appointment-nudges+summary/missedYesterday/clinic-modules/activenow mergesclinic_permission_templates[role](5-layer order: ALWAYS_ON → module rows → receptionist blocklist → role template → per-user override)- API docs updated in
docs/api-reference.md(new endpoints,UNASSIGNED_APPOINTMENTerror, merge order).
8. Cron jobs added / changed
eod-email-reporteligibility flipped to opt-in (COALESCE(..., false)) — only clinics with explicittruematch.- No new cron schedules this window (marketplace crons shipped in v1.7).
9. Configuration / env vars
DEEPSEEK_MODEL=deepseek-v4-flashat allserver/wrangler.tomlenv blocks.- New Worker secrets:
R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_ENDPOINT(presigner, scoped toodontox-files-prod). - Rate-limiter bindings:
BOOKING_RATE_LIMITERnamespace IDs assigned (prod1003, UAT1004) — placeholders had been 10021-erroring every prod deploy.MY_RATE_LIMITER→GLOBAL_RATE_LIMITERacross the 3wrangler.tomlblocks +api.ts/middleware/rate-limit.ts(counter keyed by namespace_id, so cosmetic). ANALYTICS_DATABASE_URLconsumed bygetAnalyticsDb(optional; falls back to primary).- Removed: PostHog entirely (
posthog-js,src/lib/posthog.ts,ph.odontox.iofrom CSPconnect-src, persisted keys); dead-demo deps@hugeicons/*,@number-flow/react. Added:@aws-sdk/s3-request-presigner. Sentry made lazy off the entry chunk (inits at idle frommain.tsx). - Meta hardening:
fetchApprovedTemplateswrapped in an 8 sAbortController; Templates listing soft-fails to[]withmetaError.
10. Superadmin tooling (internal-only)
- Platform invoice builder. Full-page split layout with a debounced (~700 ms) live PDF iframe tagged by sequence number to drop stale renders; clinic profile cached on mount; invoice history + bank-QR upload moved to slide-overs. Scan-to-Pay QR ships reliably:
bankQrSrcaccepts HTTPS URL or data URI, prefers the publicassets.odontox.iopath (zero Worker bytes, same path as the logo), snapshot storesbankQrUrlalongsidebankQrR2Key/bankQrMimeso historical invoices regenerate. New permanentDSseries on platform invoice numbers (INV10-YYYYMMDD-DS###, monotonic, never resets) viaMAX(SUBSTRING ... '-DS([0-9]+)$');RC10/EST10keep per-day counters. Browser preview (PlatformInvoicePdf.tsx) and Worker email render (SubscriptionInvoicePdf.tsx) are byte-aligned. Refreshed Lahore office address + “Dental Management Systems” issuer. - Impersonation guard. Tenant-tools Impersonate validates the target has a real login account up front (
checkImpersonable, 5 call sites) — id-less / no-auth targets (e.g. visiting-doctor records) get a clear message instead of a malformed 400. - Smoke-test prod guard.
server/src/scripts/chat-v2-smoke.tsrefuses to run when the DB host matches prod patterns (odontox-prod, Neon prod pooler,api.odontox.io) orENVIRONMENT=production; override only viaODONTOX_SMOKE_ALLOW_PROD=yes-i-mean-it. Prevents synthetic “smoke test message” rows leaking into a live inbox.
11. Known issues / follow-ups
- Public KB gaps. v1.8 features are largely undocumented on q.odontox.io (KB last pushed 2026-05-23). Net-new pages needed: Todos planner, owner dashboard, dashboard customisation, sidebar order/presets. Stale: every
ai/*page still says “3 per hour” (now 15/day) and omits the cache layer;clinical/appointments.mdxstill describes click-to-book-on-empty-slot (now off by default); WhatsApp role-access + bank-QR + MRN updates. Track against the doc-gap audit. - Superadmin tab-links. 11 superadmin modules still use local-state tabs (no URL sync) — migrate to
useDeepNav. /public-documents/:token/pdfonly renders invoices; receipt/quotation/treatment_plan/lab_case return 415 (scope-limited fix).
12. Deploy checklist
A. Apply database migrations
B. Deploy the Worker
C. Deploy the UI (Cloudflare Pages)
D. Marketplace app
No change this window.E. Bridge release
No change this window.F. Smoke verification
- Login page shows
v1.8. - Ruby Insights renders with “Cached · X ago” badges; refresh consumes 1 of 15.
- Reception Day Brief / Prep Hints / EOD on a receptionist account.
- Calendar Week/Month on a busy clinic returns the full window.
- WhatsApp invoice/quotation/receipt attachment delivers as a real PDF.

