Nenda kwenye maudhui
KAMOUniverse

Mabadiliko

Nini kipya.

Kila kuboresha jukwaa, kama ni meli.

  1. · kamo-shared-library

    Count DISTINCT events across a rollup's owners

    A phone number can be a contact point on more than one owner at once, and two leads on one account sharing one is the ORDINARY case — they are commonly the same person enquiring twice. The matcher writes a row per owner deliberately, because each of those timelines needs its own. Summed over a rollup, plain counts report one call as two, and a rail reading 11 above a list showing 6 is worse than no rail at all. COUNT(DISTINCT sourceId) over a list of owners of one kind counts exactly what the merged timeline shows, and it turns an account's 26 count queries into two.

  2. · kamo-internal

    Gate the line window per member, and show which mode you are in

    The rollout control. Default off, opt-in per member from Phone Settings, nested under the VOIP switch because it means nothing for a member with no line. One hook owns the gate so its two consumers cannot disagree — the NavTop control that opens the window and the strip inside the softphone that offers it. A member who sees one but not the other is worse off than one who sees neither. It fails closed: an unreachable config read hides the feature rather than offering a window that may not work, and the in-page softphone is unaffected either way. The strip answers a question a member otherwise cannot see the answer to: is this call safe from a refresh? With the window open the call lives there and the panel is a remote control, so it offers to bring the window forward — a popup can end up behind the browser and nothing else can raise it. With the window closed the call lives in the tab and a refresh ends it, so it says so. Inbound now uses the one gesture it can produce: the desktop notification's click, which previously had no handler at all.

  3. · VOIPService

    Read and write the line-window gate

    GET surfaces lineWindowEnabled; PUT accepts it as a nullable Boolean so that absent means "not mentioned" and preserves the stored value. Every existing caller of this endpoint omits the field, and treating those as false would switch the feature off for any member whose phone settings were saved for an unrelated reason. Also normalises a null read to false before saving: a row written before the column existed can still carry null through Hibernate, and the column is NOT NULL.

  4. · kamo-shared-library

    Map LINE_WINDOW_ENABLED on MemberVoipConfig

    The per-member gate for the dedicated softphone line window. Version pin stays at 1.6.0 — adding a field needs no bump, and a split pin would break CI for every consumer at once. The column already exists in the database; the DDL landed in KamoInitializer first, deliberately, because a service that maps a missing column fails at bean creation rather than degrading.

  5. · InitializerService

    DDL for the per-member line-window gate

    Adds **************** defaulting to false. A column rather than a feature flag because there is no flag system here and MemberCapabilities is a closed, fail-closed set with no spare slot — so the control lives beside the member's other phone preferences, which is also where an administrator would look for it. Its own runner rather than a line in MultiInstancePhoneMigration because the ordering is the point: KamoInitializer has no CI, so this is applied by hand and must land BEFORE a VOIPService build that maps the column, or Hibernate fails at bean creation and the service will not start. The ALTER has already been applied to the live database for exactly that reason; this runner makes it repeatable and idempotent for every other environment.

  6. · kamo-internal

    An AI sub-tab in the Interaction Center

    AI sessions live in AIService and are not MediaSessions, so this bucket gets its own loader in front of the /api/media/sessions/chat fetch the other four share, then adapts each session into the same Row shape rather than a second renderer — the list, its search, its avatars and its unread chip are one surface, and a second one would drift from the first within a release. Unread comes through the ai: namespace the hexhead badges read (getUnread, not conversationKeyForWindow — a chats-list row is not a tool window), so a row and its minimized window can never disagree about the number. Three gaps beyond the plan's own brief, caught before they shipped quietly: bucketOf() never routed sessionType 'AI' anywhere, so every AI row resolved to 'teamMembers' and the AI tab would have rendered permanently empty; the row's own title never read row.title, so every AI conversation would have shown as "Empty chat"; and 'ai' was missing from ALL_BUCKETS, so the per-member sub-tab preference would silently refuse to restore it after a switch. The brief's own unreadFor() example is also not used as written — replacing the existing unread computation with it wholesale would have zeroed every SOCIAL row's badge (never in the live map, unlike CHAT/SUPPORT) and flashed every row to zero before the live snapshot loads; the AI branch is folded into the existing computation instead, preserving both.

  7. · kamo-internal

    The dedicated line window

    The call now lives in a window a refresh of the workspace cannot touch. SoftphoneProvider becomes owner-or-follower. Only the owner REGISTERs and opens the STOMP socket; a follower renders mirrored state, sends commands over the bridge, and stays silent — no ringtone, no ringback, no event sounds, because it holds no session and its Answer button would do nothing. Where Web Locks is unavailable the tab simply owns the line and there is no window, which is today's behaviour: never degrade into two registrations. /phone renders through a third AuthedChrome branch carrying SoundProvider and SoftphoneProvider and nothing else. Excluding ToolWindowRestore is load-bearing — its guard is module scope, so a fresh document would replay the member's whole dock inside the popup — and so is excluding SessionManager, which navigates to /logout on expiry and would delete the shared Redis session out from under the workspace tab. For the same reason useUserInfo and apiClient now refuse to self-navigate from this route; a lost session is rendered, not redirected. The window opens synchronously from a click in NavTop (transient activation does not survive an await), is named so a second click focuses rather than duplicating, and reports a blocked popup instead of failing silently — the in-page softphone keeps working either way. Inside: a Start gate that buys sticky activation before anything needs it, reload-key swallowing, a close confirm, and a screen wake lock.

  8. · kamo-internal

    A communications tab that rolls up the account's leads

    The level most people mean when they say "this customer's calls". Several leads commonly hang off one account precisely because they are the same person enquiring twice, and each of their calls was matched to whichever lead held the number at the time — so reading one lead's timeline answers a narrower question than the one somebody looking at a customer is actually asking. This renders the platform's existing timeline, not a second one. The spine is lead's history draws an account's given a different base. One matcher, one The tab says what it is NOT showing. The server gates each rolled-up lead on the same rights the lead page applies and caps how many it rolls up, and both are surfaced: "N leads are not shown because you do not have permission" is a different sentence from "this account has more leads than we roll up at once", and one banner for both would hide the first behind the second. Notes and chat are absent rather than empty. Both are lead-native — a note lives on a lead's media stream, chat on its social link — so an account has nothing to put there, and a permanently empty tab reads as broken rather than inapplicable.

  9. · KlusterServices

    Bounce a bare visit to the Kamo login, and end the session on tab close

    Closing the KamoDesktop tab left a live session sitting on the machine. Guacamole keeps its auth token in localStorage (GUAC_AUTH), which survives closing the tab, closing the window and restarting the browser, and the token stays valid for 60 minutes past its last use. So the next person to open the gateway on that computer inherited the previous person's desktop. Reproduced end to end. Two changes, deliberately both server-side first. The edge now redirects a bare document request to login.kamocrm.com. KamoDesktop has exactly one legitimate entry point — a signed, single-use link minted for someone already authenticated to Kamo — so a person typing the hostname, or restoring a bookmark, or letting the browser autocomplete it, has no business seeing Guacamole's password box. Serving one offers a credential-stuffing target for the `sage` account and lets a browser holding a live token silently resume a session. Scoped to Path(`/`) on purpose: a cold SPA load pulls ~41 assets and API calls that carry no ?data=, so anything broader breaks the application outright. The #/client/... fragment never reaches the server, so a bookmarked deep link is a bare `/` here and is bounced too — which is correct. And the token is now revoked when the tab goes away, rather than merely forgotten: DELETE /api/session with the Guacamole-Token header (verified live — 204, and the token stops working immediately). Skipped for a bfcache freeze, so pressing Back does not sign anyone out. kamo-reconnect.js is gone. It existed to paper over dropped sessions, it took the desktop down twice, and the causes it was compensating for — deploys restarting the gateway, and the OOM — are fixed at their source. What replaces it is a quarter of the size and its worst failure is that you are signed out, which is the outcome it is trying to produce.

  10. · kamo-internal

    Line ownership, the bridge, and the no-yank handover rule

    Three pure modules with no consumers yet, so this ships inert. lineOwnership elects exactly one SIP registrar per browser via Web Locks. That is mandatory here rather than defensive: verified live, every FreePBX AOR is max_contacts=1 with remove_existing=yes, so a second REGISTER evicts the first and the evicted document keeps showing "Registered" while receiving nothing. Web Locks needs no liveness protocol — the lock belongs to the agent, so a crashed or task-killed window releases it and the next waiter is promoted. Where the API is absent we report "unsupported" and the caller keeps today's behaviour rather than risking two registrations. lineBridge is a versioned BroadcastChannel envelope. Not window.opener: that goes stale exactly when the opener reloads, which is the event this whole feature exists to survive. Version skew degrades to a stale remote control, never a throw in a live call UI. The mirrored state is an allow-list — directory data stays where it is loaded. lineHandover is one rule: a live call is never yanked.

  11. · kamo-internal

    The tab title counts everything waiting, not just chat

    The tab strip has room for one number, and 'chat and support only' stopped being what a member is waiting on once SMS, AI, missed calls and unreviewed reports each got an indicator.

  12. · kamo-internal

    Badge the AI option with unreviewed replies

  13. · kamo-internal

    A total on the phone icon, the breakdown in the menu

    This icon carried no badge at all, so a missed call while the softphone was closed reached the member nowhere. Opening the phone clears the missed count.

  14. · kamo-internal

    A total on the icon, the breakdown in the menu

    pendingCount was the whole badge, so unreviewed bug reports and webinars booked on somebody's behalf had no way to reach anyone. Opening the webinar row stamps them seen, so the count can actually go out.

  15. · kamo-internal

    The chat badge counts chats

    It was silently chat plus support, so a ticket lit the chat bell and nothing distinguished the two. Support has its own icon now; social joins chat, because a social conversation lives in the Chats list and has no icon of its own.

  16. · kamo-internal

    Let a dropdown row carry its own count

    The icon shows the sum and the rows show the breakdown — the point of splitting the indicators is seeing WHICH thing wants you without opening anything.

  17. · kamo-internal

    Thump on arrival, with the glow dissipating outward

    Its own layer inside .hex-scale: that element already animates transform for the hover grow, and two animations on one element do not compose — the second replaces the first, so a thump mid-hover would drop the hover scale. Keyed rather than class-toggled, or two messages in quick succession thump once. Gated the scale-pulse on a `.thumped` class present only after a real thump: the key change makes `.hex-thump` a fresh DOM node both on a real thump AND on first mount, and CSS cannot tell those two apart from the key alone, so the brief's literal wiring would have played the pulse on every head a restored session spawns — the "screenful of pulsing hexagons" shouldThump exists to prevent.

  18. · SecurityService

    An account-level communications timeline

    The level most people mean when they say "this customer's calls". The spine is already keyed on an owner, so the account endpoint is the same merged, component renders it with nothing but a different base. CommsSubjectResolver decides which owners a timeline may read, and it is the one place that answer is allowed to be wider than the subject asked about. Only ACCOUNT expands. A lead does not inherit a sibling lead's traffic: the two may be assigned to different people. A patient inherits nothing at all, whatever the account graph says — a chart showing a CRM lead's messages would be a disclosure, and that test exists to keep it that way. The rollup is capped at 25 owners and the response says when the cap bit. A rollup that silently drops owners is a timeline missing messages for no visible reason, which is worse than one that admits it is partial. Cross-org and missing accounts get the same 404 so the response cannot be used to probe which accounts exist, and the miss is audited too. resolveOrgId rather than a new synonym: the endpoint ratchet recognises that name, and it reads one method body without following calls, so a helper it cannot see reads as an unguarded endpoint.

  19. · kamo-shared-library

    Let an account roll up its leads' communications

    Several leads commonly hang off one account precisely because they are the same person enquiring twice, and each of their calls was matched to whichever lead held the number at the time. So "this customer's communications" is the union of the account's own rows and every one of its leads' — reading one lead's timeline answers a narrower question than the one being asked. findUidsByAccount projects uids rather than entities on purpose: Lead sits in a JOINED inheritance hierarchy, and selecting entities here would drag the whole fetch plan in to produce a list of ids. PhiResourceType.ACCOUNT so the account timeline's audit rows name what was actually read. Appended, never inserted — the enum is ordinal-persisted.

  20. · kamo-internal

    One context for the four indicators' counts

    Composes what exists and owns the three that had no home: unreviewed bug reports, unreviewed webinars, and the phone's missed calls and voicemails. The 60s foreground tick is not belt-and-braces — several of these arrive over core NATS with no replay, so a frame published to a sleeping tab is not coming.

  21. · kamo-internal

    The drop beacon reports which path ICE actually chose

    The beacon shipped iceCandidateType as a permanently-null field. That is worse than omitting it: it makes the telemetry look more informative than it is, and this is the one measurement that can redirect the whole line-window project. The web softphone builds its peer connection with one hardcoded public STUN credential payload has never carried it. So a member behind a symmetric NAT, a VPN or carrier CGNAT can fail to establish media with nothing to fall back to, and what they report is "the call dropped". After the fact that is indistinguishable from "I hit refresh", and the two have completely different fixes — one of them two lines of TURN config rather than a new browsing context. `relay` / `srflx` / nothing is the evidence that separates them. The SIP.js path reuses the candidate pair its existing diagnostic dump already computes, so it costs no extra getStats; the RingCentral path samples raw.rtcPeerConnection on answer. A null reading is RECORDED rather than skipped — "ICE selected no pair" is the relay-less failure and the most valuable reading available — and callDurationSeconds separates it from "the call ended before we sampled". Also records in the plan what actually shipped, including the Task 6 guard that differs from the plan as written.

  22. · kamo-internal

    One day picker, shared by the calendar and the settings tab

    The two surfaces had each grown their own answer to "which days?". The settings tab put Workweek Days, Weekend Days and Every Day behind a dropdown; the calendar editor had a multi-select ribbon and no groups at all. Whichever a member learned first, the other could not do what it did. They share AvailabilityDayPicker now — the group buttons AND the seven chips, on both pages, defined once. That is the only arrangement that keeps them the same the next time either is touched. Groups select rather than save. Pressing "Workweek Days" turns on five chips and nothing downstream interprets a sentinel: the member can immediately deselect the Wednesday they are away for, and what reaches the backend is what it has always taken, one window per day. A group button reads as on only when EXACTLY its days are chosen, so the control can say "this is the workweek" instead of leaving somebody to count lit chips. The settings tab gains something it never had — an arbitrary multi-day pick. "Monday, Wednesday and Friday" was not expressible there before; it was one day or one group. daysToSave therefore takes a day LIST rather than a day-or-group, which is what lets both surfaces share a selection type. Both guardrails still hold and are still pinned: an edit never expands into several rows, and a multi-day pick never collapses to one. The panel's "What does this mean?" now starts collapsed for everybody. Opening it for a member with nothing declared was meant as a kindness and was the wrong one — it pushed the list and the button that starts the whole thing down the column, so the first thing a new presenter met was a wall of prose rather than the control the prose is about. Also raises the timeout on the sound catalogue's whole-tree scan. It reads and concatenates every source file in the app, lands at 6-13s whenever the machine is busy, and was failing the suite against vitest's 5s default — with a message that named no event, because it never reached its assertion. The scan is worth keeping; the default timeout is simply not sized for it.

  23. · kamo-internal

    Badge a minimized window with its server unread count

    The one component that sees both the window list and the unread maps — the engine knows nothing about conversations and the unread context knows nothing about windows, so neither could answer this alone. Dispatches only on change: the thump keys off an increase, so a repeated count would look like an arrival and the heads would pulse on the refresh timer.

  24. · kamo-internal

    Set an absolute unread count from the server

    Replaces hexhead:incoming, which nothing ever dispatched. Its +1 would have counted a re-delivered snapshot as a new message, and its spawn-if-missing branch would leave a head whose window does not exist — clickable, and inert.

  25. · kamo-internal

    Messages and calls, on the existing timeline component

    CommunicationsTimeline and EmailEntry take an optional endpointBase and keep the lead path as the default, so every existing call site is byte-for- does not care what the timeline is ABOUT — the owner keying is entirely server-side, exactly as MediaStream takes a feedEndpoint and the DocManager takes an (assocType, assocObjectId). A patient's messages are drawn by the The contact-point strip above the timeline is the part that would otherwise be missing. A timeline is a view; inbound calls and messages only ever LAND on an owner the matcher can resolve, so a patient with no registered number has an empty timeline for a reason nothing else on the screen would explain. Its empty state says exactly that, rather than implying nothing ever happened. 3149 tests green, typecheck clean.

  26. · SecurityService

    A patient's calls, texts and email — on the platform's own spine

    Not a second timeline. LeadCommunicationsService now takes a CommsSubject and the lead overload delegates to it, so /leads keeps its exact behaviour drift. A patient is just another owner. Notes and chat appear only for a lead, and that is a fact rather than a guard: notes come from a lead's media stream and chat from SocialRemoteUserLead, neither of which exists for a patient. Both sources already return empty when their link is absent, so a patient gets matched traffic and nothing pretends otherwise. Registering contact points is the half that makes it work. Pointing a timeline at a new owner changes what is READ; inbound traffic only ever LANDS on an owner the matcher can resolve, so a patient whose number is not a contact point receives calls that resolve to nobody. Registration is point-in-time like the lead spine — changing a number closes the old point and opens a new one, so a call from two years ago still explains what it matched — and it applies the same seven-digit floor, because a four-digit extension would otherwise match every patient sharing those digits. Reading any of it needs a care relationship with the patient, exactly like the chart, and every entry shown writes its own audit row. A voicemail from a patient is as identifying as a problem list. The email-body endpoint re-checks the row against the patient in the path. The care-relationship check is satisfied by the patient in the URL, so without that second check a clinician with a relationship to one patient could read any message in the practice by supplying its id — the lead endpoint makes the same re-check for the same reason. Its 404 is one answer for "not this patient's", "not an email" and "not locatable yet", so the response cannot be used to probe for message ids. Also here: the TEFCA exchange controller — a patient's §164.528 accounting, everything sent to one partner (the query somebody runs the hour a breach is reported), Part 2 disclosures as their own question, and the cross-organization match review queue. 1278 tests green.

  27. · kamo-internal

    Stop losing calls to session expiry, teardown and stale devices

    Four guardrails that share two files, so they share a commit. A live call now counts as session activity. ClientActivityTracker equated presence with input events, so a member listening on a long call read as idle: the Redis TTL decayed, the expiry warning opened in a tab they were not looking at, and /logout tore down the tree holding the SIP session. Routed through the existing markNow() so logout's disable still wins — sessionMonitor's own floors (120s activity window, 60s re-extend minimum) govern the cadence, so this cannot run away. A document that dies holding a call now says so. pagehide fires a best-effort BYE — the far end previously sat on a dead leg until the registrar timed the contact out, up to ten minutes at expires:600 — and a keepalive beacon carrying member, org, provider and hadLiveSession. Nothing anywhere counted how often a teardown actually costs a call, so there was no number to weigh a bigger fix against. The beacon allow-lists its fields: no remote numbers in a pod log, and ids stay strings because they are int64. A second tab no longer rings with a dead Answer button. The VOIP STOMP topic is a broadcast, so every open document set hasIncomingCall while only the one the provider sent the INVITE to could answer. Gated on the session rather than the SDK, because with two tabs open both hold a registration. A device change reaches the call from any window. saveAVSettings dispatches a same-window CustomEvent; only useAVSettings also listened for the cross-document storage event, so every call-path consumer missed a change made elsewhere — silently, because applySinkId swallows every failure. One subscribeAVSettings now covers both halves.

  28. · InitializerService

    Widen the comms spine to an owner, in the order that keeps it safe

    LEAD_CONTACT_POINTS and LEAD_COMMUNICATIONS gain the document manager's (assoc_type, assoc_object_id) pair so an account, a patient or a commerce record can own communications without borrowing a lead. Four steps, and the order is the whole point. Backfill first: every existing row is a lead's, so assoc_type = 'LEAD' and assoc_object_id = lead_uid::text. Running it after the constraint swap would build the new unique index over nulls, enforcing nothing. Then swap the unique constraint onto the owner pair. (org_id, lead_uid, channel, source_id) is what makes ingestion idempotent — a RingCentral call is seen twice, once by the webhook and once by the reconciliation sweep — and with lead_uid null for a patient, Postgres treats every one of those NULLs as distinct, so the second sighting inserts a duplicate. The new constraint is added BEFORE the old one is dropped, so a failure between them leaves the table protected by one rather than by neither. Only then relax lead_uid's NOT NULL. Hibernate's ddl-auto: update adds columns but never relaxes a constraint, so this cannot be left to the mapping — and doing it before the swap opens exactly the duplicate window above. Then verify, by reading pg_constraint and information_schema back. The clinical primary keys were correct in Java and wrong in the emitted DDL, and only reading pg_constraint showed it. A row left with no owner type would never appear on any timeline, so that case logs an error rather than reporting success. Idempotent throughout, and a missing table is skipped rather than failing the boot — an exception here would take every later runner with it. 176 tests green.

  29. · kamo-shared-library

    Key the communications spine on an owner, not a lead

    The spine was built lead-shaped — LEAD_CONTACT_POINTS.LEAD_UID, LEAD_COMMUNICATIONS.LEAD_UID — and the association it actually needs is broader. An account owns several leads, typically sharing the same point of contact because they are the same person enquiring twice. A commerce record hangs off the account rather than any one lead. And a patient chart has calls and e-mails about it and no lead at all. Keying to leads meant each of those either borrowed a lead or grew a parallel timeline. Both tables now carry the document manager's (assoc_type, assoc_object_id) pair, with LEAD, ACCOUNT, PATIENT and COMMERCE_RECORD as owners. LEAD_UID is KEPT and still filled for lead-owned rows, so every existing /leads query, index and call site is untouched — the lead-keyed repository methods and the lead timeline overload both remain, and the new owner-keyed ones sit beside them. Three things this had to get right, and one I nearly got wrong. The unique constraint moves to the owner pair. (ORG_ID, LEAD_UID, CHANNEL, SOURCE_ID) is what makes ingestion idempotent — a RingCentral call is seen twice, once by the webhook and once by the reconciliation sweep. With LEAD_UID null for a patient, Postgres treats every one of those NULLs as distinct and the second sighting inserts a duplicate. The contact-point table matters more than the timeline. Pointing a timeline at a new owner changes what is READ; inbound traffic only ever LANDS on an owner that has contact points, because that is what the linker matches against. A patient whose number is not a contact point receives calls that resolve to nobody. The one I nearly shipped: the linker de-duped matches by cp.getLeadUid(). For a non-lead contact point that is null, so seenLeads.add(null) would let exactly ONE non-lead owner through per inbound message — a call to a number two patients share would reach one of them, silently. It now de-dupes on the owner. assoc_type is a STRING, never an ordinal. Hibernate freezes a CHECK (col BETWEEN 0 AND N) over an ordinal enum column at CREATE TABLE and never revisits it, so appending a value later rejects every insert carrying it — silently, because the failing statement sits inside a @Transactional method whose rollback removes the evidence. Seven of those were found full and already breaking things on this platform in August. A stored null reads as LEAD, because every row written before the column existed is a lead's and returning null for those would blank the timeline this change exists to widen. An UNKNOWN value reads as null rather than LEAD — attaching somebody else's traffic to a lead timeline is the one wrong answer worse than none. Also in this commit: the TEFCA exchange layer (partners, the disclosure ledger, cross-organization patient matching). Its purpose codes are now the same HL7 v3 ActReason strings PhiPurposeOfUse already uses — a test caught me inventing "T" for treatment where the platform says "TREAT", and an exchange row and an audit row describe the same disclosure from two angles, so a report joining them joins on those strings. 1932 tests green.

  30. · kamo-internal

    SMS and AI counts, and a per-surface split

    One MediaService snapshot covers three surfaces that belong on two different nav indicators, so the totals are separated from the server's declared sessionType rather than from which windows happen to be open. An entry with no sessionType counts as chat — that is what an older build sends, and it keeps the badge doing what it did rather than dropping a count mid-deploy.

Ulimwengu wako unasubiri.

Kuanza bure katika dakika. Ongeza bidhaa wakati wowote wewe ni tayari - hakuna uhamiaji, hakuna logins mpya.

Bure milele - hakuna kadi ya mkopo inahitajika