Mabadiliko
Nini kipya.
Kila kuboresha jukwaa, kama ni meli.
· kamo-shared-library
Show what turning off fail-open would cost
kamo.entitlement.fail-open grants any app the plan matrix never mentions. It is documented as a temporary valve for the catalog backfill — turn it off once the matrix is seeded — but nothing could answer the question that decides whether turning it off is safe: which tenants are holding which apps only because of it. Flipping it blind would have taken apps away from live organizations, with the first support ticket as the notification. failOpenGrants resolves each organization twice, once granting unmentioned apps and once denying them, and reports the difference. It does not read the live flag, so it gives the same answer before and after the flip. The re-resolution goes through resolvePlan rather than the plan id directly, mirroring the real path's fallback to a planCode lookup — otherwise a grant that would in fact survive gets reported as a loss.
· kamo-internal
Show whether someone is there, and their extension
Every row now carries a presence dot and word beside the name, and its extension in the detail line — so choosing a person no longer means picking a name and hoping. This is the one people picker in the app, so it lands on the Manager field, e-sign signers, adding people to a chat and assigning support alike. Offline beats whatever was declared. availability_status only moves on sign-in, sign-out and a manual toggle, so somebody who never signs out reads AVAILABLE indefinitely — showing that as a green dot would be confidently wrong about the one thing the dot is for. Not-connected draws as an outline ring rather than a filled dot, so it reads as absence instead of as another status colour. A directory that never carried presence — an in-memory source behind `fetcher` — shows no indicator at all. Absent is unknown, and unknown should say nothing rather than claim everyone is offline.
· SecurityService
Carry availability, presence and extension on a directory row
Picking a person is easier when the row says whether they are there and how to reach them, so each one now carries what they last declared, whether anything of theirs is actually connected, and their work extension when they have one. Availability and presence are both here because either alone misleads: availability_status only moves on sign-in, sign-out and a manual toggle, so a member who never signs out reads AVAILABLE indefinitely. Presence is read from the PRESENCE:{memberId} key MediaService writes — a cross-service read worth naming. The alternatives were an HTTP round trip to MediaService on every keystroke of a search, or reading presence in the browser, and the browser cannot: ToolProviders renders the tool dock as a sibling of the tree holding the presence providers, so a picker opened inside a support ticket window would report every colleague offline. Both services share the Redis host and database. A failed lookup reads as offline rather than failing the search.
· BillingService
Buy a block when an opted-in org runs out of room
Runs shortly after the nightly snapshot sweep, because that is what produces the usage figure it decides on — expanding against yesterday's number would buy a block a day late every time. One block per run, never a catch-up burst, and it stops at the owner's ceiling. If an organization is far past that ceiling the right outcome is that writes stop and someone looks at it, not that the platform quietly buys its way out at their expense. Auto-expand on with no ceiling reads as disabled rather than unlimited, because an absent limit is far more likely to be an oversight than an intention.
· InitializerService
Add the auto-expand columns
· kamo-shared-library
Opt-in auto-expand with a ceiling
Two columns on the subscription that holds the blocks: whether to buy another one automatically when the organization runs out of room, and the most it may ever buy. Off by default on purpose — silently increasing someone's bill is worse than an upload that fails with an explanation, so turning it on is the owner trading a little money for never having to think about it, and that is only their trade to make. A ceiling is required rather than optional: an automatic purchase with no bound is an open-ended commitment to a process nobody is watching.
· BillingService
Let a catalog price change actually reach Stripe
Stripe Prices are immutable and the mapping holds one row per (org, type, code, cycle) behind a unique key, so once a price existed it could never change. A price edit was a permanent no-op on revenue, and nothing reported it. StripePriceReconciler compares every mapped price against what the catalog now says and, when asked to, points the mapping at a fresh Stripe Price with the old one archived — archived rather than deleted, because Stripe keeps billing an archived price for subscriptions already on it and refuses it for new ones, which is exactly the behaviour wanted. Metering is carried across, so a metered price cannot come back unmetered and silently stop billing usage. It deliberately does not move existing subscribers onto the new price. Silently re-pricing a live customer because someone edited a catalog row is a charge nobody authorised; whether to grandfather them is a commercial decision, and the fact that it was never a decision is what was missing. The count still on the old price is reported instead. Exposed report-only by default at GET **************** with the repoint behind an explicit POST.
· InitializerService
Add the columns that make a price change detectable
Order(0), nullable, no backfill — a null amount reads as "we do not know what this price was created with" and the reconciler treats it as unknown rather than as drift.
· kamo-shared-library
Record what each Stripe price was created with
The mapping stored only which price id to use, so a catalog price change could not even be noticed. ensurePlanPrices short-circuited on the row's existence and the unique key forbade a second one, which meant raising a plan from $72 to $79 updated the marketing page, the in-app quote and the plan DTO and changed nothing about what anybody was charged — with nothing reporting the divergence. unit_amount_cents and currency give the reconciler something to compare against. Both nullable and left NULL for existing rows: null means "unknown", which is true of every row predating the column, and treating unknown as mismatched would recreate every price in the estate on the first run.
· kamo-internal
Type the storage capacity the summary now returns
Keeps the client contract in step with BillingService, which now reports what the plan includes, what blocks were bought, the combined ceiling and the fraction used alongside the bytes stored. The panel that renders it needs new copy, and kamo-translation-dictionary is mid-edit by another session, so the UI lands separately rather than sweeping up in-flight work in that repo.
· BillingService
Report capacity alongside usage
The storage panel reported bytes stored against nothing at all — no allowance, no percentage, no money — so an owner looking at 400 GB had no way to tell whether that was comfortable or about to stop their uploads. Their first knowledge of a problem would have been a failed upload. The summary now carries what the plan includes, what blocks were bought, the combined ceiling and the fraction used, plus the block size and price so a surface can say what another block would give and cost without hardcoding it. All of it comes from the same catalog rows the charge is priced from.
· kamo-shared-library
Give the storage allowance an effect
Storage was measured across nineteen domains, nightly, with a build-failing coverage test — and nothing anywhere computed an allowance or refused a byte. The plan's storage figure was data nobody read, and the Additional Storage add-on had no product effect at all: paying changed nothing and not paying cost nothing. StorageQuotaService resolves what an organization may hold — the plan's per-user allowance pooled across its seats, plus the blocks it has bought — and ImageService refuses content that would exceed it. The check sits after the dedup lookup, so re-attaching a file the platform already holds is never refused for space; only genuinely new bytes count. It lives here rather than in BillingService because the services that write bytes cannot each call a billing API on their upload path, and it is injected by field rather than through the constructor because ImageService is assembled by hand in several places — a constructor argument is the one edit that compiles everywhere except the tests and then fails the image build. Two deliberate ways of being wrong. "GB" is read as a gibibyte, which is what a file manager means when it prints GB and the more generous reading. And an organization with no live subscription, or a service with no quota bean wired, stores exactly as before: this gate exists to hold a paying customer to what they bought, not to turn a billing gap into data loss. Reads are never gated — filling your storage stops you adding more, never takes away what you have.
· kamo-internal
Tell the requestor whether their agent is actually there
A silent thread does not distinguish "nobody has looked at this" from "my agent stepped away", and the person waiting cannot tell which. The notice under the conversation says which, and when the answer is "not now" it says what happens next — the message is received and will be answered when they are back — rather than leaving someone to wonder whether it landed at all. Placed under the chat rather than in the header because it answers "is anyone there?", which is the question being asked while typing, not while scanning the ticket's metadata. Server-answered, not read from the requestor's presence map: presence is broadcast per organization, so a customer-org member being helped by the provider org would see their agent as permanently offline. A confidently wrong "they're away" is worse than no notice. For the same reason a failed poll keeps the last known answer instead of flipping the notice. Polled at 30s and only while the tab is visible. Availability is a comfort signal, not a correctness one, and a backgrounded tab is nobody watching.
· MediaService
Reassignment reaches the new agent, and the requestor is told who is there
Reassignment did not reach anybody. assignmentOpenedAt records that a client put the conversation on screen, and it is what raises a ticket at sign-in — but it still held the PREVIOUS agent's stamp, so the new one got the live push only if they happened to be online that second, and nothing whatsoever if they were signed out. applyAssignment now clears it whenever the assignee actually changes, which makes the handover durable for whoever owns it now. GET **************** tells the person who asked for help whether the person helping them is reachable. Answered server-side on purpose: presence is broadcast per organization, so a customer-org requestor cannot see an agent in the provider org at all and the status would read "offline" for every single cross-org conversation. Scoped to the people already in the conversation — a ticket id is a UUID, not a secret, and "is this person at their desk" should not answer to a guess.
· kamo-internal
Assign a chat request, and assign from the tickets list
Two gaps, same cause — assignment was treated as something that happens to a ticket rather than to a piece of work. The assign button was hidden for pre-tickets outright, so a live chat request, the case where getting it to the right person matters most, had no way to be handed over at all. That exclusion dates from when a chat request was not yet "a real ticket" and assignment only happened on conversion. It is assignable now, and the status survives, so it can still be converted afterwards. And assigning required opening the conversation first, which is the wrong shape for triaging a queue — the person working the queue is often not the person who should answer. The Assigned To cell on the tickets list is now the control: hovering shows assign or reassign, clicking opens the same picker the ticket window uses. The row click still opens the conversation. The response patches its own row rather than reloading, so assigning does not move the operator's place in a filtered, sorted, paged queue.
· kamo-internal
Quote engine for the plan picker, matching what Stripe will bill
The arithmetic behind a live running total, kept apart from any component so the numbers can be tested directly. It mirrors **************** exactly: per-seat add-ons scale with headcount, flat ones bill by units bought however many people are on the plan, and metered ones are named rather than guessed at. That correspondence is the point. Both places the platform previously quoted a price disagreed with the charge — the in-app order wizard added each add-on once regardless of seats, and the public page billed an organization-wide storage pool per user.
· BillingService
Make a first plan purchasable, and return real plan prices
Two things stopped an organization ever buying a first subscription. Plan lists were reachable only through sub.marketId on a subscription the org already held, so an org with none could not see a plan anywhere in the product. GET /api/billing/markets/for-org resolves the market from the caller's entitlement root — the same org whose Free plan already supplies their baseline, so plans and entitlements cannot come from different catalogs. And the plan DTO read **************** which nothing ever populates: they are NULL for every KamoCRM plan, because the seeder writes prices into **************** A buying surface therefore showed no price at all, and the account recorded a null charge. It now reads the price rows, with the plan columns as a fallback for catalogs that carry them. Also exposes the add-on unit size so a buying surface can say "per 10 GB" without hardcoding it.
· kamo-internal
Gate paid actions on a licensed seat, at the chokepoint
Nearly every gateable action in the product — dial an extension, email a member, open the AI assistant, play a recording, start a social conversation — is a bare **************** and the tool registry had no gate at all. Gating those call sites one at a time could never be complete: a dispatcher added later, or simply missed, routes straight around it. The check now lives in ToolWindowsContext's single tool:open handler, so every caller is covered. Actions that never pass through a tool window are gated where they live: attachments (including drag-drop and paste, which are the same capability by another route), growing a chat into a group, and the meeting entry points. That also settles three inconsistencies the audit found. Two of the three "invite to meeting" entry points checked organization.isMeet and the third did not; two of the three email entry points checked ACCESS_MAIL and the third did not; and dialling a member's extension from the roster had no VOIP check of any kind. All of them now agree. Capabilities default to locked — while the first load is in flight, if the call fails, and if an older backend omits the field. Guessing "allowed" would hand out paid features, which is the mistake the entitlement path already made once with its fail-open default.
· SecurityService
Resolve what a member may do, not just what their plan includes
Feature grants describe a plan. They never described this member, because the console gates navigation on org-level org.features — so a member with no license at all saw every app the organization was entitled to, and the member-level licensing that already existed in the database had no effect on anything. MemberCapabilities is that missing answer, resolved once on the server. A member without a seat keeps a deliberate floor: text conversation with teammates, support, their own profile, their own billing — enough to reach a human and to buy a seat back. Incoming-message translation stays on too, because reading what you were sent is not spending. Everything that costs money to run is off. An active org trial licenses everyone in the org rather than only the seat holder. A trial org is seeded with the Free plan's single seat, so gating the trial on licenses would have meant the owner alone could evaluate the product while the team they invited to evaluate it saw nothing. Login is untouched: authentication has never consulted billing state and still does not. Losing a seat costs features, never access.
· BillingService
Bill per-unit add-ons by units bought, not by headcount
Threads the new add-on quantity through create and update: a FLAT add-on now takes its Stripe quantity from what the subscription actually bought, so four 10 GB storage blocks bill as four blocks rather than once per seat. Quantities are filtered against the resolved add-on codes on the way in, so a stale or hostile body cannot park a quantity against a code it was not entitled to buy.
· InitializerService
Reprice Additional Storage as $2.50 per 10 GB, org-wide
The add-on described an organization-wide shared pool and was then priced USAGE_BASED at $4 with "25 GB" stated only in prose. Nothing ever submitted a storage meter event, so it billed nothing; and had it been attached, Stripe rejects a quantity on a metered price, so the subscription create would have thrown. Meanwhile the public page rendered it as "+$4 /user/month". It is now FLAT with unit_size 10 at $2.50 — blocks the owner buys, priced the same however many people are on the plan. AddOnQuantityMigration adds the two nullable columns at Order(0), before any runner can load an entity that maps them.
· kamo-shared-library
Give add-ons a unit size and a purchasable quantity
An add-on's unit only ever existed as prose inside its description ("Each unit adds 25 GB") and as a hardcoded constant in the marketing site, so nothing could bill against it. SubscriptionAddOn gains unit_size, and AccountSubscription gains addon_quantities — how many units of each add-on the subscription bought. active_addon_codes stays the entitlement source of truth, so nothing that only asks "is this app granted" has to know the quantity map exists. Also fixes the order wizard's quote, which added each add-on once with no seats factor even though most are PER_SEAT: a 25-seat order with four modules was quoted $31 against the $775 Stripe would bill. The quote now follows the same pricing model the charge does, and omits metered add-ons rather than guessing.
· kamo-internal
Pick the agent to reassign a ticket to, instead of typing their id
Assign and reassign opened a text box you typed a raw member id into — which meant already knowing the id of the person you wanted, for an action whose whole purpose is finding the right one. It now uses MemberPickerDialog, the same picker behind the Manager field on a member's profile and the one people picker in this app. Its directory search runs server-side and matches on name, alias, work and account email, job title and department, so "ana lending" finds the Ana in Lending; each row carries enough to tell two people of the same name apart. The current assignee is pinned above the results so it is not lost mid-search. Member ids exceed JavaScript's exact integer range, so the id stays a string from the directory to the wire.
· MediaService
Endpoints for the sign-in auto-open handover
GET /sessions/chat/auto-open returns the chat conversations this member's client should raise; POST /sessions/{guid}/auto-opened records that it did. The durable counterpart of the support handover, and for the same reason: the decision belongs to the platform, not to one browser tab's sessionStorage. The write is scoped to the caller's own membership row, so one member's client can never retire another's, and stamps server time rather than a client-supplied value — the comparison is against message timestamps written by this same clock, and a skewed browser would otherwise either suppress a conversation forever or re-raise it endlessly. ChatAutoOpenHqlTest parses the query against the real entity model and pins the three predicates it exists for. Spring Data only validates @Query at context boot, so a typo would otherwise surface as a failed startup or a 500 on a path every member hits at sign-in; the test reads the query off the annotation by reflection so it cannot pass against a stale copy.
· kamo-shared-library
Durable record of a conversation the client raised by itself
MediaSessionMember gains AUTO_OPENED_AT, beside msgIdLastViewed because it is the same grain — a fact about one member's relationship to one session. That one answers "how much have they read"; this one answers "have they already been shown this, unasked". It replaces a per-tab sessionStorage flag, which was not a record of anything: a second device re-opened every conversation the first had already popped, and clearing storage lost the fact entirely. findChatSessionsToAutoOpen returns the sessions holding something the member has not read where something has arrived since the last raise — so a conversation deliberately left unread stops re-opening on every load but comes back when a genuinely new message lands. Chat sessions only; support tickets have their own handover. The member's own messages are excluded, and a dangling msgIdLastViewed pointer counts as unread rather than as caught-up, because a scalar subquery over a vanished row yields NULL and would read as permanently read.
· kamo-internal
Overhaul the preferences panel
Four dropdowns and a switch on a panel washed top to bottom in the tenant colour at full strength, so every control fought its own background. Each select hid three or four options behind a click and repeated the same twelve lines of border styling. The panel is a surface now, and the colour belongs on whatever is selected. - Every option is on screen. These are four settings with three to five short values each; showing them is faster to use than opening a menu, and one Choice component replaces four blocks of duplicated styling. - Reminder time and method share a card, because neither means anything without the other. Reminder offsets are mono, matching the rest of the calendar's treatment of time. - The default calendar shows its colour, so it is recognised the same way it is in the sidebar and the event dialog rather than by name alone. - The header stays put while the body scrolls — with four sections the title otherwise scrolls away. These settings save on change with no button to press, and previously said so with a bare spinner. There is now an explicit "Saved", which fades after a couple of seconds because it is a receipt for the change just made, not a permanent state of the panel. Sections stagger in and the confirmation springs; both are suppressed under prefers-reduced-motion. Every string is an existing key. Verified with a production build in a detached worktree.
· kamo-internal
Replace the mailbox spinners with a skeleton that assembles
Every loading state on /messages was a Loader2 on an otherwise empty pane — the boot, the message list, the folder tree, the message body and the next page all span in place and told the reader nothing about what was coming. They now draw the thing that is loading. Each placeholder copies the geometry of the component it stands in for, down to the 40px avatar, the folder indent and the 104/120px message row measured off the real markup, so the layout does not move when the data lands. One idea carries the motion: a band of light crosses each placeholder left to right and starts a beat later on every row below it, so the highlight descends the stack as a single diagonal pass rather than every row flashing on its own clock. Rows rise into place in the same order. The light is mixed from --accent-comms, the messages module's own family accent, so it reads as this product rather than a generic gloss. prefers-reduced-motion flattens the whole thing to still bars. No new message keys: the placeholders announce themselves to screen readers through the three loading strings that were already there.
· kamo-internal
Add and remove guests from the event dialog
The last thing the dialog could not do. It could display who was already on an event, but there was no way to put anyone there — the only guest control was "update and notify", which re-mails people who were already invited. Guests are added by email, listed with their RSVP as a coloured dot titled with the state, and removed on hover. Enter adds a guest rather than submitting the dialog, because you are mid-list rather than finished. The organiser is marked and cannot be mistaken for an ordinary guest. Email validation is deliberately permissive: the real test is whether mail to it bounces, and a stricter pattern rejects addresses that are perfectly valid. It catches an obvious typo, nothing more. The empty state invites rather than apologises. Strings shipped ahead of this in kamo-translation-dictionary — next-intl throws on a key missing from en.json and kamo-internal's CI clones that repo at build time, so landing this first would have shipped a calendar page that crashes. Verified with a production build in a detached worktree; another session holds the in-tree lock.
· EmailService
Manage an event's guest list
The dialog could show who was already on an event but never add or remove anyone; the only guest operation was re-notifying people who were somehow already there. Events now accept a guest list, replaced wholesale on save. Replace rather than merge, because the dialog owns the list — a guest removed there has to disappear here. A null list means "not supplied" and leaves the guests alone, which is what a client editing only a title sends. Two things a client does not get to assert, so neither is read off the request: the RSVP, which is the guest's to give, and the organiser flag, which records who called the meeting. Both survive an edit — recreating rows instead of updating them would reset every reply and strip the one person allowed to cancel the event. Guests come back batched. A month view returns a hundred events, and asking per event is a query per row. Deleting an event now removes its guests first. They carry a non-null foreign key back to it, so the delete either orphaned them or failed once the database enforced the reference.
· kamo-shared-library
Batched guest lookup, and delete-by-event
The event dialog shows who an event is with, and the list endpoint returns a month of events at a time. Asking for guests per event is a query per row; findByEventIdIn asks once. deleteByEventId is the first half of replacing a guest list wholesale, and of removing an event without orphaning rows that carry a non-null foreign key back to it.

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