The care console to-do list: everything the console still needs, in the words of the person who asked for it. Drew named the plan Operation Takeoff on 2026-09-17. Last updated 2026-09-18 (CC-36 to CC-40 were added on 2026-09-17's second message).
Read it as a page: https://operation-takeoff.pages.dev — rebuilt from this file on every push to main, no passcode. Edit this file, not the page.
If you are new here and want to help: read this page top to bottom, pick an item that is still open, say which number you are taking, and build it. Start with START-HERE.md for the thinking behind the project and CONTRIBUTING.md for how changes ship.
How this list works
- Open work sits at the top. Finished work moves to Done at the bottom, with the date and how it was checked.
- Every item keeps Drew's exact words, in a quote block at the end of the item. Nothing gets paraphrased away. If an item was asked for twice, both askings are kept, with dates.
- The numbers never change. CC-07 is CC-07 forever, even after it is done and moved down. Say "I'm taking CC-07" and everyone knows what you mean.
- **"Done" means merged to
main and verified live** — never "pull request open". That is the standing rule for this repository. - One item, one owner, one pull request where possible. Put the CC number in the pull request title.
- This document is updated with every push that touches the console. When work lands, the item moves to Done with the date, the pull request number and the merge commit that confirmed it, so the list doubles as the history. A push without a TODO.md update is a push that forgot something. The merge commit's id cannot be known before the merge, so the pull request records its own number and the NEXT update to this file fills in the merge commit — a rolling one-step lag that keeps every entry true at the moment it is written.
Where the other lists live. BACKLOG.md is the engineering backlog — bugs, debt, and work nobody has asked for by name. This page is the owner's list: what the person paying for and using the console has actually asked for. The numbered build catalog (features F##, surfaces S##, views V##, templates T##, department tasks D##) lives on the private plan page and is referenced by number there, not here.
Status key
| Mark | Means |
|---|
| 🔴 | Not started |
| 🟡 | Started, not finished |
| 🔵 | Blocked on a decision or on someone outside this repo |
| ✅ | Done — the item has moved to the bottom |
The list
| # | Item | Status |
|---|
| CC-01 | Therapy finder should be a widget, not its own tab | ✅ |
| CC-02 | Remove the "Our list" tab everywhere | ✅ |
| CC-03 | Top navigation bar breaks at different screen sizes | 🟡 |
| CC-04 | One home for clinicians: availability and profile together | 🟡 |
| CC-05 | Call-script button should not be on the clinician home page | ✅ |
| CC-06 | Directory tab should become "Send a referral" | 🟢 |
| CC-07 | Combine Cockpit and Care & support into the call script tab | ✅ |
| CC-08 | Bring back the call-note popup above the bottom bar | ✅ |
| CC-09 | A "Match now" button inside the call script | 🟡 |
| CC-10 | A form inside the call script that sends the note for you | 🟡 |
| CC-11 | Every organization can build and edit its own call script | 🟢 |
| CC-12 | Port the Kentlands matching "charm" into the shared matcher | 🔴 |
| CC-13 | Simplify the matching sliders and the result limit | 🟢 |
| CC-14 | Let a match end in an action: form, text, or templated email | 🟡 |
| CC-15 | Clinicians update their own information | 🟢 |
| CC-16 | Our own secure database, with two-way sync to GoHighLevel | 🔴 |
| CC-17 | Make Kentlands and Kid Matters verified partner orgs by hand | 🟡 |
| CC-18 | Gather organizations we haven't onboarded, mark who is verified | 🔴 |
| CC-19 | Search across partner organizations | 🟡 |
| CC-20 | A search bar at the top of Messages | 🟡 |
| CC-21 | Fix the "connect a chat widget" message | ✅ |
| CC-22 | Start a conversation from the directory; suggest partnerships | 🟢 |
| CC-23 | Schedule a call from inside a conversation | 🔴 |
| CC-24 | Floating save + unsaved-changes warning on Edit team member | ✅ |
| CC-25 | We never send the invitation — the person does | 🔵 |
| CC-26 | Two ready-made invitation messages | 🟢 |
| CC-27 | Joining should recognise people already in the system | 🟢 |
| CC-28 | A guided sign-up where we do the verifying | 🟡 |
| CC-29 | A short introduction to how our nonprofit works | 🟢 |
| CC-30 | Take "try it as Kentlands" out of Plan & support | ✅ |
| CC-31 | Get ready for Twilio | 🔵 |
| CC-32 | One shared number with extensions and a call-back voicemail | 🔵 |
| CC-33 | Can the stricter privacy mode be switched on later? (research) | 🔵 |
| CC-34 | Encouragements and badges — thinking first | 🔵 |
| CC-35 | liah.cc and what we call ourselves | 🔵 |
| CC-36 | Avatar on the right, org mark on the left, and a login page per org | 🟡 |
| CC-37 | The "Latest" pill in Messages never goes away | ✅ |
| CC-38 | Every option under "+" in a chat must actually work | 🟡 |
| CC-39 | Every organization gets a shared room with the LIAH team | 🟡 |
| CC-40 | GHL notification → secure-chat nudge: verify it is done | 🟡 |
1 · Tabs and navigation — make the console simpler
CC-03 The top navigation bar breaks at different screen sizes
Status: 🟡 Live 2026-09-18 — console PR #179, merge 2e34fe2, CI run 35317463474 green, live STAMP 20260918.36f2d2fc. Verified on the live site: base.css carries the icon-only search rule and the 63.99rem breakpoint, shell.js labels every workspace tab. Stays 🟡 until Drew has tried it.
How it was measured. There is no way to see the bar without a login, and a login is not something this work types. So a small lab now lives in the repo (scripts/topbar-lab/, never deployed): it renders the console's real top bar with made-up data — any org name, any set of roles, a chosen view, light or dark — at any width, and reports overflow and overlaps in numbers, not impressions.
Breakdown by size (before → after).
| Width | Before | After |
|---|
| 320 (side panel dragged narrow) | Org name ran 56px under the search icon | Name ellipsizes; monogram never shrinks |
| 375 (phone / side panel) | Name ran 25px under the search icon | Clean |
| 375, chosen view on | Pill at "Coordinat…", nothing overlapping (the documented floor) | Unchanged |
| 639 | Clean | Clean |
| 640–767 (small tablet, half a laptop) | Lens spilled 38px past the bar; labels over Search and icons | Lens icon-only, search is a single icon, org name capped |
| 768–1023 (tablet, docked panel) | Lens labels over the Search box; Search squeezed to 97px | Lens icon-only, Search box back at 239px+ |
| 1024 and up | Search box AND a second magnifier, side by side, on every desktop | One search entry |
| 1280 / 1920 | (same duplicate) | Clean, full lens labels |
What shipped. Three CSS fixes in styles/base.css plus an aria-label on each lens button so the icon-only band still names the workspace to a screen reader; a six-assertion source test (test/topbar-all-widths.test.mjs, proven non-vacuous: restoring the old search rule fails it); the lab and a topbar-lab entry in .claude/launch.json so the next person can re-measure in a minute.
Deliberately not done. No new JS breakpoint logic — the one 640px switch stays, and the middle band is pure CSS media queries, which cannot glitch on resize the way a script can.
What. The header at the top of the console sizes wrong, jumps, and glitches as the window or device changes. This needs a real look, not a patch: measure it at phone, tablet, laptop and wide desktop, in light and dark, with long and short organization names, with many tabs and few, and fix every case.
Done looks like. A short written breakdown of what was wrong at each size (in the pull request), the fixes, and screenshots at each size proving it holds. Verified live on care-co.loveisahabit.org, not only in a local preview.
Drew, 2026-09-17: "Also we need to correct the nav bar in all screensizes and view ports at the top of the care console... it sizes incorrectly, glitches, changes and has a ton of errors... do a ui/ux breakdown to finetune it, fix it and verify it's completion"
CC-04 One home for clinicians: availability and profile together
Status: 🟡 Live 2026-09-18 — console PR #180, merge 5437981, CI run 35318536467 green, live STAMP 20260918.6058f831 (live registry.js carries the redirect row, live availability.js carries the Update my info button). Stays 🟡 until Drew has tried it.
What shipped.
- The registry row for My profile now carries a
redirect to Availability with panel=profile, so the Profile tab is gone from the rail, the bottom bar and More for every clinician — including owners who are also admins or coordinators. Admins keep their screens untouched. - Availability shows an Update my info button in its header. It opens the real profile editor as a popup (the same module, mounted inside a modal, so nothing is duplicated). "Change this" on the availability line inside the popup closes it, since availability is right behind it.
- Old links to
work/profile (the onboarding checklist, bookmarks) follow the redirect and land on Availability with the popup already open. The deep link panel=profile does the same. - The button honours the same rules the tab did:
ui.tools.hide: ['work/profile'] removes it, and a non-clinician never sees it. - The top-bar lab (
scripts/topbar-lab/, never deployed) gained ?tool=availability, which mounts the live Availability tool with stubbed data, so this and future clinician-home work can be measured without a login.
How it was verified. In the lab at 1024 px light and 375 px dark: no Profile entry anywhere in the navigation; the popup mounts the editor at 704 px / 343 px wide with zero overflow; saving posts {"fields": …} to /clinician/profile; the editor's own team-managed dialog opens and closes inside the popup and the popup takes the modal back afterwards; the hide list removes the button. Tests: test/one-clinician-home.test.mjs (new) plus the nav, lexicon and role tests updated to the new bar.
Deliberately not done. The guided tour for the old profile tab is left in place but unreachable; closing the inner team dialog with the browser Back button does not hand the modal back to the popup (the X still works); panel=profile stays in the address after the popup closes.
What. When a clinician signs in, the console opens straight to their availability. That is good. But their profile lives on a separate tab, and that tab shows no availability — which feels odd, because the two are the same person's information. Give clinicians one landing tab: availability front and centre, with an Update my info button that opens their profile in a popup (see CC-15 for the popup itself).
Done looks like. A clinician has one home tab. It shows availability and opens the profile editor as a popup. The separate profile tab is gone for clinicians (admins keep what they have).
Drew, 2026-09-17: "Also the fact that the clinician comes to the home pages and it auto opens to availablitiy is great... but also they already have access to their profile thorugh that it seems odd to me that availability content is NOT available in the profile tab... (to me maybe there should just be one availability, landing page tab for clinicians which gives access to updating ones profile in the popup"
CC-06 The Directory tab should become "Send a referral"
Status: 🟢 Live 2026-09-18 — console PR #183, merge d5fb92e, CI run 35323375287 green, live STAMP 20260918.a28261b6 (registry label and the search placeholder confirmed on the live files). Went 🟢 when the sent referral became tracked and live (CC-22, same day).
What shipped. The Network tab that read "Directory" now reads Send a referral (compact bar: Refer); its internal key stays directory, so every saved composition and hide list still works. The intro and the search box now say the job — type a concern, an insurance, an age group, or a place — and the search box has focus the moment the tab opens on a laptop (never on a phone, where the keyboard would cover the list, and never over a phrase handed in from the call script). Every organization card and drawer already ended in a Refer button that opens the composer on "Referral" and posts {toOrg, type: 'referral', message} to /partner/reachout; that is unchanged.
How verified. New test directory-send-a-referral.test.mjs plus four older label fixtures moved to the new name; full gate green (console 1891, api 849). Lab ?tool=directory at 1024 and 375px: searching "medicaid" found a person and an organization, Refer opened the composer on Referral, one POST left with type: 'referral'. The focus bug the lab caught (the loading screen has no search box, so focusing before the data arrived did nothing) is fixed and pinned by the test.
Deliberately not done. The label is the same for every role — there is no per-role label mechanism, and the job is the same for an admin. Tracking the sent referral is CC-22.
What. For a clinician, a "Directory" is a thing to browse. What they actually want to do is send someone to the right place. Rename and reshape the tab around that job: search, find the right organization or person, and send the referral — with the partner-organization search from CC-19 behind it.
Done looks like. The tab reads "Send a referral" for clinicians, opens on a search, and ends in a sent referral that is tracked (see CC-22 for tracking).
Drew, 2026-09-17: "The directory tab also doesn't make much sense for clinicians (it might be better as a "Send a refferal tab"..."
2 · The coordinator's seat — one call script tab
The old Kentlands coordinator console got two things right that the care console has not yet matched: the call note was a popup that sat just above the bottom bar and stayed with you through the whole call, and matching was a popup you could open from any point in the script with the call's details already filled in. Both are asked for again below. Drew has asked for these before and the work did not land; that is why this page exists.
Where the pieces live today: the call script is the playbook tool, the Cockpit is the cockpit tool and Care & support is the support tool, all under the console's coordinate workspace in apps/liah-console/tools/coordinate. The Kentlands coordinator console that Drew is pointing at is the kentlands-ccc-console project in the umbrella folder next to this repository — open it and use it before rebuilding these.
CC-09 A "Match now" button inside the call script
Status: 🟡 Live 2026-09-18 — console PR #181, merge 48b06e9, CI run 35320112626 green, live STAMP 20260918.ddd1d794; verified on the live site (playbook.js carries the dock button and auto-run, matchnet.js exports ownFirst). Stays 🟡 until Drew has run a call with it.
What shipped.
- The script's matching tool is now called Match now everywhere: the top-bar button (key M), the popup's title, and a second Match now button sitting right beside the Call note button in the dock at the bottom of every step. One tap from any point in the call.
- Opening it runs the search by itself when the call has given it anything to go on (type of care, age, format or a focus area from the notes). It refuses to auto-run in three cases, on purpose: the coordinator has already touched the form, a search is already running or done, or the call has captured nothing yet — then it waits for a click, exactly as before.
- Closing and reopening keeps the results. When the call has moved on since the search, the popup offers Use the newest answers instead of silently re-searching.
- Own organization first is now a rule in the browser as well as in the Worker: a shared helper (
ownFirst in app/matchnet.js) puts the coordinator's own org's group ahead of every partner group in both readers of the answer — the script's popup and the Cockpit. The order inside each tier is untouched. - The top-bar lab gained
?tool=playbook, which mounts the real script against a stubbed Worker so the popup can be measured without a login.
How it was verified. In the lab at 1024 px: started a new-client call, answered one question (format = Virtual), tapped the dock's Match now — the popup opened titled Match now and ONE request went to /partner/match with {"location":"virtual","topN":5} and no click on Preview matches. The stub answers with the partner group listed FIRST; the screen showed Lab Org · Your clinicians above Partner Practice X · a referral out. Closed the popup, changed the service, reopened: no second request, both groups still on screen, Use the newest answers offered. At 375 px the dock's four buttons sit inside the column with no horizontal overflow and the popup fits at 343 px wide. Tests: test/match-now.test.mjs (5) holds the button, the auto-run guards, the own-first helper and the Worker's own-first construction; the full npm run check caught two older tests that read the dock's first button and the import order — both updated to read the toggle button itself.
Deliberately not done.
- The third tier, "other relevant organizations", needs the verified-partner tier (CC-17/CC-18) to exist first. Today the answer has two tiers: own org, then every partner the org refers to.
- No second keyboard shortcut for the dock button — M already opens Match now from anywhere, and a duplicate shortcut would make the key hint ambiguous.
- The Worker already answers own-first; the browser rule is a second lock, not a fix for a live bug.
What. Next to the note button on the script, add a Match now button. It opens the matching tool as a popup with everything already gathered on the call filled in (age, concerns, insurance, preferences — whatever the script has captured so far). From any point in the call, one tap to matches. Results come in this order: our own organization first, then partner organizations, then other relevant organizations.
Done looks like. Start a call, answer a few script questions, tap Match now — the popup opens with those answers pre-filled and shows own-org results first. Works from any step of the script.
Drew, 2026-09-17: "same with the old popup coordinator console matching system... so smooth from any point in the call it was easy to match (maybe another little button next to the note button on the script that says "match now" and invites the popup than with all the relevant info added... remember the goal is for this all to allow high qaulity matching for coordinators to first see their org, than other partner org, than other relevant orgs... i feel sad bc i've tried to update this with you before but you never were able to make it fully work so i really need your help to make it work this time..."
CC-10 A form inside the call script that sends the note for you
Status: 🟡 Live 2026-09-18 — console PR #182, merge aa772e8, CI run 35322038103 green, live STAMP 20260918.8e2b3729 (probed live: formembed.js serves parseFormEmbed, the call script carries intakeCardHtml, the handoff points at Call scripting). Stays 🟡 until an organization has pasted its own embed code and a coordinator has sent a note through it.
What shipped.
- What already existed before tonight, and was kept: the coordinator's handoff popup (the script's Send note button opens the organization's intake form in a frame with the call note already typed into it), the form address and note-field settings under Organization → Resources & links, and the Worker's write of
ui.intakeForm on the organization's own record. - New: a reader for GoHighLevel's embed code (
app/formembed.js). Paste the whole thing — iframe, script tag and all — and it pulls out the form's https address, its id and its name. A bare address works too. It refuses http, javascript: and data: addresses outright (the address becomes a frame a caller's name is typed into), and it never returns the <script> tag as the address. - New: a card Where the call note goes at the bottom of Organization → Call scripting, on both the templates screen and the editor. Paste box, then the address, form name and note-field fields fill themselves; Connect form saves with one request; the card then reads Connected — <host> with a Disconnect button. Not connected reads honestly: coordinators can copy the note but not send it.
- Resources & links gained the same paste box and a form-name field, and both tools write the SAME tenant field through one shared record builder, so they cannot drift.
- The coordinator's "not connected" message now points an admin at Call scripting by its registry label — one place to go, not two.
- Lab:
?tool=callscript, ?tool=links, and ?tool=playbook&intake=1 mount the real tools against a stubbed Worker so the whole flow can be measured without a login.
How it was verified. Lab at 1024 px: pasted a real-shaped embed code into the Call scripting card — the box emptied, address and name filled, Connect form lit up, toast "Read your form: New Client Intake — connect it below". Clicked it: exactly ONE POST /partner/org-links with a body of only {intakeForm:{url,name,formId,noteField:"message"}}; the card turned to Connected — link.alpha.test with Disconnect showing and the template grid still on screen. Coordinator side: started a call, pressed Send note — the popup framed the fixture form with ?message=<the note> (organization name, NOTES and CONFIRMED lines all present, decoded) and named the form. Resources & links: an http paste shows the refusal line and saves nothing; a good paste fills the fields and the save body carries the full record. At 375 px the card is 343 px wide with no horizontal overflow. Full npm run check green (console 1887, API 849, all four verticals). New tests: test/formembed.test.mjs (8, incl. the refusal list and script-first ordering), test/callscript-intake.test.mjs (7 — the card is in BOTH template branches, the save body names no other field, connected/not-connected/error states), and one added to test/handoff.test.mjs pinning the pointer to the registry label.
Deliberately not done.
- Kentlands' form is NOT connected. Its embed code belongs in Kentlands' own tenant record, pasted by a Kentlands admin (or Drew) through the card — never in code. The live
/config for kentlands still shows no intake form. - The console frames the form; it never runs the organization's
form_embed.js in its own origin. - No cross-organization sending. The note only ever goes to the coordinator's own organization's form.
What. The original clinician console could send the script's note, already filled in, through a form. Give every organization a place in Organization → Call scripting to paste a GoHighLevel form embed code, and have the call script show that form with the note pre-filled. Bring the form over from the Kentlands coordinator console as the first, working example.
Done looks like. An admin pastes an embed code once. A coordinator finishes a call, opens the form from the script, and the note is already in it. Submitting sends it through the organization's own form.
Drew, 2026-09-17: "(our original clinician console also had teh ability to send the script note autofilled via the form (you can leave a place for a ghl form embed code and auto place the form from the kentpsych coordinator console over to the care console )"
CC-11 Every organization can build and edit its own call script
Status: 🟢 Live 2026-09-18 — console PR #187, merge dafaff2, CI run 35328902117, live STAMP 20260918.3c154c63 (verified by fetching the live console: the banner and the editor's test button are both in the served files).
What shipped.
- The banner. On the Call script screen, an admin sees "Edit or customize your call script" at the top, above the starter notice and the call-type tiles, with one button: "Open Call scripting" (the editor's name follows the organization's own vocabulary, so a renamed menu still matches). It says plainly whether they are looking at the starter or at their own saved words. Only someone holding the admin role sees it — the editor would refuse a coordinator, and a door onto a refusal is worse than no door. It is never shown during a live call.
- Test as a coordinator. The editor's header gains a "Test as a coordinator" button beside Save. Unsaved changes are saved first (the coordinator's screen reads the SAVED script, so testing an unsaved draft would show the old words and make the editor look broken); a failed save keeps the admin in the editor with the draft intact; a clean draft opens the coordinator's screen directly. From there the banner brings them straight back to the editor — edit → save → test → edit is a loop with no menu-hunting and nobody from our team involved.
- No new routes, no new data. The editor still writes
ccccfg:<slug>:playbook through POST /partner/call-script; the coordinator screen still reads it on mount.
How verified. Two new test files, both proven non-vacuous (each fails when its rule is removed): playbook-admin-edit-banner.test.mjs (admin sees it, coordinator/clinician/no-role never do, the legacy hasRole ctx shape still answers, launcher-only, navigates to organization/callscript, degrades to a toast without a router) and callscript-test-as-coordinator.test.mjs (clean → go; dirty → save then go; failed save → stay). Full gate green: console 1911/0 (both time zones), API 852/0, verticals 4/4, nav-labels clean. Lab: admin+coordinator shows the banner at 594px and 375px (button wraps under the text, no horizontal scroll); coordinator-only shows no banner; the editor's button, clicked with unsaved changes, posted one save to /partner/call-script and cleared the unsaved note before navigating.
Deliberately not done. The coordinator's Call script row in the menu stays coordinator-only (CC-05's decision, 2026-09-17); an admin reaches that screen through the editor's test button, not through the menu, so an admin who also sees clients still lands on their clinician home.
What. Creating a call script inside the console should be possible and easy for every organization — not a thing only we set up. Admins and owners should be able to create, edit, manage and test their script without help. After CC-07 combines the tabs, add an Edit / customize your call script banner at the top of the admin's screen that opens the editor.
Done looks like. An admin at any organization can open the editor from the banner, change a step, test it as a coordinator would see it, and save — with no one from our team involved.
Drew, 2026-09-17: "I actually do want creating a call script inside the care console to accessible and possible for everyone... admins /owners should have the easy ability to create and manage the call script and edit it and test it easily (maybe after we update and consolidate those mulitple tabs into one call script tab we can allow admins /owners to have a edit /customize the call script banner at the top of their screens)"
3 · Matching quality
CC-12 Port the Kentlands matching "charm" into the shared matcher
Status: 🔴 Not started. Long-standing; the standing rule is that Kentlands' matching improvements port in, their look does not.
What. Over months, the Kentlands matching tool picked up dozens of small tunings — the little things that make it match well: keyword syncing, how concerns map to specialties, how ties break, what gets shown and hidden. Go through the Kentlands repository, list every one of those tunings, and bring each into the shared matcher (packages/matcher-core) so every organization benefits.
Done looks like. A written list of each tuning found, and for each: ported, or deliberately left out with a reason. The shared matcher's tests cover the ported ones. Kentlands' own copy is not modified.
Drew, 2026-09-17: "We need to go through the kp matching tool "charm" (all the little things we updated on kentpsych repo to make the matching tool match well … even all of the key word syncing we used..."
CC-13 Simplify the matching sliders and the result limit
Status: 🟢 LIVE 2026-09-18 — PR #190 (branch cc-13-limit-and-sliders, head 491dc30), merge 25bb5c3, deploy run 35345027446 succeeded, STAMP 20260918.4695ee8e. Live check: that STAMP is the one the live app/config.js serves, and the live tools/organization/matching/matching.js carries TOPN_MAX = 50, TOPN_EVERYONE = 500, "Everyone who fits", kcc-mt-slider-word and "What a search cares about" — so both halves of this item, the limit and the words, are the deployed code and not just the merged code. The 1–12 setting shipped 2026-09-17; this PR is the rest.
Not verified. Nobody has yet saved a real organization's matching settings through the deployed console and watched a public widget pick the new limit up within the minute. The write path is proven by tests and the nudge is proven by the same mechanism every other hub write uses, but the end-to-end run on live data has not been done.
What shipped in #190.
- Best 1 → everyone who fits. The limit slider runs 1 to 50 and one notch further, which reads "Everyone who fits". That notch is stored as 500, a real integer, because the three deployed matchers slice the ranked list to this number — a 0 would show nobody and a blank would fall back to the widget's built-in 3, and none of those matchers can be redeployed from this repo. The Worker accepts 1..500 and rejects 0, 501 and 3.5 with a plain reason.
- Sliders speak in words. Each weight shows Ignored / Counts a little / Counts / Counts a lot beside its number. Storage stays 0–10, so nothing already saved changes meaning. "Back to recommended" sits beside "Undo changes" and drafts the shipped defaults without saving.
- A live website follows within a minute. The matching save now nudges the public widget host, the same way every other hub write does. It was the one write that did not, and the widget host's own refresh cron has never fired on that account.
- Card copy reworded for a practice owner and off the lexicon words; ceiling 68 → 66.
How verified. 11 new tests (6 console, 5 Worker driving the real worker.fetch), each proven non-vacuous by a mutation that fails exactly it. Full npm run check EXIT 0. Lab render at desktop and 375px: words update live, Save posts {display:{topN:7}} then {topN:500}, label wraps on a phone. Deliberately kept all eight sliders: each is a live scoring field on every deployed matcher, and hiding one from the owner while it still scores would be worse than showing it.
What. Redo and simplify the matching sliders so an organization can do a lot of high-quality things without a manual. The result limit should go from best 1 to unlimited, not stop at 12. The sliders should be fewer, clearer, and worded for a practice owner, not an engineer.
Done looks like. The Matching tab reads simply. The limit control offers 1 → unlimited. Every control has one plain sentence saying what it changes. A search on a live website respects the saved settings within a minute.
Drew, 2026-09-17: "(and redo and simplify the matching sliders so that it can do lots of user firendsly high quality things like set the limit of clinicians that could pop up (aka best 1 to unlimited)..."
Drew, earlier in September 2026: "some searches only pop up 3 clincians... we need to create the ability to decide how many clinicians we want to show up as a max in searches..."
CC-14 Let a match end in an action: form, text, or templated email
Status: 🟡 Console half LIVE 2026-09-18 — PR #188, merge bc8d91a, PR run 35340632649, deploy run 35341270609, live STAMP 20260918.05faf7d3 (verified: kcc-mt-after-opts in the live matching.js, after-match-email in the live playbook.js). Goes 🟢 when the public widget half ships (below) and one organization has saved a choice — none has yet.
What shipped.
- The choice, per organization. Organization → Matching gains a card, "After a match, one tap does this", with three options: Nothing extra; Open our intake form (the form already connected under Call scripting — if none is connected the option says so and links to that editor, and the saved choice quietly does nothing until a form exists); Open an email we wrote (subject and message in the organization's own words, with
{first name}, {clinician} and {org} filled in at tap time, a live preview as they type, and "Start from an example" when the box is empty). One honest line on the card: text messages are not an option yet — this console does not send texts today (that is CC-31). No "coming soon". - The tap, on the match. On the coordinator's Call script, every result card gets one small button beside "Add to note" — Intake form or Email — only when the organization chose one. Intake form adds the clinician to the note and opens the same handoff as "Send this note", form framed and note prefilled. Email opens the coordinator's own mail program with the organization's template filled in;
{first name} is left for them to type there. The caller's name never passes through this console — that is the whole design, not a shortcut. - Where it lives.
matching.afterMatch on the tenant record, saved through the existing POST /partner/matching (admin-only, own org, merge-safe), validated by one pure sanitizer in match-referral.js, read back from GET /config. text is refused with the reason text_not_available, so a stored promise the product cannot keep never reaches a match card. Email body capped at 1500 characters because it rides in a mailto: link browsers truncate silently.
How verified. 20 tests in four files (after-match.test.mjs, aftermatch.test.mjs, matching-aftermatch.test.mjs, playbook-aftermatch-button.test.mjs); three mutations each fail the suite — letting text through, blanking unfilled placeholders, and showing the button when nothing is chosen. Full gate green after the nav-labels gate caught a literal editor name in the form option's note (fixed: registry name only). Lab at 594px: card renders with the saved choice selected, typing updates the preview without losing the caret, two saves posted exactly { afterMatch } (email then form); the Call script shows no button without a choice, 3 Email buttons with one, the tap built mailto:?subject=Next step with Lab Org&body=Hi {first name}, … Own One at Lab Org …, and the Intake form tap opened the handoff with "Lab Intake" and the clinician in the note.
Deliberately not done. The public widget's copy of the action — the widget is served from the matcher repo (care-platform/services/matcher → liah-widgets), not this one. The key it needs is already published on /config, so that half is a read, not a redesign. Texting stays out until CC-31 gives the console a way to send one.
What. When a search finds the right person, the next step should be one tap. Give each organization the choice of what that tap does: open a GoHighLevel form, send a text message, or send a templated email — and let them edit that template themselves.
Done looks like. In Matching, an admin picks the action and (for email) edits the template. On the public widget and in the console, a match result shows that action. Texting depends on CC-31.
Drew, 2026-09-17: "we should alsoo give poeple the choice to add a ghl form in their or send a text message or something else like send a templated email (and edit that templated email)"
4 · Who owns the data
CC-15 Clinicians update their own information
Status: 🟢 LIVE 2026-09-18 — PR #189, merge 9c154c5, PR run 35342861917, deploy run 35343721732, STAMP at merge 20260918.e637a1b9. Live check done 2026-09-18 against tools/work/profile/profile.js on care-co.loveisahabit.org: 200, and "Where you meet people", data-chip and "Update my info" are all in the served file. The STAMP is deliberately not the check here — CC-13 deployed after this and the live stamp is now 20260918.4695ee8e, so a stamp match would have proved only that something newer shipped. The file content is the evidence. The one thing no offline check can prove — /clinician/me.editable on a real login — is still unverified. Pairs with CC-04.
What shipped.
- The same fields as an admin. The Profile popup on a clinician's availability home now edits focus areas, communities served, therapy approaches, age groups, where you meet people, telehealth, services, credentials, degree, gender, cross-state licensure and the three bio fields — as chips, check groups, a select and text, not one free-text box. Options come from the organization's own field schema, with the shared care vocabulary as the fallback; a saved value not on the list is kept.
- One rule for both editors. The Worker derives what a person may edit on themselves from the same allow-list the Team tab uses: everything an admin may write, minus
status (inactive revokes the login — nobody should be able to lock themselves out), minus whatever the connection declares display-only. On the shared hub nothing is display-only, so a clinician there can change their own specialties. Decided by connection type, never by an organization's name. - Honest errors. A word outside the shared vocabulary comes back in plain English when the organization enforces it; a GoHighLevel "not an allowed option" is shown as such, not as a crash.
- The meter tells the truth. It said "missing where you meet people" to someone whose location was set; fixed, and a gap only offers "edit now" when that person can actually edit that field.
How verified. 5 Worker tests drive the real worker.fetch (a write lands on the signed-in person's record, canonicalised; status never leaves; readonly subtracts; enforce refuses; a 400 is a 400). 7 console tests. Both suites proven non-vacuous by mutation (six mutations, each fails). Lab render with a stubbed Worker: 16 labels, save disabled until a real change, one POST with exactly the changed keys. Full gate green; the lexicon-reach ceiling ratcheted 72 → 68 by keeping the new prose off our words instead of raising the ceiling.
Deliberately not done. Headshot upload stays with the admin (Team tab). A service can be added but not removed from either editor — an unchecked box sends an empty value, which the Worker skips for RADIO fields; the hub's option value for "no" is not known and was not guessed. The live /clinician/me editable list needs a session token and was not checked against production.
What. Today an admin can edit a team member's specialties and every other field through the Edit team member popup on the Team tab — and that popup is good. Clinicians should be able to do the same for themselves: an Update my info button on their availability home opens the same kind of popup, showing only their own record. Reuse the existing popup rather than building a second one; popups are the preferred shape here.
Done looks like. A clinician signs in, taps Update my info, changes a specialty, saves, and the change shows up everywhere that record is read — the directory, the matcher, the public widget. An admin's view of that person shows the same change.
Drew, 2026-09-17: "The clincians info --> specialities and all variables need to be updatable in the console by the own clinician(not just the admin --> the edit teamm ember for admins pop up is awesome and it could be like that for clinicians updating (great ui /ux already there to duplicate or use in some other way when someone clicks update my info on the clinician home page availabiltiy view (i like popups for this better)"
CC-16 Our own secure database, with two-way sync to GoHighLevel
Status: 🔴 Not started. A foundation item — the biggest on this list. Also the fix for "a practice's own GoHighLevel and the hub copy disagree" (see the org-sync notes in this repo's history).
What. Right now GoHighLevel is where clinician data truly lives and the console reads it. Flip that: the care console keeps its own secure database as the source of truth for every clinician at every organization. GoHighLevel becomes something we sync with, both directions and automatically — when a practice with its own GoHighLevel (Kentlands, Kid Matters, and every practice after) updates a custom object, the console gets it; when someone updates their record in the console, their GoHighLevel custom object gets it. A practice's token is what carries the update, so each organization only ever touches its own data.
Done looks like. A written design first (one page, plain English on top): what lives in the database, what syncs, what wins on a conflict, and how a practice connects its token. Then: the database, the sync in both directions, and one practice (Kid Matters) running on it live with a hand diff proving nothing was lost.
Drew, 2026-09-17: "We need to create a secure database for all of the data to live for all of the clincians in the care console (rather than ghl being the source of truth, on the to do list we need to add the ability to update ghl custom objects for KP and all practices so that the token can send updates directly to the care console and back and forth automatically)"
CC-17 Make Kentlands and Kid Matters verified partner organizations by hand
Status: 🟡 Mechanism live 2026-09-18 — console PR #186, merge ecfc7cd, CI run 35328122707, live STAMP 20260918.8885cf90. The two flags themselves are set by hand against the live Worker (below) and the status goes 🟢 when both show the mark on the live directory.
What. Only two organizations are verified today: Kentlands and Kid Matters. Mark both as verified partner organizations by hand, so the checkmark in CC-18 has someone to show it on from day one.
Done looks like. Both tenants carry the verified flag in the registry; the directory shows the mark on both.
What shipped.
- Two facts, two words.
verified on a directory row has always meant "somebody from this organization once signed in". The new verifiedPartner means "the platform team met this organization and verified it by hand". They are never merged: an org can be either without the other. - The registry holds it. The flag is
verifiedPartner: true on the tenant record, written only by POST /admin/tenant (platform staff on any org), coerced like isTestOrg so a stray "false" string verifies nobody, and preserved across unrelated saves. No /partner/* route can set it; an organization cannot verify itself. - Every reader sees it.
GET /partner/directory reads each active org's tenant record and puts verifiedPartner on the row, and badgesFor leads the badge list with a gold Verified partner chip (verified_partner, in app/badges.js as the first entry). - The mark. A green check on the organization's logo/monogram, on the directory card and in the drawer header (one renderer,
logoHtml, so it cannot appear on one and not the other). Screen readers get "Verified partner". Nothing is drawn for an unverified org — positive-only, as ever. - Setting the two flags (platform admin session, the console's admin password — Drew runs this):
POST https://care-console-api.liah-carecoord.workers.dev/admin/tenant Authorization: Bearer <admin session token from POST /admin/login> { "slug": "kentlands", "verifiedPartner": true } { "slug": "kid-matters-counseling", "verifiedPartner": true }
There is no toggle for it in the console yet (nothing in the console calls /admin/tenant); the route is the way. I did not run it: it needs the admin password, which I never type.
How verified. API tests directory-verified-partner.test.mjs (3): flag set by admin reaches a DIFFERENT org's directory read with the leading badge, signing in alone never sets it, "false" never verifies, survives a rename, /partner/org cannot set it. Console tests directory-verified-mark.test.mjs (4), proven non-vacuous: keying the mark off verified fails one. Lab at 1024 and 375: check on the card monogram and drawer head, chip first, no overflow.
Deliberately not done. No admin UI toggle in this PR (the route is enough for two orgs; a toggle on the platform admin screen comes with CC-18's wider verified/unverified work). Unverified orgs carry no "unverified" label — CC-18 decides how gathered listings are set apart.
Drew, 2026-09-17: "(varified practices only inlcude kp and kid matters which need to be made partner orgs manuelly--> add to the to do list)"
CC-18 Gather organizations we haven't onboarded yet, and mark who is verified
Status: 🔴 Not started.
What. A search that only knows the organizations who have signed up is a small search. Start building up a wider set: gather public information about practices and programs in the areas we serve, bring them into the database (CC-16) as unverified, and let searches include them. Show a green checkmark on the profile avatar for verified organizations and verified clinicians, so the difference is obvious at a glance.
Done looks like. A documented, repeatable way to gather and refresh unverified listings (what we collect, from where, how often, and how a practice asks to be removed). Searches return verified and unverified results, visibly different. The checkmark shows on avatars.
Drew, 2026-09-17: "and speaking of the data base for all orgs on our care console... we need to start beafing up and scraping data and encorporating unverified practices into our searches … (we need like a green checkmark for varied orgs and clinicians on the profile avatar)"
CC-19 Search across partner organizations
Status: 🟡 Live 2026-09-18 — same console PR #183 as CC-06, merge d5fb92e, CI run 35323375287, live STAMP 20260918.a28261b6. Stays 🟡 until verified organizations are marked (CC-18).
What already existed (found while scoping, not built now): the tab already searched every organization in the network by name, focus, service, insurance, city and state, and already fanned out to every other organization's published clinician roster, so people at partner organizations were searchable by name, focus, age group and place. New now: a person is also found by the insurance they take, and the intro and search box say so. Search reaches the whole network for every role that can see the tab (clinician, coordinator, admin).
Deliberately not done. No "verified" mark — the Worker's verified flag means "has ever signed in", which is not what CC-18 means by verified, so nothing is shown rather than something misleading. A person's row has no Refer button of its own (the row opens their organization, whose drawer has one); the referral always goes to the organization, which is where intake happens.
What. Clinicians — and anyone else with a login — should be able to search partner organizations quickly: to refer someone, or just to research who does what nearby. Today the search inside the console reaches one matcher; it needs to reach the whole network.
Done looks like. From the Send a referral tab (CC-06), a search by need, insurance, age or area returns people and programs across every partner organization, with verified ones marked (CC-18).
Drew, 2026-09-17: "we need to start building the search functionality for partner orgs as well so clincians can do easy searches to reffer or research and other individual too"
5 · Messages
CC-20 A search bar at the top of Messages
Status: 🟡 Built 2026-09-18 — console PR #178 merged f6ae777, live STAMP 20260918.606c3a25 (CI run 35316095631; live inbox.js carries the Start rows and the Directory row). npm run check 849 pass, verticals 4/4. Left 🟡 until Drew has tried it on the live console.
What shipped. The box already found conversations and searched the words in them; it could not START one, so searching a teammate you had never written to answered "No conversations match" — which reads as no such person. Now:
- First in the pane, always. Drew's 2026-09-17 words ("at the top") win over the 2026-08-28 placement ("below the protected channel"); the protected channel is still the first channel. The box no longer disappears on an empty screen — a person with no conversations is exactly who needs to start one.
- Start rows. Type a name; a teammate with no conversation yet is offered under "Start a conversation" with a Start button that opens the chat and clears the filter. Someone you already talk to is a row, never also an offer.
- Same name, two people. Each Start row carries a subline — job title first, role if there is no title ("Front desk" / "Clinician") — so twins are told apart before the tap.
- No results. The honest dead end stays ("No conversations match … Searched the words in your rooms and your team…"), and under it a way out: Find “…” in the Directory tab (the tab's name comes from the org's own registry label), offered only when no partner organization matched and never to a guest, who has no Directory.
- Phone and dark mode come from the console's tokens (
--fs-caption, --fs-micro, --accent, --text-muted); the rows are the same 2.75rem-tall thread buttons the list already uses.
QA cases tried (tests): empty inbox → box drawn first; stranger → Start row, no dead end; known teammate → row, not offer; tap Start → chat opens, filter cleared; two "Sam Lee" → sublines differ; "zeta" → Directory row; "alpha" (a real partner) → no Directory row; guest → no Directory row. Non-vacuity: removing the Start rows from the pane fails 3 of the 5 new tests.
What. Put a search bar at the top of the Messages tab. Type a name and find the conversation, or start a new one if it does not exist yet. Think it through as a user would: what happens with no results, with two people of the same name, on a phone, in dark mode.
Done looks like. Built, verified live, and double-checked for friendliness on phone and desktop. A short QA note in the pull request listing the cases tried.
Drew, 2026-09-17: "Also at the top of the messages tab I think should be a search bar to easily search and find or create a new chat. build and verify, double check and think through for quality assurance and userfirendliness... these are all crucial decisions..."
CC-22 Start a conversation from the directory, and suggest partnerships
Status: 🟢 Live 2026-09-18 — console PR #185, merge af53272, CI run 35326714791 green, live STAMP 20260918.77e6891f (directory.js carries the sent panel and the Connect button, messaging.js says "Partnership connection", inbox.js says "Find a clinician or program", /partner/sent answers 401 without a session). The complementary-partner suggestions with a reason already exist (the Find a complementary partner toggle from CC-19); the tracking is what was new here.
What shipped.
- Tracked per organization, the two kinds apart. The Worker's
POST /partner/reachout now also writes the SENDER's own row (sent:<org>:… in NOTIFY, 90 days, an excerpt of the message rather than a second full copy), and a new GET /partner/sent lists them — scoped to the session's organization, never a query parameter. Registered in the KV namespace register and the route auth contract. - "What you've sent" — a card at the top of Send a referral: partnership connections and referrals as two separate numbers (a third, "other messages", appears only when coordination, consult or untyped messages exist), then a fold-out list of the latest — organization · kind · when · who. Refreshed after every send, so the count includes the message the person just wrote. It renders in every state, including "Nothing sent yet", so an organization that has never sent one discovers it.
- The two buttons. Each organization card offers Connect (a typed partnership connection) and Refer; the org drawer says Make a partnership connection / Make a referral and keeps an Other message button for the kinds that are neither. The composer's kind is now labelled Partnership connection (was "Introduction"; the Worker key
introduction is unchanged), and the inbox shows an arrival as "Made a partnership connection with you". - The partner section of Messages now leads with Find a clinician or program (was "Find an organization"), which opens Send a referral — the search-and-start-a-conversation door Drew asked for.
How verified. API: test/reachout-sent.test.mjs (3 tests) — A sends a partnership connection and a referral to B; A's list has both, typed, newest first, excerpt trimmed to 160, no message field; B's own sent list is empty while B's inbox has two (isolation); untyped stays untyped; 401 without a session. Console: test/directory-sent.test.mjs (4 tests) — the split counts, the panel in all four phases, escaping of an organization name, fold state, refresh-after-send and load-at-boot; proven non-vacuous (mis-counting a referral as a connection fails 2 tests; removing the refresh fails 1). directory-refer-type and inbox-sections updated for the new labels. Lab ?tool=directory at 1024 and 375: counts 2/1 from stubbed rows, list opens, a Connect send posted type: "introduction" and the panel re-read to 3/1 with the new row on top; ?sent=0 shows the empty state; no horizontal scroll.
Deliberately not done. No popup search inside Messages itself — the button opens Send a referral, which already IS the search; a second search surface would drift from the first. No per-partner "already connected" mark on the cards yet (the list answers it; a badge is a small follow-up). The recipient still gets exactly what they got before — nothing about the sender's copy is visible to them.
What. Once the directory has become Send a referral, connect it to Messages:
- In the partner section of chats, a button opens a popup to search for new clinicians or programs and start a conversation with them.
- A "Make a partnership connection" button — for reaching out to a whole practice (routed to one representative). A directory algorithm suggests complementary partners: practices nearby whose strengths fill the gaps in ours, so the suggestion has a reason attached.
- A "Make a referral" button — about a specific client. The wording and the flow are built around that idea, and a referral is where the covered chat is sometimes the right channel.
- Both kinds of contact are tracked: partner contacts and referral contacts, separately, so an organization can see who it has reached out to and why.
Done looks like. Both buttons live in the console; a partnership suggestion shows its reason; both contact types appear in a simple tracked list per organization.
Drew, 2026-09-17: "on the note of the messaging tab, after we make the updates to the directory / make a refferal page, individuals could be encouraged with a button that turns to a popup to search for new clinicins or programs in the parnter secotion of chats and begin a conversation (potential complementary refferal suggestions is a directory algorithm that matches refferal gaps to allow for potential parnterships in nearby areas that support each other in key gaps... a make a potential partnership connection button should be available and it could be either with the entire practice(going to one representantive) or with an entire practice... if a org staff member goes the "make a refferaL" that is about a specific client and the button they click should be based around that idea and both partner and refferal contacts should be tracked accordingly... refferalls also are where reaching out via a hippa compliant widget is sometimes ideal..."
CC-23 Schedule a call from inside a conversation
Status: 🔴 Not started.
What. Two clinicians at different practices caring for the same person need to talk. That is the plain case for a Schedule a call action inside a conversation: pick a time, both get it on their calendar, and the conversation keeps the record. This also gives the console a natural way to notice who is active (see CC-34) without watching anyone.
Done looks like. In any conversation, Schedule a call proposes a time, the other side accepts, and both get a calendar entry. Verified between two real logins at two organizations.
Drew, 2026-09-17: "another way that clinicians might organize their contacts are partner therapists who they're managing a client with, this a coordination of care situtation is a prime example of why we have talked about a "schedule a call" feature that can be accessed in messaging converssations"
6 · Team and admin
CC-24 moved to Done.
7 · Inviting and joining
CC-25 We never send the invitation — the person does
Status: 🔵 Needs one confirmation from Drew, then small.
What. An invitation should come from a person the partner already knows, not from our platform. The Invite an organization tool already offers Copy link, Copy email and an open in your mail app button. It also has a path where the backend sends the email itself (the invite route accepts a send option). Turn that path off so the platform never emails a partner directly, and make the copy-and-send-it-yourself path the only one. Add a text message option next to email (CC-26).
Confirm first. The same backend route is used when an admin adds a staff member to their own team. Drew's instruction is about partners; the question is whether staff invitations should also stop going out from the platform. Default if no answer: partners never, staff unchanged.
Done looks like. No code path sends a partner invitation from our servers. The tool hands the person a ready message and gets out of the way.
Drew, 2026-09-17: "Also as for the invite an org tab ---> I think we should give them a message to send (and not send emails on behalf of them--> we need them to send the email)..."
Drew, 2026-09-17: "(Again we don't want emails sending from our platform directly to partners) we need to encourage each org to reach out directly)"
CC-26 Two ready-made invitation messages
Status: 🟢 Live 2026-09-18 — console PR #184, merge caa472f, CI run 35324835577 green, live STAMP 20260918.860a22b9 (messages.js served with "Keep my information current" and sms:?&body=; plan page shows #184).
What shipped. The invite tool's ready screen now has a chooser — Join us / Keep my information current — and an Email / Text toggle under it. The message field is editable (it was read-only); the buttons are Copy email / Copy text, Open in email (mailto with a subject per kind) and Open as a text (sms: with the body, no recipient — the sender picks the contact on their phone). Edits are kept per message while switching between the four, so nothing typed is lost. Both messages carry the same join link: for the practice we already refer to, their own portal IS the "form that keeps my information current". The sender's organization name is worked into the join text when the console knows it. The words live in one pure module, apps/liah-console/tools/network/invite/messages.js, so they are tested directly.
How verified. test/invite-two-messages.test.mjs (7 tests): the four messages, link once each, the two asks differ, texts one paragraph under 320 chars, mailto/sms encoding, and a driven mount that switches kind and channel, edits, switches away and back, and reads Copy — proven non-vacuous (restoring readonly fails one test; dropping the edit-persistence line fails another). Full npm run check green (console 1898, api 849). Lab ?tool=invite at 1024 and 375: chooser, toggle, editable field, mailto:?subject=Join%20our… and sms:?&body=…, no horizontal scroll on the phone.
Deliberately not done. No third message kind; no server-side storage of edited drafts (they live in the tool's state for the visit). "Open as a text", not "Open in Messages" — the phone app's name collides with our own Messages tool in the nav-label gate and would confuse a reader too.
What. Offer two messages, each as an email and as a short text:
- Join us — "Hi, to help us coordinate care together, join here…" — for a practice we want in the network.
- Keep my information current — "So I can send referrals to you more easily, could you fill out this short form so what I have about you is up to date…" — for a practice we already refer to.
Keep the current message's warmth; it is liked. Get creative with the wording, keep it short, and let the sender edit before copying. The current default, kept here so it is not lost:
Hi there,
We use a shared referral network to coordinate care and send and receive referrals between organizations. I'd love to have you in it.
You can join and set up your own free portal here (takes about a minute):
Where. The emailBody function in apps/liah-console/tools/network/invite/invite.js.
Done looks like. Two message choices, each with an email and a text version, editable, copyable, and openable in the person's own mail or messages app.
Drew, 2026-09-17: "secondly we want the email to essentialy have two email or text options ---> 1) being Hi to help coordinate care join here Or "to be able to more easily send your refferals to clients could you fill out this form to make sure the inforamtion i have is up to date..." get creative with it... i like waht we have here --> Just would definitely like to adpat it"
CC-27 Joining should recognise people who are already in the system
Status: 🟢 LIVE 2026-09-18 — PR #191 (branch cc-27-join-recognises-you, head c100988), merge 2a78e02, deploy run 35383627811 succeeded. The live check is the page, not the stamp. npm run stamp reported "already 20260918.4695ee8e — nothing to write" on every commit here, because join.html is not in the stamp's input set. So the proof is the served bytes: the live /join carried none of network/join/recognize, id="recognise", orgText or "First, your work email" before this deploy (29,856 bytes, checked), and carries all four after. Anyone re-checking this item should key on those strings and ignore the stamp.
What shipped.
- One field first: your work email. An invite link opens on that question. Three answers are possible, and the wizard is only the third of them.
- We already know you. A sign-in link goes to that address and the sign-up never starts — the case Drew described, where a partner organization was pre-registered or a colleague added you weeks ago. The old flow would have let you build a second organization beside your own.
- We know your organization but not you. The page says so rather than letting a duplicate be created. Two organizations with the same staff split a team in half and nothing downstream repairs it.
- Nobody knows you. The existing three-step wizard begins with the email already filled in.
- The route writes nothing, burns nothing, mints nothing. A recognised person receives the same mail
/auth/request would have sent, because the handler dispatches to that route instead of keeping a second copy of the nonce, code and mail machinery. The Worker's default export is a named app for that reason — one front door, one limiter, one set of rules.
The gate that caught the administrator dead end. The first draft of the "we know your organization" reply told the reader to ask their own administrator. npm run check refused it, and was right to: signin-dead-end-copy.test.mjs exists because on 2026-08-25 two organizations — 41 people, 22 work emails, no phone numbers — were told by every screen they could reach to ask an administrator who could not help. A tenant record can exist with nobody on the roster at all. So the route now counts the organization's active admin rows before it picks the sentence, names an administrator only when one is actually there, and otherwise says the fix is ours and gives an address. adminCanAct travels in the response so the page cannot drift from the server's answer. The page had the same bug in miniature: its fallback copy invented an administrator whenever the server sent no detail, and our address was plain text in the one reply written for a reader with nobody else to ask. Both fixed; orgText() renders server text as text and linkifies only the address, so no server string reaches the page as markup.
Three findings from an adversarial review, all fixed. Three independent passes read the first commit — tenant isolation, abuse surface, mechanism correctness — and each confirmed a defect the tests did not cover.
- A suspended organization still owns its domain. The owner was captured after the status check, so a new hire at a suspended practice fell through to
orgRegistered:false, the wizard opened, and a slightly different org name cleared both of /network/join's uniqueness checks — a duplicate tenant and GHL sub-account for a practice that already had one. - A refusal that costs nothing is an oracle. A bound invite refused every other address before the limiter bumped, so the one address it was minted for could be guessed at any rate for the life of the link. It costs a try now, which makes the eight-per-link bound real.
- The uniform 200 is not proof a mail went. This route believed
/auth/request's status code, which is the one thing that code deliberately cannot tell you, and sent people off to watch an inbox that was never going to ring. Fixed with a probe object the caller puts on its env — a network caller cannot set it, so the external response stays byte-identical. A response header would have handed the browser the same answer and reopened the enumeration oracle.
One finding left open on purpose. rlGet/rlBump is a read-modify-write with no atomic increment, so every limiter in this Worker can under-count under truly concurrent requests. It predates this route, affects every limiter equally — the PIN limiter's own comment already says so — and the honest fix is a Durable Object. Recorded, not quietly dropped, and not fixed here.
Enumeration, answered honestly. /auth/request is uniform on purpose and signin-readiness.mjs depends on that. This route is deliberately not uniform, because recognising people is the whole point of it. What makes that acceptable is three bounds, all tested: a valid unspent invite is required (no invite, forged, or already redeemed → 401 with no roster read at all, failing closed if the rate-limit namespace is unavailable); a bound invite answers for its one address only; and both limiters hold, eight per link and thirty per IP, with a fresh link getting its own eight.
How verified. 14 API integration tests driving the real worker.fetch plus 3 console tests, all proven non-vacuous — nine mutations, each failing exactly the tests it should. The suspended-org test asserts which organization's roster was read, not how many: its first draft asserted zero reads and failed, because the inviting organization is a legitimate second candidate and is legitimately read. Counting could not tell that apart from reading the one we had no business touching. Full npm run check EXIT 0, route-auth contract at 151 routes.
Multi-tenant gate. Every organization and every person ever sent an invite link runs this path. The lookup never takes a tenant from the request: candidates are the tenant owning the email's domain and the tenant that minted the invite, in that order, and nothing else is reachable. An org that has configured nothing sees the wizard, unchanged; no roster, no admin, no connection and suspended are each handled explicitly and tested. Nothing org-specific — no slug, no name, no list. Reads stay record-filtered by the same connFor / fetchAllClinicians path as everywhere else.
What. When someone opens an invitation link, the first thing to ask is their work email. If that email already belongs to someone in the system — because their organization was pre-registered, or a colleague added them — send them a sign-in email right there and skip the rest. If not, start the guided sign-up (CC-28). The same recognition applies when an organization was already registered and a new person from it is just joining.
Done looks like. Open an invite link as an existing person: one field, one email, signed in. Open it as a new person: the guided flow begins.
Drew, 2026-09-17: "ON THEM JOINING! when they get sent the link --> it should ask them to authorize via their work email (your partner org might have already added you" check and see insert work email and if they're in the system send an auth email and streamline onboarding, if not the onboarding link has got to be type form inspired"
CC-28 A guided sign-up where we do the verifying
Status: 🟡 Three of the seven steps are LIVE. Step 4 (the website check) merged as 0356a47, PR #193, deploy run 35387656966 — verified live, not just merged: POST /network/join/verify on the production Worker answers **401 invalid_invite where it answered 404** before, and the served /join page contains network/join/verify once where it contained it zero times. Steps 1 and 3 — one question at a time, and the setting-up screen with the introduction — are on cc-28-one-question-at-a-time. The remaining four steps are untouched; this item is not done.
What shipped (step 4, and part of step 3). A new organization pastes its website address at the top of the join form and we read the public page — the same page anyone visiting would see — and offer to fill the form in.
- Three checks, shown as evidence and never as a verdict. We reached the site; the admin's email domain matches it; the organization's name is on the page. The panel says in words that these are "helpful, not a stamp of approval" and that a person still reviews every organization.
verifiedPartner stays hand-set in ADMIN_TENANT_FIELDS (handSet: true) and nothing computed here may touch it — a check a stranger can arrange by buying a domain is not the same fact as a person having looked. - Consent is a tick, not an inference. Nothing off the website is used until the box is ticked; unticking it takes the logo back. The description lands in an editable field, so by the time it is submitted it is the person's own text and travels as
org.description like anything else — the logo, which they never typed, travels separately as site.logo and is re-validated server-side. What was consented to (which site, which fields, when) is recorded on the orgjoin: notification; the text itself is not. - The fill never overwrites what somebody typed. Only a field left alone is filled. Otherwise this becomes a thing that happens to them, which is the opposite of the point.
- A website we cannot read is a 200 with an explanation, never an error. Not reaching a website is a reason to type four fields in, and never a reason somebody cannot join. All fourteen refusals have their own plain-English line, and a test enumerates the module's vocabulary so a fifteenth cannot ship with the generic fallback.
This is the Worker's first server-side request forgery surface, and it is treated as one. The guard is src/site-read.js, with its own tests, and it allows one shape rather than blocking bad ones: https, default port, a public registrable domain, no credentials in the URL, no IP literal in any form (dotted, decimal, hex, IPv6), no private or reserved name. Every redirect hop is re-guarded, so a public host cannot hand us 169.254.169.254 on the second request — the test proves the second request is never made. The body is abandoned at 512 KB rather than read to the end, non-HTML is refused, and the page never leaves the Worker: the route returns parsed strings, so it cannot be used as a reader for pages the caller could not otherwise reach. The budget is tighter than /network/join/recognize's because each try costs an outbound request — six per invite link, twenty per IP — and the invite must be valid and unspent before a single fetch is made. The route writes nothing, burns nothing, mints nothing.
Proven non-vacuous, all three suites. site-read.test.mjs 9/9 (M1–M7, including "follow a redirect without re-guarding" and "delete one refusal's copy"); join-verify.integration.test.mjs 6/6 against the real worker.fetch() with a stubbed global fetch that records every outbound URL, so "a forged invite buys no fetches" is asserted on the list of URLs actually dialled, not on a status code; join-website-check.test.mjs 5/5 on the page. Two predictions in the integration suite's mutation table were wrong and the header now carries the transcribed runs and the reason, rather than the guesses.
One honesty fix found in passing. The KV register declared a 200-character cap on the orgjoin: notification's free-text fields. Nothing enforced it at the write, and check:kv reads the register rather than the writes, so it could not have caught that. The write now trims to it and the register says what happened.
What shipped next (one question at a time). The form asked eleven things at once behind a "Step 1 of 3" counter, which is the shape of a tax return, not a welcome. It now asks one at a time and says "Question 4 of 11", so the end is visible from the beginning.
- It is a way of showing the form, not a second form. The cards are built at runtime from the fields already in the markup, keyed on an explicit running order (
Q_ORDER), so every id, label, hint and the whole of submit() are untouched. Delete that block and the page is the two-step form it was. Adding a field without adding it to the order is safe by construction — it rides along with the question above it. - **Four
alert() boxes are gone.** A browser dialog interrupts somebody and then takes the message away with it, leaving them knowing something was wrong but not what. A missed answer now walks them back to that question and puts the sentence underneath the field. - Enter moves on, except inside the description (where it makes a new line) and inside the website box (which has its own button). The handler accepts
keyCode === 13 as well as key === 'Enter'.
What shipped next (the setting-up screen). Creating an organization ended with a blind setTimeout(…, 2600) to another URL. A person pressed a button, saw a screen for 2.6 seconds and arrived somewhere else.
- The screen is a window onto real work, not a stall. It goes up when the request leaves and comes down when the answer lands. If the Worker replies in 300ms it is on screen for 300ms — nothing here is on a timer pretending to be work.
- Nobody is redirected on a timer. The screen after it has a button, and they press it when they are ready. A page that is about to be taken away is a page nobody reads, which is why the three things a new member most needs to know were being shown to an empty chair.
- The introduction is rendered in both places — while they wait and again afterwards — precisely because setup can finish instantly.
- A failure puts them back on the form they were filling in, with the error, rather than stranding them on a dead progress bar.
Proven non-vacuous. join-one-question.test.mjs 8/8, with eight mutations actually run and each one killing a test (M2 kills two, and the header says why rather than treating it as a stray). how-it-works.test.mjs gained a ninth-item honesty gate in the same pass: the page claimed "about a ten-minute read" over 1,321 words, which is seven minutes at 200 wpm. The gate now derives the number from the page's own visible words, so the claim cannot drift again.
One deviation from the stated order, on purpose. The plan puts the loading screen before the user agreement. The agreement has to come first, because nothing may be created for an organization that has not accepted one — no half-made org, no orphaned sub-account. So the loading screen sits immediately after it, where the work actually happens.
Still to build (the other four steps). The live introduction with partial, locked access; the user agreement's own step; and the guided setup that follows. CC-29's page is the introduction step's content.
Multi-tenant gate. Every organization joining runs this path. The route takes no tenant from the request — it is pre-tenant by definition, gated on the invite token alone, and it reads nothing from the platform. An organization with no website sees the form it saw before, untouched. Nothing org-specific: no slug, no name, no allow-list of domains.
What. New organizations should not have to onboard themselves field by field — we can often do it faster from what is already public. The flow, one screen at a time, Typeform-style:
- Email, then a few simple questions.
- A live introduction to the software — they can look around with partial access, locked until their organization is verified.
- Verification with three simple checks: their website, their business email domain, and one more easy check.
- From the website: pull the organization's public details and photos, strip and prepare them, and ask consent to use the live photos and information.
- A loading screen while we set them up.
- The user agreement.
- Then straight into using the features, with a short guided setup.
Done looks like. A new practice goes from link to signed-in, verified, pre-filled organization in one sitting, with consent recorded. Tested with a fresh browser session, not a logged-in one.
Drew, 2026-09-17: "the onboarding link has got to be type form inspired ---> email, simple questions, live introduction to the software (partial software access granted, but locked until their org is authorized (it needs to be varieifed via 1) website 2) business email & somethine else simple (because I don't want each org to have to onbaord all themselves because we can do it quicker sometimes, take website, verify org, verify domain, verify and strip photos and get consent to get live photos and info from their website --> than go through a loading screen -> than have them complete the user agreement, this is similar if an org was already pre registered as well and they're just joining... than after in we want them to utilize the offerings and get set up with the features )"
CC-29 A short introduction to how our nonprofit works
Status: 🟢 LIVE 2026-09-18 — PR #192 (branch cc-29-how-it-works), merge 847fae7, deploy run 35385529370 succeeded. Verified live: https://care-co.loveisahabit.org/how-it-works answers 200 and carries all seven section ids — who, free, ask, referral, information, leaving, call. Like CC-27, the live check is the page, not the stamp — how-it-works.html is not in the stamp's input set, so key on those ids.
The copy is a first draft and wants Drew's read. It is live because a page nobody can open cannot be reviewed, not because the words are settled. Anything in it can change without touching code.
What shipped. One page, seven questions, in the order a person actually asks them:
- Who we are — a nonprofit, and what that means for them day to day rather than as a tax status.
- What it costs — free, and why it can be, so "free" does not read as a trial about to end.
- What we ask in return — keep your roster true, answer referrals, tell us when you stop taking clients. Small, specific, and honest about being the whole ask.
- How a referral moves — the actual path, including the part where a person decides, because a practice that thinks this is automatic will not staff it.
- How information is handled — what our system holds (de-identified organization-level facts), what it does not, and where client detail lives instead.
- Leaving — said plainly, on the way in. An organization that knows it can leave joins more easily than one that has to guess.
- Who to call — a person and an address, never "contact your administrator".
Two rules it had to satisfy, both enforced by gates, not by care. check:plain-copy and the own-org lexicon mean this page says organization for the reader's own practice and never practice; and signin-dead-end-copy reasoning applies to the "who to call" section, which is why it names us rather than an administrator the reader may not have. how-it-works.test.mjs pins the seven sections and their anchors, that the page says in writing it is not the agreement and links to the one that is, that "stays free" and "no commission, no contract" are on the page rather than implied, that every page it links to actually exists, and that it names no HIPAA or BAA coverage this nonprofit does not have — while still telling a reader to keep names and personal details out, so the no-claim rule cannot be satisfied by saying nothing. Proven non-vacuous: one failing test per mutation, no assertion carrying another's weight.
Drew, 2026-09-17: "more on this... I'd love to streamline their joining process which we need to make a list for so we can easily get new people who join understanding how our nonprofit systems work!"
8 · Cleansing Kentlands out of the code
CC-30 moved to Done.
9 · Texting and calling
CC-31 Get ready for Twilio
Status: 🔵 Needs Drew's go-ahead on the plan in Twilio, in plain English below. The detailed plan page is twilio-plan.pages.dev (same passcode as the plan page).
What. One Twilio number for the whole care console: text notifications and two-way texting, turned on per person and per conversation. Email stays the default notice; texting is the upgrade a person chooses. Everything the console needs to be ready — one send function with the safety rules inside it, opt-in by keyword, per-person toggles, a kill switch — is written up on the plan page.
Done looks like. A production Twilio account under the existing login, a registered brand and campaign, one number, and the console sending its first notice to the first opted-in person (Drew).
Drew, 2026-09-17: "another to do list item is to make sure that we are prepped to incorporate twilio into the mix... while i would prefer to skip dev altogether and jump straight to twilio live, i understand dev is beneficial... a section at the bottom should be dedicated to explaining to me in plain english simple 3rd grade english how we will create an entire dev version of the software to run while as create a prod version of the software to the the other twillio acocunt... also which emails should those twilio accounts (one dev and on prod) be using ??? should we create two emails to create the accounts, and how do we get the nonprofit discounts once it's time (see gmail connector to find the email with twilio if needed). on this plan put the link to take action and give me your guess as to how we will get the nonprofit authorized and steps for dev build out a2p for prod and also risks if i skip straight to prod (because I am not scared of doing a2p --> partly beecause i am more scared that when i try to switch from dev twilio to prod twilio it will be a nightmare that i have to test all over again( but than again it sounds like it will be good practice and probably save me headaches when tests new text systems live down the road so we don't get blocked) --> as you've seen in my other plans... our goal is to have one twilio number for text notifications (and two way texting) for the whole care console as is turned on per chat / indiviaul"
CC-32 One shared number with extensions and a call-back voicemail
Status: 🔵 Needs a decision. This re-opens a choice made 2026-09-09, when voice was taken off the plan. Drew is free to re-open it; this item records that he has.
What. Every organization user gets calling through one shared number, each person automatically given an extension. When someone calls out from the console and reaches voicemail, the console speaks a message at the start of the voicemail: "Call back (number) at extension 5671 to connect. Again, that's extension 5671." When the person calls back, the shared number answers, asks for the extension, and routes the call to that user in their browser. Calling is off by default for an organization and switched on deliberately. Safeguards can be added as problems appear.
Done looks like. A design page first (plain English on top), then: one voice-capable number, extensions assigned automatically, browser calling in the console, the voicemail message, and the call-back routing — tested between two real people.
Drew, 2026-09-17: "We will also give every therapy org user (and every org staff user call ability through a shared number where everyone was automatically given an extension and than call back automated voicemail at the beggining of someones voicemail when an org member calls out via the care console it can say ("Call back xyz phone number at extension "5671" to connect, again that's extension "5671" ---> than when the call back it might be good for it to start with just a number routing for extension (and that automated voice message with the extension inserted)... this will allow calling if necessary (and we can put safeguards evntually if we encounter problems)... also many orgs phone calling ability will be off"
CC-33 Can the stricter privacy mode be switched on later? (research)
Status: 🔵 Answered below; Drew decides whether anything follows.
Answer, short. Yes. Twilio offers a stricter privacy mode (what they call HIPAA-eligible) on paid accounts. It is turned on later, on the same production account, by accepting an added agreement and using only the products marked eligible for it. It is a settings-and-paperwork step, not a rebuild. Note that Drew told Twilio in writing on 2026-09-02 that the care-coordination traffic will carry no protected health information, and the console is designed on that basis — so nothing on this list needs it today.
Drew, 2026-09-17: "(another thing to research about twilio is whether or not hippa compliance can be turned on via prod later and if so, how easily or not so easily)"
10 · Encouragement and badges
CC-34 Encouragements and badges — thinking first
Status: 🔵 Brainstorm, not build. Nothing ships until Drew has read the thinking.
What. Conversations, referrals and scheduled calls naturally show who is active. That could power gentle encouragements or badges for clinicians. Drew likes the current structure and wants deep thought before anything is built, because this can easily feel invasive. First thinking is in Badges, first thinking below.
Drew, 2026-09-17: "chats and tools like this can also be used to track activity to give poeple active clinican encouragements or badges(this needs to be thought deeply about as it shouldn't come across invasive so be thoughtful on our badges... as of now i like our structure so lets plan and think and brainstorm on the whole badges system, just things to think about)"
11 · Branding and domains
CC-35 liah.cc and what we call ourselves
Status: 🔵 Parked, Drew's decision. The liah.cc zone is live on the LIAH Cloudflare account with no records on it yet (checked 2026-09-17).
What. Should the console's backend — and eventually the whole console — live at liah.cc, with care-co.loveisahabit.org kept and forwarding? "LIAH" is fine as the console's short name; "liah.cc" is short and memorable; liah.org may come later. The open questions are trust (does a two-letter ending read as secure to a practice owner?), what every link in the system needs to change, and what it does to the brand. Thoughts in Branding: liah.cc below.
Done looks like. A decision recorded here with the date. If yes: a list of every address that changes, forwarding from the old address that never breaks, and one practice's links tested end to end.
Drew, 2026-09-17: "(on all of these links we need to think about liah.cc and the brand switch to have the backend live at liah.cc ---> I am not sure what this will do to our branding, I definitely still want forwarding.. I think it's okay for the care console to be called liah slang (eventually i bleieve we'll get the liah.org domain so it might all fit very easiily --> coordinating care liah,org) branding is something to think about... LIAH care console is where Liah.cc comes from liah is loveisahabit. that's also where. Care-co.loveisahabit.org comes bc i wanted the domain to be shorter when i made it and couldn't chose care coordination console or just care console so i took the overlap co... I thought about the domain care.co but that's taken and liah.cc just is so short and nice...liah care console liah.cc www.liah.cc i hope that comes across trustworthy and secure... it might take some getting used to..."
12 · Added 2026-09-17, second message
CC-36 Avatar on the right, org mark on the left, and a login page per org
Status: 🟡 Parts 1 and 2 done 2026-09-17 — PR #175, merged as 13f63ff, live STAMP 20260917.92a760db; part 3 🔵 waits on CC-35 and on Drew's yes to the subdomain recommendation below.
What shipped. The far-right icon in the top bar, and the head of the account menu it opens, now show the signed-in person: their photo when their staff row has one (the same headshot the Team screen shows), otherwise their initials by the person rule (first name + last name). The left mark is unchanged and already did what part 2 asks: an organization that sets a logo in Branding sees it there instead of initials (logo_url on the org record flows to the console's /config as the brand logo). Only a real http(s) address is trusted as a photo. Three tests; proven non-vacuous by wiring the button back to the org monogram (two fail). Who else this touches: every organization, every role — a sign-in with no staff photo sees initials, never an empty circle.
What. Three related things about identity in the top bar:
- The far-right icon in the top bar should be the signed-in person's avatar (photo, or their initials until they upload one). Today both ends show the organization's initials ("KM … KM" in Drew's screenshot), which reads as a bug.
- The left mark stays the organization's — and organizations should be able to show their logo there instead of initials (optional, from Brand).
- Eventually each organization gets its own login page, something like
kidmatters.liah.cc.
Recommendation on the subdomain question (Drew asked me to decide for the plan). Auto-choose the subdomain from the organization's slug, and let an admin change it once in Brand. Auto-choosing means every organization has a clean address the day they join with nobody typing anything; the one-time edit covers the practice that wants "kidmatters" instead of "kid-matters-counseling". Free text with no default leaves half the network with no page. This depends on CC-35 (liah.cc), so it is sequenced after that decision; the avatar and logo parts are not, and can ship now.
Where. The top bar is built in apps/liah-console/app/shell.js (kcc-topbar); avatar photos already live on the org_staff row (see the Team tool); the logo is brand_logo on the tenant.
Done looks like. Sign in as two different people at the same organization: the right-hand icon differs, the left-hand mark is the same. An org with a logo uploaded sees it on the left.
Drew, 2026-09-17: "another thing i want to add as of now(not in the last message) is that the icon on the far right on the nav bar should be the users avatar logo icon or image (whatever we decide) and the other logo / initials are on the left ( i think it might not hurt to put orgs logos potentially on the care console nav bar as optional too (I think we should eventually consider each org having their own login page like kidsmatter.liah.cc or something like that --> admin could chose their subdomain maybe --> or it can auto chosen for them) --> decide the better option for the plan"
CC-38 Every option under "+" in a chat must actually work
Status: 🟡 First jump 2026-09-17 — PR #176, merged 38463c9, live STAMP 20260917.bacd7018. The rule is in: a row under "+" is live or absent, never greyed. Get a person outside rooms landed with CC-39 (PR #177, 2026-09-18): from a partner or team chat it opens the organization's own room with the LIAH team, confirm row already open. Remaining: Attach outside rooms (see below) and the call helper (CC-23).
What shipped (PR #176). Every row a reader sees now does something when pressed:
- Bring someone in works in every kind of chat that can hold a third person. In a room it is the rooms invitation; in a group you manage it opens the group's own manage panel with the add-a-teammate picker; in a one-to-one team chat it opens a new group with that teammate already picked. In a partner chat it is not listed (people from another organization come in through their own console).
- Somewhere to go, in person is listed only once your rooms account is joined, where it already worked; Call desk only once a phone system is connected. Attach and Get a person are listed in rooms, where they work, and not elsewhere.
- Tests rewritten for the new rule (13, non-vacuous: restoring greyed rows fails 4).
Deliberately not done yet, and why. Attach in team and partner chats needs somewhere to put the file: the console Worker has no file storage (its bindings are the rooms engine, KV and D1), and the rooms engine's attachment door is tied to a room. The honest options are (a) give every team and partner chat a home in the rooms engine, or (b) add an R2 bucket to the Worker with the same limits the rooms door enforces. Either is its own jump.
What. The "+" beside the message box opens "More you can do here" with five options — Attach a photo or a document, Bring someone in, Get a person, Somewhere to go, in person, Call desk. In Drew's screenshot every one of them is a grey explanation of why it does not work here ("rooms only", "once your rooms account is joined", "hasn't connected a phone system"). None is clickable. Each must either work in this chat or not be listed. In particular:
- Attach a photo or a document — make it work in team and partner chats, not only rooms.
- Bring someone in — add a teammate to the conversation, right here.
- Get a person — reach the LIAH team (ties to CC-39's shared room).
- Somewhere to go, in person — either real (a directory of in-person help) or removed.
- Call desk / coordinate a call — becomes the "schedule a call" helper (CC-23).
Done looks like. Every listed option does something when tapped, tested and verified one by one; an option that cannot work for this organization is not shown, rather than shown greyed.
Drew, 2026-09-17: "ALSO !!! when I click "+" on the chat none of the options are clickable and have been bulit out yet... we need these to work,,, as seen in the second photo each of those + options like coordinating a call helper should all be test, verified and working"
CC-39 Every organization gets a shared room with the LIAH team
Status: 🟡 Built 2026-09-18 — console PR #177 merged fc0097e, live STAMP 20260918.b097fb43; rooms engine PR LoveIsAHabit/liah-dashboard#10 merged b1a55b7 and deployed. Open part: the end-to-end check with a fresh test organization, which needs both deploys live.
What shipped.
- Rooms engine: a new console door,
POST /api/console/orgs, creates (or finds) the organization and its room liah-<slug>, named "<Org> × Love Is A Habit", and seats every LIAH staff member in it. Idempotent, so asking twice is safe; a room slug already owned by another org is refused rather than taken. Because the seat is a real membership, the org shows up in every LIAH team member's switcher on Drew's dashboard — that is the "LIAH-enabled" part. 6 engine tests, non-vacuous. - Console Worker: the hand-kept
STAFF_FEEDBACK_ROOMS list still wins when it names an org; for every other org the room is derived as liah-<slug> and the engine is asked to make sure it exists before anyone is seated. Registering an org (platform admin, network join, hub onboarding) asks the engine right then, so the room exists before anyone opens Messages. The engine being down never fails a registration — the next Messages load asks again. Worker tests cover derived vs configured, create-once-not-on-edit, and an engine outage; all non-vacuous. - Console: Get a person under "+" is live from any chat once that room exists (closes the CC-38 gap). 14 tray tests.
Mini-CRM in LIAH-enabled rooms — done / suggested. *Done (engine routes under /api/crm/*, staff only, scoped to the caller's active org):* a desk view of every non-staff person across the org's rooms; per-room people with their notes; per-person notes (add, edit, remove, pin/unpin), tags, and a stage; org-editable stage lists; notes pinned to a single message. Suggested next: (1) a follow-up date on a person so the desk can sort by "who is due"; (2) the desk should surface the new liah-<slug> rooms as a section of their own, so LIAH staff see every organization's room in one list; (3) a one-line "last heard from" per organization, derived from the room's last message, which is the single number Drew asked the dashboard for. None of these is started.
What. The moment an organization joins, it should automatically have one shared room with the LIAH team for feedback and support. These are the LIAH-enabled rooms: they sync to Drew's dashboard and are visible to every LIAH team member, so LIAH can help and manage from one place. While there, the mini-CRM inside LIAH-enabled rooms needs freshening: a list of what is already done in it, and suggestions for what would help.
Where. The Worker's STAFF_FEEDBACK_ROOMS setting already seats LIAH staff in named rooms per org (tests cover it) — the gap is that it is a hand-kept list, not automatic on join, and not every org has one.
Done looks like. Register a fresh test organization: a room with the LIAH team exists before anyone opens Messages, shows up on Drew's dashboard, and every LIAH team member can see it. A short "done / suggested" note on the mini-CRM is added under this item.
Drew, 2026-09-17: "also for now every single org who joins the care console should automatically have a shared room with the liah team for feedback so that liah can help manage and support (this should be synced and show up in Drew's dashboard and visable to all liah team members--> these are liah enabled rooms --> speaking of the mini crm dynamics in the liah enabled rooms needs a freshening up too... completed items her and suggestions would help)..."
CC-40 GHL notification → secure-chat nudge: verify it is done
Status: 🟡 Built and proven for one org; verification for the rest is the open part.
What. When a practice's GHL "remove tag" automation fires the webhook, the console should nudge the person toward their organization's own secure chat widget. What is true today (checked against the runbook and PR #154, merged 2026-09-11): the Worker route /notify/reply receives the webhook, and the console shows the nudge on four surfaces — a green dot on the Messages tab, the header count badge, a count pill on "Open your secure chat", and an "Open / Not now" strip under the header. On 2026-09-11 the GHL workflow was proven to reach the Worker for Kentlands. By design the webhook carries only the clinician's email, so the nudge says "your organization's secure chat has something new", never which conversation.
Still to do. Confirm the GHL workflow is wired for every org that has a chat widget (today that is the one org with a published widget), write the two-field setup (webhook URL + secret) into the admin's Console setup tool so a new practice can connect it without us, and re-test with a fresh session. Runbook: docs/reference/SECURE-CHAT-UNREAD-INDICATOR.md.
Done looks like. A tag removed in a partner practice's GHL lights the nudge in that practice's console within 30 seconds, for a practice other than the first one.
Drew, 2026-09-17: "spealing of messaging features... we talked alot about a plan to have the notifications that come from ghl trigger a relevant notification in the hippa complaint chat widget popup button to guide individuals there when the webhook is triggered via the remove tag automation notification on the care console work around... lets verify this has been done and will get done if it hasn't yet"
Explainers, in plain English
Done
Finished work, newest first. Each keeps its number, its words, and the push that confirmed it.
CC-30 Take "Try it as Kentlands" out of Plan & support and the disclaimer
Status: ✅ Done 2026-09-17 — PR #174, merged as 408f9b8, live STAMP 20260917.d49559c3. The four previews now say "Try it with a sample organization" (the own-org wording gate rejected "sample practice", which is the right call: that word is for the network, not the reader). The playbook's "am I the default org" check reads the tenant's isDefault flag, never the slug; the join page no longer special-cases a slug (it asks the Worker and trusts isDefault); the internal launch page calls the account links "anchor practice" instead of naming it. The gate: npm run check:sample-practice (in the check:core chain, so CI blocks a deploy) scans every console code file, masks comments, and fails on the name; the two scrub rules that must spell it to remove it carry a sample-practice-allow: marker with a reason. Proven non-vacuous: putting the old button label back produced one finding and a failed build. Deliberately left alone: the bundled example call script is Kentlands' real script, shown to other organizations only under an "Example script" banner with the identifying details blanked by the scrub — changing the file would change what Kentlands' own coordinators read. The Worker's DEFAULT_TENANT || 'kentlands' fallback is Worker-side and stays as documented in CLAUDE.md ("read isDefault, never the slug").
What. The Plan & support tab shows four premium previews with a "Try it as Kentlands" button (in apps/liah-console/tools/organization/plan/plan.js). Kentlands is one partner among many and should not be attached to us more than any other practice. Change the previews to a made-up sample practice. While there, keep going: the app shell still treats Kentlands as the default tenant (the one that gets the bundled look when no organization is named), the join page special-cases the slug, and the internal launch page lists Kentlands' account links. Each of those is the same habit.
Done looks like. No visible Kentlands wording anywhere a user can see. No code path where "no organization" silently means "Kentlands". A gate that fails if the wording comes back.
Drew, 2026-09-17: "Also as for the Plan & support page and disclaimer get rid of the try as kentlands language, we do not want kentlands to be attached to us any more than any other practice (we started there, but have to cleanse the codebase accordingly)"
Drew, earlier in September 2026: "we need to verify that the basic color is no longer kentlands orange and all kentlands or clinician conosle or coordinator conole laziness in the code is change and updated and cleansed"
CC-24 Floating save button and an unsaved-changes warning on Edit team member
Status: ✅ Done 2026-09-17 — PR #174, merged as 408f9b8, live STAMP 20260917.d49559c3. The Save bar is now sticky at the bottom of the popup at every scroll position, with an "Unsaved changes" note that appears the moment a field differs from what the popup opened with (ticking a box off and back on counts as no change). Cancel, the X, clicking the grey area outside, Escape, and the browser Back button all ask the same question in Drew's words — Save your changes, or leave without saving? — with Keep editing / Leave without saving / Save. Nothing changed → it just closes. The bar lives inside the scrolling popup, so it never covers the last field; on a phone it rises by the on-screen keyboard's height and clears the home bar. The shared modal helper gained a beforeClose hook so any other popup can veto Back the same way. Five tests; proven non-vacuous by wiring Cancel back to the old close (one test fails). Who else this touches: every organization's Team screen, identically; no data or permission change.
What. The Edit team member popup on the Team tab is long. Add a floating Save button pinned to the bottom-right so it is always in reach. Auto-save would be annoying at this length, so instead: if someone has changed something and clicks away, ask them once — Save your changes, or leave without saving? Think about the small things: the button should not cover the last field; on a phone it should sit above the keyboard; Escape should trigger the same question.
Where. The popup and its save-member action live in the roster tool (apps/liah-console/tools/organization/roster/roster.js). Closing the popup today does not check for unsaved edits.
Done looks like. The floating Save is visible at every scroll position on phone and desktop. Changing a field and clicking outside asks the question; saving or discarding both work; closing with nothing changed asks nothing.
Drew, 2026-09-17: "ALSO on the edit team member popup on the team tab i think we should have a floating save button in the bottom right corner (auto save might be annoying with how long some of the things are), we just need a warning if they made changes and go to click out that prompts them to save or not save... I think that's a good ui ux fix for that particular functionality... think about it and optimize!"
CC-21 Fix the "connect a chat widget" message for people who can't do it
Status: ✅ Done 2026-09-17 — PR #173, merged as 998333e. Checked in code and in the Messages screen test with a clinician-only role set: the set-up button was already admin-only (Drew saw it because he is an admin everywhere). What changed is the sentence everyone else reads, now in Drew's words: "Nothing connected yet. Ask your admin to set up the covered chat. Until then, use the chat below for anything that is not client detail." No button for non-admins; the admin branch is unchanged. Two tests, one on the card and one on the full Messages screen as a clinician, both proven to fail against the old wording.
What. When an organization has no covered chat connected, the Messages tab invites the reader to connect one. Clinicians and coordinators cannot do that — only an admin or owner can. For everyone who is not an admin, the message should say something like: ask your admin to set up the covered chat, or just use the chat below for anything that is not client detail.
Where. The card is built in the console's chat-surface module (the protected-channel card in apps/liah-console/app/chatsurface.js) and shown from the Messages tool. The code already has an admin and a non-admin branch; check what a real clinician login actually sees, because Drew saw the invitation.
Done looks like. A clinician login sees the "ask your admin" wording, never a set-up button. An admin still sees the set-up button. Covered by a test.
Drew, 2026-09-17: "also on the fact the message tab for a practice with no hippa compliant chat widget it still invites clinicians and coordinators to "connect a chat widget" --> they couldn't do that anyway... that is the admins job or the owner and so we need to change the tab if a widget doesn't exist yet to allow it to say something else that they can ask their admin to set up the hippa complaint chat or just use the non phi chat below (something like that will help)"
CC-05 The call-script button should not be on the clinician home page
Status: ✅ Done 2026-09-17 — PR #173, merged as 998333e. The cause: the call script was granted to the admin role as well as coordinators, so an owner who also sees clients landed on the clinician home with a Script tab on the bottom bar beside their availability. A pure clinician never had it. Now the call script (and the Cockpit and Care & support panels folded into it) belong to the coordinator role only; someone who takes calls and also runs the organization holds both roles on their staff row, which the Team screen sets. Admins keep the call-script editor under Organization. Covered by a new test that fails if any role set without coordinator gets the script back (proven: restoring admin fails 2 of its 3 tests). Who else this touches: every admin on the network who does not hold the coordinator role loses the Script tab; if one of them actually takes calls, add coordinator to their row.
What. A clinician's home page shows a call-script button. Clinicians do not take coordination calls; that button belongs to coordinators. Find what decides to show it and stop it showing for clinicians.
Done looks like. A clinician-only login never sees the call-script button on its home page. A coordinator still does. Covered by a test that fails if the button comes back.
Drew, 2026-09-17: "Also why does on the clinician tab the call script. button pop up on the homepage... that needs to be corrected"
CC-07 Combine Cockpit and Care & support into the call script tab
Status: ✅ Done 2026-09-17 — PR #172, merged as 01befd9, CI run 35300833222 green including the plan-page publish, live STAMP 20260917.86470f64 (probed: the live script module carries the panel buttons and the sheet rule; the registry rows carry their redirects). One "Call script" destination under Coordinate. Cockpit (press O) and Care & support (press S) open from the script's own top bar as panels, each running the tool's real code inside; the old addresses redirect into the script with that panel already open, so bookmarks and the onboarding step still land right. Inside the script the cockpit drops its own "Call script" tab and lands on Saved notes. Nothing a coordinator could do before is lost. Verified by npm run check (845 tests) and a new test file proven to fail when any of the four halves is removed; live verification recorded below after merge.
What. Three tabs — Call script, Cockpit, Care & support — should become one tab. Everything a coordinator reaches for during a call should be reachable from inside the script, without leaving it. The other two tabs' content can sit along the top of the script tab, or the bottom bar can change shape when the script is open and become a script-specific bar. Start by combining; polish the layout after.
Done looks like. One "Call script" destination under Coordinate. The Cockpit's tools and Care & support's content open from inside it. The two old destinations redirect to it so old links keep working. Nothing a coordinator could do before is lost.
Drew, 2026-09-17: "Finally the cockpit tools should all be accessible in the call script, we can fully put the cockpit accessible in the script tab and remove the script tab... maybe only for the script tab when we open the script tab the bottom nav bar transforms into a full section and a new adapted script experince bottom nav bar appears (the first goal is to COMBINE cut the Cockpit and the care and support tab place it into the script tab --> combine all three into one script tab(the two other tabs content can be on the top nav bar of the script tab or something"
CC-37 The "Latest" pill in Messages never goes away
Status: ✅ Done 2026-09-17 — PR #171, merged as f97f853, CI run 35298868505 green including the plan-page publish, live STAMP 20260917.0303461c. The pill now shows only when the transcript is laid out, has messages, actually overflows, and the reader is off its foot; its state is recomputed on every paint. Three new tests, proven to fail with the old rule restored (the empty-thread case is exactly Drew's screenshot). Verified live: an empty conversation shows no pill.
What. The floating "Latest ⌄" pill above the message box (built as plan item F26, the "jump to latest" button in apps/liah-console/tools/network/inbox/inbox.js) is meant to appear only when you have scrolled up and newer messages are below. In Drew's screenshot it shows in an empty conversation, and it does not disappear. Fix: hide it whenever the view is already at the bottom or the thread has nothing to jump to; show it only when there is somewhere to jump. Then walk the rest of the conversation view with the same eye (send states, the read tick, the empty state) and tune what is half-built.
The rule Drew set. Anything added to Messages has to be finished quality — working, relevant, and tested with real sends — before it ships. A floating control that lies is worse than none.
Done looks like. Open an empty thread: no pill. Send three messages: no pill. Scroll up in a long thread: pill appears; tap it or reach the bottom: pill disappears. Checked in a fresh session on phone and desktop widths.
Drew, 2026-09-17: "Also another new thing as seen in the image their is a latest floating icon above the messaging box... that's fine to have but it needs to be relevant and work...IT WON:T DISAPPEAR !! SUPER ANNOYING, if we're going to make any updates like this it needs to be quality... no messages have been sent so we need to fine tune and develop this feature (and any other helpful messaging features)"
CC-02 Remove the "Our list" tab everywhere
Status: ✅ Done 2026-09-17 — PR #171, merged as f97f853, CI run 35298868505 green including the plan-page publish, live STAMP 20260917.0303461c. Removed from Kid Matters and Kentlands (their repositories and the console's copies); TeachCreate's owner had already replaced it with a real TeachCreate tab, which landed in the console the same evening (PR #167, SDK 1.1 apps.connected()). The scaffold no longer creates it for new organizations — a new repository starts with no tabs and the worked example lives in scripts/core-example-tab.js. Verified: check:verticals passes with the directories gone, and live: no "Our lists" tab for those admins.
What. The "Our list" tab adds nothing. Remove it from every place it appears: the registry, the navigation, any home-page shortcuts, any deep links, and the tests that reference it. If it holds any saved data, say so in the pull request before deleting it.
Done looks like. No trace of the tab in the console or the code. npm run check green.
Drew, 2026-09-17: "Our list tab --> worthless and should be removed everywhere"
CC-01 The therapy finder should be a widget, not its own tab
Status: ✅ Done 2026-09-17 — PR #171, merged as f97f853, CI run 35298868505 green including the plan-page publish, live STAMP 20260917.0303461c. The finder is now a card on Organization → Website widgets for Kid Matters, mounted inside its own "Customize widget" panel; the standalone tab is gone. Built as a general seam, not a one-off: any organization's repository can mark a tool "surface": "widget" and it becomes a card there instead of a tab. Verified: composition test (the entry never enters the navigation) and live at care-co.loveisahabit.org as a Kid Matters admin.
What. The console still shows the therapy finder as a tab of its own. It was supposed to live as one of the widgets on Organization → Website widgets, next to the others, with its own "Customize widget" panel. The tab should go; the widget should stay (it already exists and is live).
Done looks like. No standalone therapy-finder destination in the tool registry or the navigation. The finder appears only as a widget on the Website widgets tab. Nothing an organization saved is lost.
Drew, 2026-09-17: "already mentioned but incomplete --> the therapist finding tab still exists as it's own tab, when the code was supposed to be placed as one of the widgets on the widgets tab"
Drew, earlier in September 2026: "there is an entire tab called (The therapy finder …) and that should be added to the website widgets page and synced in so that as availability is updated it is all synced accodingly"
Twilio, in plain English
Drew asked for this in third-grade English. Here it is.
What Twilio is. Twilio is the company that gives us a phone number the software can use. With it, the console can send a text ("Someone sent you a referral"), get a text back, and — if we choose — make phone calls.
One login, not two emails. You already have a Twilio login: it is the loveisahabit.now@gmail.com account that has been talking to Twilio's sales assistant. You do not need a second email. Inside one login, Twilio lets you make more than one account. Twilio's own rep suggested two: one called LIAH Dev (for practicing) and one called LIAH Prod (for real). Both sit under the same email and the same password.
The honest catch with "dev". A Twilio account that has not been upgraded (a "trial" account) cannot send a real text to a real US phone. The carriers require every sender to register first (this is what "A2P 10DLC" means: application-to-person, ten-digit long code — a normal-looking phone number used by software). Trial accounts cannot register. So a dev account does not let us "test texting for real" — it lets us test that our code talks to Twilio correctly, which Twilio already gives us for free with test credentials and magic phone numbers (a pretend number that always succeeds, one that always fails, one that is blocked, and so on). Those live on the same production account. So the practical answer to "dev vs prod" is: one production account, and our code has a "practice mode" switch that uses Twilio's test keys and magic numbers. Nothing gets built twice, and there is no scary switch-over day, because the only thing that changes between practice and real is two keys and one phone number in the settings.
This is also the decision Drew made on 2026-09-09 (no dev environment, no second Twilio account — because it means building everything twice and hand-syncing forever). Drew re-opened the question on 2026-09-17. Both are recorded here; the recommendation is unchanged.
The steps, in order.
- Upgrade the account (add a payment card). This is the one Twilio calls "upgrade from trial". Link: https://help.twilio.com/articles/223183208
- Buy one US phone number (about a dollar a month).
- Make a Messaging Service and put the number in it.
- Register the brand (that's us, Love is a Habit, with our EIN 88-2736198). This usually takes minutes. Link: https://www.twilio.com/docs/messaging/compliance/a2p-10dlc
- Register the campaign (what the texts are for — "care-coordination notices, opt-in by keyword"). This is reviewed by a real person and takes about 10–15 days. Nothing sends until it is approved. Link: https://www.twilio.com/docs/trust-hub/registrations/a2p-10dlc-campaign
- Attach the number to the campaign.
- Send the first text — to Drew, who has texted the opt-in keyword first.
During the 10–15 day wait, we finish the console side using test credentials, so the day the campaign is approved we flip two settings and go.
Nonprofit discount — my guess, to be confirmed. Twilio has a nonprofit program (Twilio.org, the "Impact Access" program). My best guess at how it works: create the account first, then apply on twilio.org with our EIN and 501(c)(3) letter; once approved the discount is applied to the existing account. Drew asked Twilio's rep for the exact 501(c)(3) sign-in link on 2026-09-02 and it has not been answered in the thread. Action: reply to that thread asking again, or start at https://www.twilio.org and look for "apply". I am guessing at the mechanics; the program itself is real.
Risks of skipping straight to "real".
- Real texts reach real people while we are still finding bugs. A test loop that accidentally fires fifty times sends fifty texts. Our answer: one
sendSms function, an opt-in check inside it, a daily cap, and a kill switch. Nothing else in the code may send. - Carrier filtering. If test traffic looks like spam (same message, many times, fast), carriers quietly block the number, and getting un-blocked is slow. Our answer: practice mode uses the magic numbers, so real carriers never see test traffic.
- No safe place to test the STOP/HELP replies. Magic numbers cover this too.
- The fear that "moving from dev to prod is a nightmare" — this is exactly why we do not build a separate dev account. Practice and real are the same code, same account, different keys.
Where the real detail is. The full plan, with the code rules and the per-person toggles, is the Twilio plan page (twilio-plan.pages.dev, same passcode as the plan page). Every fact above about what trial accounts can and cannot do was checked against Twilio's own documentation on 2026-09-09.
Voice (calling) — see CC-32. Everything above is about texting. Calling would add a Voice-capable number, browser calling through Twilio's JavaScript SDK (version 2 — version 1 is retired), a small backend endpoint that hands out call tokens, and the extension/voicemail logic. Link: https://www.twilio.com/docs/voice/sdks/javascript/get-started. Voice was taken off the plan on 2026-09-09; Drew's 2026-09-17 message puts it back as a question to decide.
Badges, first thinking
What Drew asked for: deep thought, not a build. The worry is real — a clinician who feels watched will stop using the tool. Here is a starting frame.
What a badge is for here. Not ranking. Not pressure. A badge is a thank you that is hard to fake: a quiet way for the network to notice that someone kept showing up for other people's clients. If a badge can make anyone feel behind, it is the wrong badge.
Principles to test each idea against.
- Celebrate helping, never volume. "Answered a referral within a day" is helping. "Sent 50 messages" is volume.
- Private by default, shareable by choice. A person sees their own encouragements. Nothing appears on their public card unless they turn it on.
- No leaderboards, ever. Comparison is the fastest way to make a kind tool feel invasive.
- Nothing derived from client content. Only from that a conversation happened, never from what was said. Counts, not contents.
- Say what we count, in the settings. One plain sentence next to each badge: "This counts how many referrals you replied to. It never reads the message."
- Encouragement over achievement. "Three practices reached a client faster because you answered" lands better than a gold star.
- Off switch. One toggle turns the whole system off for a person, and for a whole organization.
Ideas worth considering (none decided). Quick to answer — replied to a referral inside a day. Bridge builder — first conversation with a new partner practice. Steady hand — availability kept current for a month. Open door — accepted a client from a partner. Team player — helped a colleague in the team chat. Each of these is about the person's care for others, and none needs to read a message.
Open questions for Drew. Do badges appear on the public directory card at all? Do organizations (not just people) earn anything? Is there a yearly "thank you" moment rather than a stream of them?
Branding: liah.cc
Where the names come from, in Drew's words: LIAH is Love is a Habit. The console lives at care-co.loveisahabit.org because "care coordination console" was too long for a domain and "co" was the overlap. care.co was taken; liah.cc is short and nice.
What liah.cc could do. The zone is already on the LIAH Cloudflare account with no records on it (2026-09-17). The widget host already uses widget.liah.cc, so the name is in front of practices today. The console could become liah.cc (or app.liah.cc), with care-co.loveisahabit.org kept and forwarding forever.
The trust question, honestly. ".cc" is an ordinary ending (Cocos Islands, sold worldwide). A practice owner who does not know that may pause for half a second. What makes a link feel trustworthy is mostly what is around it: the person who sent it, the padlock, a clear page with the nonprofit's name and the same colours they saw in the email. "Love is a Habit" spelled out in the page header carries the trust; the domain carries the memory. liah.org, if it comes, ends the question.
What has to change if we say yes. Every invite link, every sign-in email, the extension's allowed hosts, the widget embed snippets each practice pasted into their website (these keep working because widget.liah.cc does not move), the plan page links, and the docs. It is a list-and-test job, not a rebuild. Recommendation: decide after the joining flow (CC-27/28) is rebuilt, so new links are minted once, at the new address.
Done
Most recent first. Each line says how it was verified.
- 2026-09-17 — Website widgets: Customize panels, Brand popup, "How many people a search shows" (1–12), font upload, colours from your logo. Console PR #166 → main
f6d8e7c; deployed by CI; checked live in Website widgets. Partially covers CC-13. - 2026-09-17 — Widget host: guided (one-question-at-a-time) finder, uploaded fonts, reads the console's matching settings when its own are empty. care-platform PR #23 → main
d9d2ed8, deployed to widget.liah.cc with the guardrailed deploy script; fetched live. - 2026-09-17 — Kid Matters is the first organization with its own console layer. Vertical repo
liah-console-kid-matters synced in; SDK bearer-header bug fixed (PR #157) with a test proven to fail when the bug is put back; org active with 12 logins. - 2026-09-17 — Therapy finder, openings badge and how-it-works mirrored in Website widgets. Console PR #165; checked live.
- 2026-09-17 — Widget host moved onto LIAH's own Cloudflare account (widget.liah.cc). Every Kentlands hostname removed by derivation; deploy guardrail caught five more; console search still proxies to the matcher (
MATCHER_BASE move is on Drew's list). - 2026-09-11 — Optional password door beside the email link (8 characters, set or reset by email). Live for teachcreate and liah-console-test.
- 2026-09-11 — Secure-chat unread indicator (PR #154). Live.
- Early September 2026 — Kentlands residue sweep. Default colour no longer Kentlands orange; "clinician console / coordinator console" naming replaced; gate added. CC-30 finished the residue on 2026-09-17 (PR #174).
Drew's words, kept whole
First message
The request that created this document, 2026-09-17, exactly as written (one invite link shortened because it carried a personal sign-in token). This is the source for every quote above. Nothing is lost if the items above ever drift from it.
create a document of all the to do lists here for the care console... every word i give matters as it's an instruction that needs to be documents organized and remembered and we need to plan accordingly, make sure every word and correlated instruction and goal is record and touched up for grammar in the plan document for me to view her in claude accoridngly... all that we've mentioned... all that is coming up at the top and whats done once finished move to the bottom... all in plain english and updated at the end also make a habit of adding my exact words somewhere at the end of the specific reqeust so my words are always saved and heard and never lost, falling on deaf ears, this also allows me to improve my prompting over time)... I need this to do list to be saved in the repo, beautiful and viewable easily as the most recent care console to do list for any other team members to hop in and join through github
already mentioned but incomplete --> the therapist finding tab still exists as it's own tab, when the code was supposed to be placed as one of the widgets on the widgets tab
Our list tab --> worthless and should be removed everywhere
The clincians info --> specialities and all variables need to be updatable in the console by the own clinician(not just the admin --> the edit teamm ember for admins pop up is awesome and it could be like that for clinicians updating (great ui /ux already there to duplicate or use in some other way when someone clicks update my info on the clinician home page availabiltiy view (i like popups for this better)
We need to create a secure database for all of the data to live for all of the clincians in the care console (rather than ghl being the source of truth, on the to do list we need to add the ability to update ghl custom objects for KP and all practices so that the token can send updates directly to the care console and back and forth automatically)
We need to go through the kp matching tool "charm" (all the little things we updated on kentpsych repo to make the matching tool match well (and redo and simplify the matching sliders so that it can do lots of user firendsly high quality things like set the limit of clinicians that could pop up (aka best 1 to unlimited)... even all of the key word syncing we used... we should alsoo give poeple the choice to add a ghl form in their or send a text message or something else like send a templated email (and edit that templated email)
we also have already made all these updates on the call script to allow the call note to be a popup that sits behind the bottom nav bar (just like the old kentpsych coordinator console, which you can view), it also had an easy ui/ux beautiful easy to navigate notes situation and easiely went from our org, to partner org to another org (I AM SO FRUSTRATED BC IVE ALREADY UPDATED THIS TWICE BUT THE EDITS DIDN'T SAVE FOR SOME REASON)... this is why the to do list and making sure everything is updated is so crucial (our original clinician console also had teh ability to send the script note autofilled via the form (you can leave a place for a ghl form embed code and auto place the form from the kentpsych coordinator console over to the care console ) ----> the call note pop up was so smooth on the old coordinator console... i loved it... same with the old popup coordinator console matching system... so smooth from any point in the call it was easy to match (maybe another little button next to the note button on the script that says "match now" and invites the popup than with all the relevant info added... remember the goal is for this all to allow high qaulity matching for coordinators to first see their org, than other partner org, than other relevant orgs... i feel sad bc i've tried to update this with you before but you never were able to make it fully work so i really need your help to make it work this time...Finally the cockpit tools should all be accessible in the call script, we can fully put the cockpit accessible in the script tab and remove the script tab... maybe only for the script tab when we open the script tab the bottom nav bar transforms into a full section and a new adapted script experince bottom nav bar appears (the first goal is to COMBINE cut the Cockpit and the care and support tab place it into the script tab --> combine all three into one script tab(the two other tabs content can be on the top nav bar of the script tab or something
Also the fact that the clinician comes to the home pages and it auto opens to availablitiy is great... but also they already have access to their profile thorugh that it seems odd to me that availability content is NOT available in the profile tab... (to me maybe there should just be one availability, landing page tab for clinicians which gives access to updating ones profile in the popup
Also why does on the clinician tab the call script. button pop up on the homepage... that needs to be corrected
Also we need to correct the nav bar in all screensizes and view ports at the top of the care console... it sizes incorrectly, glitches, changes and has a ton of errors... do a ui/ux breakdown to finetune it, fix it and verify it's completion
The directory tab also doesn't make much sense for clinicians (it might be better as a "Send a refferal tab"... and speaking of the data base for all orgs on our care console... we need to start beafing up and scraping data and encorporating unverified practices into our searches (varified practices only inlcude kp and kid matters which need to be made partner orgs manuelly--> add to the to do list) we need to start building the search functionality for partner orgs as well so clincians can do easy searches to reffer or research and other individual too (we need like a green checkmark for varied orgs and clinicians on the profile avatar)
Also at the top of the messages tab I think should be a search bar to easily search and find or create a new chat. build and verify, double check and think through for quality assurance and userfirendliness... these are all crucial decisions...
Also as for the Plan & support page and disclaimer get rid of the try as kentlands language, we do not want kentlands to be attached to us any more than any other practice (we started there, but have to cleanse the codebase accordingly) ---> I actually do want creating a call script inside the care console to accessible and possible for everyone... admins /owners should have the easy ability to create and manage the call script and edit it and test it easily (maybe after we update and consolidate those mulitple tabs into one call script tab we can allow admins /owners to have a edit /customize the call script banner at the top of their screens) --->also on the fact the message tab for a practice with no hippa compliant chat widget it still invites clinicians and coordinators to "connect a chat widget" --> they couldn't do that anyway... that is the admins job or the owner and so we need to change the tab if a widget doesn't exist yet to allow it to say something else that they can ask their admin to set up the hippa complaint chat or just use the non phi chat below (something like that will help) & on the note of the messaging tab, after we make the updates to the directory / make a refferal page, individuals could be encouraged with a button that turns to a popup to search for new clinicins or programs in the parnter secotion of chats and begin a conversation (potential complementary refferal suggestions is a directory algorithm that matches refferal gaps to allow for potential parnterships in nearby areas that support each other in key gaps... a make a potential partnership connection button should be available and it could be either with the entire practice(going to one representantive) or with an entire practice... if a org staff member goes the "make a refferaL" that is about a specific client and the button they click should be based around that idea and both partner and refferal contacts should be tracked accordingly... refferalls also are where reaching out via a hippa compliant widget is sometimes ideal... another way that clinicians might organize their contacts are partner therapists who they're managing a client with, this a coordination of care situtation is a prime example of why we have talked about a "schedule a call" feature that can be accessed in messaging converssations --> chats and tools like this can also be used to track activity to give poeple active clinican encouragements or badges(this needs to be thought deeply about as it shouldn't come across invasive so be thoughtful on our badges... as of now i like our structure so lets plan and think and brainstorm on the whole badges system, just things to think about)
another to do list item is to make sure that we are prepped to incorporate twilio into the mix... while i would prefer to skip dev altogether and jump straight to twilio live, i understand dev is beneficial... a section at the bottom should be dedicated to explaining to me in plain english simple 3rd grade english how we will create an entire dev version of the software to run while as create a prod version of the software to the the other twillio acocunt... also which emails should those twilio accounts (one dev and on prod) be using ??? should we create two emails to create the accounts, and how do we get the nonprofit discounts once it's time (see gmail connector to find the email with twilio if needed). on this plan put the link to take action and give me your guess as to how we will get the nonprofit authorized and steps for dev build out a2p for prod and also risks if i skip straight to prod (because I am not scared of doing a2p --> partly beecause i am more scared that when i try to switch from dev twilio to prod twilio it will be a nightmare that i have to test all over again( but than again it sounds like it will be good practice and probably save me headaches when tests new text systems live down the road so we don't get blocked) --> as you've seen in my other plans... our goal is to have one twilio number for text notifications (and two way texting) for the whole care console as is turned on per chat / indiviaul + We will also give every therapy org user (and every org staff user call ability through a shared number where everyone was automatically given an extension and than call back automated voicemail at the beggining of someones voicemail when an org member calls out via the care console it can say ("Call back xyz phone number at extension "5671" to connect, again that's extension "5671" ---> than when the call back it might be good for it to start with just a number routing for extension (and that automated voice message with the extension inserted)... this will allow calling if necessary (and we can put safeguards evntually if we encounter problems)... also many orgs phone calling ability will be off (another thing to research about twilio is whether or not hippa compliance can be turned on via prod later and if so, how easily or not so easily)
ALSO on the edit team member popup on the team tab i think we should have a floating save button in the bottom right corner (auto save might be annoying with how long some of the things are), we just need a warning if they made changes and go to click out that prompts them to save or not save... I think that's a good ui ux fix for that particular functionality... think about it and optimize!
Also as for the invite an org tab ---> I think we should give them a message to send (and not send emails on behalf of them--> we need them to send the email)... secondly we want the email to essentialy have two email or text options ---> 1) being Hi to help coordinate care join here Or "to be able to more easily send your refferals to clients could you fill out this form to make sure the inforamtion i have is up to date..." get creative with it... i like waht we have here --> Just would definitely like to adpat it -->Hi there,
We use a shared referral network to coordinate care and send and receive referrals between organizations. I'd love to have you in it.
You can join and set up your own free portal here (takes about a minute):
ON THEM JOINING! when they get sent the link --> it should ask them to authorize via their work email (your partner org might have already added you" check and see insert work email and if they're in the system send an auth email and streamline onboarding, if not the onboarding link has got to be type form inspired ---> email, simple questions, live introduction to the software (partial software access granted, but locked until their org is authorized (it needs to be varieifed via 1) website 2) business email & somethine else simple (because I don't want each org to have to onbaord all themselves because we can do it quicker sometimes, take website, verify org, verify domain, verify and strip photos and get consent to get live photos and info from their website --> than go through a loading screen -> than have them complete the user agreement, this is similar if an org was already pre registered as well and they're just joining... than after in we want them to utilize the offerings and get set up with the features ) https://care-co.loveisahabit.org/join?invite=… (personal token removed)
(Again we don't want emails sending from our platform directly to partners) we need to encourage each org to reach out directly)
more on this... I'd love to streamline their joining process which we need to make a list for so we can easily get new people who join understanding how our nonprofit systems work! (on all of these links we need to think about liah.cc and the brand switch to have the backend live at liah.cc ---> I am not sure what this will do to our branding, I definitely still want forwarding.. I think it's okay for the care console to be called liah slang (eventually i bleieve we'll get the liah.org domain so it might all fit very easiily --> coordinating care liah,org) branding is something to think about... LIAH care console is where Liah.cc comes from liah is loveisahabit. that's also where. Care-co.loveisahabit.org comes bc i wanted the domain to be shorter when i made it and couldn't chose care coordination console or just care console so i took the overlap co... I thought about the domain care.co but that's taken and liah.cc just is so short and nice...liah care console liah.cc www.liah.cc i hope that comes across trustworthy and secure... it might take some getting used to...
Second message, later on 2026-09-17
Sent with two screenshots of the Messages tab (an empty thread with the "Latest" pill showing, and the "+" menu with five greyed options). The middle of the message re-pasted the first message word for word; only the new parts are kept here.
this is just a check message with a few updates ---> call the plan liah.cc - operation takeoff
guantee the document is well organized and easy to understand... check our plan has rightly noted everything and added the new things i mentioned at the end verbatim with gramatical fixes recorded... before so we can start implementation (start implementation after quality has been varified --> we have a long list of updates) as they are completed mark down the complemetion and the push that confirmed it so we have a history --> update the document plan with every git push
another thing i want to add as of now(not in the last message) is that the icon on the far right on the nav bar should be the users avatar logo icon or image (whatever we decide) and the other logo / initials are on the left ( i think it might not hurt to put orgs logos potentially on the care console nav bar as optional too (I think we should eventually consider each org having their own login page like kidsmatter.liah.cc or something like that --> admin could chose their subdomain maybe --> or it can auto chosen for them) --> decide the better option for the plan
Also another new thing as seen in the image their is a latest floating icon above the messaging box... that's fine to have but it needs to be relevant and work...IT WON:T DISAPPEAR !! SUPER ANNOYING, if we're going to make any updates like this it needs to be quality... no messages have been sent so we need to fine tune and develop this feature (and any other helpful messaging features) ALSO !!! when I click "+" on the chat none of the options are clickable and have been bulit out yet... we need these to work,,, as seen in the second photo each of those + options like coordinating a call helper should all be test, verified and working
also for now every single org who joins the care console should automatically have a shared room with the liah team for feedback so that liah can help manage and support (this should be synced and show up in Drew's dashboard and visable to all liah team members--> these are liah enabled rooms --> speaking of the mini crm dynamics in the liah enabled rooms needs a freshening up too... completed items her and suggestions would help)...
spealing of messaging features... we talked alot about a plan to have the notifications that come from ghl trigger a relevant notification in the hippa complaint chat widget popup button to guide individuals there when the webhook is triggered via the remove tag automation notification on the care console work around... lets verify this has been done and will get done if it hasn't yet