உள்ளடக்கத்திற்குச் செல்லவும்
KAMOUniverse

மாற்று பதிவேடு

என்ன புதிது.

அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.

  1. · kamo-shared-library

    MANAGE_ENTERPRISE_SUBSCRIPTIONS

    Enterprise is negotiated, never self-serve. This right gates both halves of that: seeing what it costs, and putting an organization on it. Category.OPERATIONS rather than CONSOLE - it gates an action that reaches into a tenant's billing, not one screen of the platform console. On the platform spine rather than the tenant one for the usual reason: a tenant administrator can edit their own organization's security models, so a RoleRightType here would let a customer grant themselves the negotiated tier. Persisted as EnumType.STRING, so appending is safe.

  2. · SecurityService

    Check the chart before writing a prescription

    Prescribing was added with the allergy table, the interaction-checking query and a "safety context" endpoint all sitting beside it, unread. A system that records an allergy and then writes the prescription anyway without mentioning it is worse than one that never recorded the allergy, because the practice believes it is being checked. Three checks the practice's own data can answer exactly: the drug matches a recorded allergy, the patient is already on it, or nobody has ever asked about allergies at all. That last one is its own warning — an empty list means "no known allergies" or "nobody asked", and only one of them is safe to prescribe against. A positively recorded "no known allergies" row is not treated as a substance. Warnings with a required acknowledgement, not refusals. A clinician may deliberately prescribe a drug a patient reacted mildly to years ago, and a system that forbids it is one people route around. What must not happen is prescribing in IGNORANCE, so the write is refused until the prescriber has seen the warning and says in writing why they are proceeding. That reason is logged at WARN with the member, the drug and the patient, because a later review is what it is for. 409 rather than 400: nothing is malformed. It is a valid prescription that conflicts with what is recorded, and the identical request succeeds once a reason is attached. The warnings travel in the body — a refusal that only said "unsafe" would send somebody hunting through the chart for what a computer already knew. Matching drops strengths and filler words. "Amoxicillin 500mg" and "Amoxicillin" are the same drug so a differing strength cannot hide an allergy; "tablet", "oral" and "solution" appear in half of all drug names and matching on them would flag every prescription against every allergy, which is the fastest way to make people stop reading warnings. DELIBERATELY NOT drug-drug interaction checking. That needs a licensed database (First Databank, Medi-Span) this platform does not have, and a home-grown approximation is the most dangerous thing the file could contain: it would look like interaction checking, be trusted as it, and miss most interactions. The class says so in as many words. 15 tests.

  3. · KlusterServices

    Phase 0 infrastructure gate

    - TURN over TLS on 5349 with a real cert (turn-kamocrm-com-cert). Without turns:, a customer behind a 443-only egress firewall has no media path. Verified externally: listener up, chain validates. - Authoritative host port allocation table; k3m1 reserved for KamoPBX. - Router NAT rules documented in-repo, incl. the missing pbx.kamocrm.com A record (needs 42.181.8.87). - Corrected coturn comment claiming k3m1 is 47.181.8.87; it is 42.181.8.87 and .87 on 47.x belongs to an unrelated third party. - Corrected relay range to the real 52000-65535 (README said 49152). - verify-phase0.sh probes from outside. UDP rows report INCONCLUSIVE rather than PASS: nc -z -u calls almost any UDP port open, which would be a false green on the SIP and RTP paths.

  4. · kamo-shared-library

    Carry a plan feature's unit to the buying surfaces

    The catalog has stored a unit per feature all along - "GB/user", "users", "contacts" - and the plan-feature DTO dropped it, so every surface rendering a measured feature printed a bare number. The pricing page showed "Storage (15)" and a comparison column of 5 / 15 / 25 / 50, and a reader had to reach the Terms far below the fold to learn it meant gigabytes per user, pooled. Deriving it per-consumer from featureCode is what put a stale block size on the marketing site once already, so it travels with the value instead.

  5. · KlusterServices

    Swap the inbound greeting to KamoCRM-Main-Intro-2

    Uploaded as a new file rather than overwriting the old one, so rolling back is a one-line change to KAMO_ATTENDANT_INTRO plus a dialplan reload — no re-upload needed. 48 kHz stereo source converted to the 8 kHz mono Asterisk requires; 59.9s, down from 70s. Verified live: CDR shows BackGround custom/kamocrm-main-intro-2, and Asterisk decoded the file to 478954 bytes of u-law, exactly 59.87s at 8000 B/s.

  6. · kamo-internal

    Author a flat-rate task book, priced by the minute

    Replaces the SW5 placeholder pane with the real authoring surface: three sub-views on one tab — books, categories, entries — following the SubView switcher MarketTaxShippingTab and MarketDiscountsTab already use for tax classes and price lists. Categories and entries stay disabled until a book is chosen, and the chosen book's name and version stay on screen while they are open. labourMinutes is deliberately the loudest field on the screen. It is the one number that does two jobs — it prices the line and it sizes the visit — and it is the whole reason a task book beats a price list. So it gets its own column ahead of price, its own framed block at the top of the entry editor with a live "1h 30m" readout, and a warning above the table counting entries that are missing it. A book of priced entries with no durations is a price list with extra steps, and nothing downstream can tell you that has happened. Also lands the two pieces the surface could not work without: - **************** No proxy covered this path: commerce/quotes has a catch-all, but it is scoped to /commerce/quotes, so every call would have 404'd in the browser. The new route exports PUT as well as GET/POST/DELETE — the quotes route it is modelled on has no PUT because quotes update with PATCH, and every task-book update is a @PutMapping, so copying it verbatim would have made every read work and every save answer 405. - app/lib/serviceTaskBookApi.ts, mirroring ServiceTaskBookApi.java. Every id is a string in both directions: defaultParts carries commerce_offerings.uid values, 19-digit unique_rowid() Longs that lose their low digits to a JS double. BigDecimal stays a number — the wire mapper gives Long a JS-safe serializer and leaves BigDecimal alone. Two API behaviours shape the form and are recorded next to the code that works around them. Listing with marketUid also filters isActive = TRUE, so an authoring surface using it could never show a book you had just retired; this lists unfiltered and narrows by market itself. And a null field means "leave it alone" on update, so blank numeric inputs are omitted rather than sent as null. Pinned by a source-level contract test: the client's paths and verbs against ServiceTaskBookController's mappings, the proxy's exported verbs against the ones the client uses, and the id types against string. The cross-repo half skips without a SecurityService checkout; the rest always runs.

  7. · SecurityService

    Right-gated task book API

    SW5 Task 3, controller half. CRUD for books, categories, tasks and options, plus GET /{bookUid}/tasks/by-code/{code} - the lookup a quote or work order performs when it holds a stored taskCode. That one is a single indexed query against idx_service_task_book_code, not a fetch-the-book-and-scan; it runs per line item over a catalogue of hundreds, which is why it is an endpoint. Reads need VIEW_SERVICE_WORK, writes need MANAGE_SERVICE_WORK. Both landed in SW1 and both already gate the nine service endpoints on POSController. The controller this one deliberately does not resemble is CommerceMarketController, which carries no @PreAuthorize anywhere and asks only whether a session exists - the same hole SW4 found on orders and SW1 found on service work. ServiceTaskBookRightsGateTest pins every handler, and separately pins that no mutation settles for the view right (MANAGE is a CHILD of VIEW in the rights tree, so accepting VIEW on a write is a silent downgrade) and that no handler accepts the customer's VIEW_OWN_SERVICE_WORK as an alternative. The tenant comes from the session and is passed as the service's first argument, where it becomes a predicate inside the JPQL. Nothing here loads a row and then checks whose it was. Path variables arrive as String and go through parseUuid, so a malformed id is a 400 rather than a Spring 500. 'Not found' means not found IN YOUR ORG, which is the only question the repository asked and the only answer a caller should get.

  8. · kamo-shared-library

    Task book service with org-scoped CRUD and code resolution

    SW5 Task 3, library half. ServiceTaskBookService exposes books, categories, tasks and options behind an org-scoped API, plus resolveTaskByCode - the lookup a quote or work order performs when it stores a taskCode. That one is a single indexed query against idx_service_task_book_code, reached through ServiceTask's direct book FK, not a fetch-the-book-then-scan. Every entry point takes orgId as its first argument and hands it to a repository whose JPQL carries the predicate. Nothing loads by uid and checks the org afterwards; that shape is what made six commerce endpoints cross-tenant reads before SW1 repaired them. ServiceTaskBookServiceShapeTest pins both halves - the signature rule and 'every servicework @Query names organization.id'. Two repositories were needed that Task 1 did not create: categories and options carry no org of their own, so their org-scoped reads join through the book (two hops for an option). Ids and timestamps leave as strings in both directions. Offering.uid is a 19-digit Long and a JSON number in a browser is a double, which is the same trap that forced QuoteConversionResult.orderUid to become a String in SW4. Reads map to views inside the transaction rather than handing entities to a controller, so nothing depends on open-in-view staying true. Deletes refuse rather than cascade: a book that still holds priced entries and a category that still holds tasks both answer 409. Retiring a book is isActive/effectiveUntil, which leaves every sold line resolvable.

  9. · kamo-internal

    Un-gate pricing, tax and tiers for service markets

    A SERVICE market got exactly three tabs — Overview, Vendors, Agreements. Customers, Tax & Shipping and Discounts & Pricing were all gated on commerceType === 'RETAIL', which hid three surfaces a service org needs: tax classes (a sub-view of Tax & Shipping), price lists (a sub-view of Discounts & Pricing), and account tiers (Customers). The three move behind a named predicate, SUPPORTS_PRICING_SURFACES, so the reason lives next to the rule and adding a vertical is one edit. Customers has to travel with Discounts: PriceList.customerTier points at AccountTier, so un-gating price lists without the tier surface leaves that field unfillable. Products and Inventory stay RETAIL-only. A task book is not a product catalogue, and showing a service org an empty product grid would imply it should fill one. Adds the SERVICE-only task-book tab entry (lowercase-hyphen key, though market tabs are policed by no coverage test). MarketTaskBookTab is a placeholder that renders a notice; the authoring surface lands next and replaces it wholesale.

  10. · kamo-internal

    Apply a remittance to a claim

    "Unapplied money" is money the payer has sent that nobody attached to a claim, and the queue could only be looked at. POST_INSURANCE_PAYMENTS gated an operation with no way to reach it. Line by line, because that is how a payer adjudicates. A lump sum against a claim cannot answer which procedure was denied and why — the question the denial-patterns queue exists to answer and the one an appeal has to argue. The claim is found by the number the payer quotes, which is the only identifier a remittance carries. Typing it and watching the lines appear is the difference between posting against the claim the payer paid and one that looked similar. Leaving a line blank means the payer has not settled it, and the dialog says so: the claim stays open until every line carries a figure. Without that sentence somebody types zeros to be tidy and closes a claim still being worked. Amounts stay strings the whole way through. Parsing them to "validate" would put binary floating point back into money, and 0.1 + 0.2 on a claim line is a balance a patient is asked to pay and cannot be told the reason for.

  11. · SecurityService

    Find a claim by the number a payer quotes

    Posting a remittance needs the claim it settles, and a remittance carries no internal id — only the number the practice sent. Without this lookup, applying money means reading the number off the remittance and hunting for the claim by eye, which is how a payment reaches the wrong one. Returns the claim with its lines, because payers adjudicate per line and posting is per line. 404 when there is no such number: "we have no claim with that number" is the answer somebody posting needs, and an empty success object reads as a claim with nothing on it.

  12. · kamo-internal

    Four things that could be looked at and not done

    Each of these had a working endpoint, a client function, and no way for a person to reach it. Closing an order loop. The open-loops queue exists because the ordered test nobody chased is the classic way a diagnosis is missed — and it could only be READ. A queue nobody can clear is one people stop opening. The reason is asked for because "result reviewed with the patient" and "cancelled, they had it done elsewhere" close the same row and only the first means somebody saw the result. Stopping a medication. Offered on the prescribed list only: a reported medication is somebody else's prescription, which this practice can record and cannot stop. The reason matters — a finished course, an intolerance and an interaction all end the prescription, and only the last two should stop it being prescribed again. Withdrawing portal access, now that an account id can be obtained. The panel loads what access the patient already has on mount rather than behind a button, because that is the first question anybody asks on it — and inviting somebody who is already signed in supersedes their working link for nothing. Collecting a finished export. It could be started, watched and cancelled and never downloaded, which is the whole point of running one. The manifest is fetched first and the files come from it: a bulk export is one NDJSON per resource type, and guessing ids would produce a records release that is quietly incomplete. Also removed fetchWaitingRoom, which nothing called. PracticeDayPanel derives the same set from the day it has already loaded — one round trip instead of two, and it cannot disagree with the schedule beside it. A comment says so, because an unused wrapper reads as a missing screen to the next person auditing this.

  13. · kamo-shared-library

    Add the flat-rate task book

    A priced task catalogue whose entries carry labourMinutes — the number that both prices a line and sizes a visit. ServiceLineItem.taskCode, landed in SW3, joins to ServiceTask.code BY VALUE and not by FK: the book is versioned, so a sold line must keep meaning what it meant when it was sold even after the entry is repriced or retired. ServiceTaskCategory mirrors ProductCategory rather than reusing it — ProductCategory is joined to Offering through ProductCategoryMapping, so sharing it would put "Drain clearing" and "Kitchen taps" in one tree. The effective window is lifted from hr/timecard/MemberPayRate. ServiceTask holds its book directly rather than reaching it through the optional category: t.category.book is an implicit join, which Hibernate renders as an inner join, so every uncategorised task would silently vanish from "the tasks in this book". columnDefinition is pinned on every FK and every element-collection join column. Verified by exporting the DDL from the mapping: no join column carries a DEFAULT, so an unset FK stays null instead of inventing a random parent id. Every repository read is org-scoped inside the query, never checked afterwards.

  14. · InitializerService

    Per-extension PBX billing schema, backfill and catalog

    PbxBillingSchemaMigration is @Order(0) because it adds a column. The shared-library entity maps phone_cost_mode from the moment the build starts, so any runner loading an OrgBillingPolicy through JPA emits it in its SELECT - ordered late this runner never executes at all, because an earlier one dies on "column ... does not exist" and takes the whole run down. org_extension_billing_days is in public, not a schema of its own: every other voip table is. It is unique on (org_id, day, addon_code, subscription_uid), one column wider than the mail equivalent, so an org with two billing groups keeps two audit rows rather than one overwriting the other. subscription_uid is NOT NULL precisely so SQL's nulls-are-distinct rule cannot defeat that index. **************** repairs the assignments InstanceSyncService had been erasing on every sweep - nine members held one and all twenty-three extension rows read null. Only fills a blank: the config side joins on an unconstrained extension string, so it is the weaker record and never overwrites an id that is set. PbxAddonCatalogMigration seeds both add-ons into every catalog that already sells the mail ones, anchored to the EMAIL_HOSTING row for org, market and compatible plans. Scoped that way rather than by a hardcoded id because two tenants own add-ons with the same codes. KamoCRMPricingLoader gets them too, for catalogs seeded from here on - it is create-only, so editing it alone fixes nothing already in the database.

  15. · BillingService

    Bill phone service by the extension

    PricingModel.PER_EXTENSION, its own reserved quantity key, an allocator, a nightly reconciler, a removal gate and the summary API - the phone twin of the per-mailbox work, differing everywhere the data does. Multi-instance is the difference that runs through all of it. An org can run a KamoPBX server and a RingCentral server at once, so both add-ons can bill in the same month: the allocator returns a count per subscription PER CODE, and every subscription carries both codes with zero where nothing was attributed. That makes driving an unused code to zero the ordinary path rather than the special case the mail reconciler needs. PER_EXTENSION is its own PricingModel, not a reuse of PER_MAILBOX, and parks its count under __extensions: rather than __mailboxes:. The two reconcilers run independently and neither may disturb the other's quantity. Licensed, never metered - a metered Stripe price rejects a quantity outright and fails the whole subscription call. PhoneCostMode is a sibling of MailCostMode for the same reason: reusing mail_cost_mode would make the column name false and would silently move the phone money when an owner changed who pays for mail. Attribution follows the assigned member directly - extensions need no primary concept, because MEMBER_VOIP_CONFIG.MEMBER_ID is unique and MemberVoipConfigController returns 409 on a second claimant, so the link is already exclusive on both sides. org_extension_billing_days is keyed on the subscription as well as the day and code. The mail table is not, so under GROUP_PAYS_OWN its second billing group overwrites the first and the day's audit row is wrong for every group but the last. Recorded as a follow-up rather than copied. The shared-lib voip.billing package is named in @EnableJpaRepositories - that list is explicit, and the failure mode is the service refusing to start.

  16. · kamo-marketing

    Give the phone add-ons a phone icon

    PER_EXTENSION already renders as "extension/month" - addOnUnitLabel handles it and needs no change. The icon map does: it is keyed on the lowercased catalog code, and an unmatched key is not an error, it silently falls back to a lightning bolt. Only the two new PBX codes are added here. Three older entries in that map (kamo_email_hosting, video_conferencing, additional_storage) do not match their seeded codes either and are left alone rather than guessed at.

  17. · kamo-internal

    Show what extensions cost, where extensions are managed

    ExtensionUsagePanel on the stats tab, beside the mailbox and storage panels: count by class, the catalog rate, the monthly total, and the rule in plain language. Both class rules can be shown at once, because an org can run a KamoPBX server and a RingCentral server in the same month - the case mail could never produce. The free numbers are given the same weight as the billable one. Devices are free under both classes, and unassigned extensions - queues, IVRs, ring groups - are free under the third-party rule and are usually the majority. Both are measured by the aggregator rather than asserted in the copy. Gating on the add-a-phone-server picker, with two rules carried over from the e-mail work because both were learned the hard way. It fails OPEN, so a billing outage cannot strand an org mid-migration. And it never locks a provider type the org already runs a live instance of - all four orgs with a phone server today hold no add-on code at all, and telling them the server they are already using requires an add-on would be false. A locked card is shown with the reason on it rather than merely greyed out. Rates come from the catalog, never hardcoded: a rate written into the UI becomes a lie the first time the catalog changes, and nothing fails when it does.

  18. · KlusterServices

    Queue timeout and "press 0" both fall through to voicemail

    Replaces the hand-rolled Dial()+Goto queue loop with app_queue. The deciding factor is DTMF: digits are NOT matched against the dialplan while Dial() is running, so "press 0 while on hold" cannot be built on Dial() at all. The queue's `context = kamo-queue-exit` setting is what makes it possible. extension -> forwarded to that extension 1 or 2 -> queue 0 -> straight to voicemail, from the greeting or from hold 10 min -> voicemail bad exten -> "invalid" prompt, then queue Main-line voicemail is mailbox 8100, deliberately not one of the staff mailboxes so main-line messages do not land in someone's personal voicemail. The greeting is played with Playback() from sounds/ and VoiceMail() is then called with `s`. Two findings drove that: - A copy installed at **************** was silently REMOVED between a voicemail reload and the next call, and VoiceMail fell back to the built-in "the person at extension ... is unavailable". The spool is managed by app_voicemail/FreePBX; sounds/ is not. - The `s` option skips the greeting as well as the instructions, despite the docs describing it as instructions only. With `su` the recording never played at all. Playback + `s` gives recording -> beep -> record exactly. Verified live, each path separately: - queue timeout: queue_log EXITWITHTIMEOUT, then CDR dcontext=kamo-voicemail lastapp=Playback custom/kamocrm-highcallv..., userfield QS:TIMEOUT - press 0: queue_log EXITWITHKEY|0 with RINGCANCELED on the ringing member, then the same voicemail leg - record + store: msg0000.gsm written to mailbox 8100 - answer: queue_log CONNECT PJSIP/8080 -> COMPLETEAGENT Two testing notes worth keeping. `channel originate` caps calls at ~30s, which truncates any longer test and looks like a dialplan fault. And silent test recordings are discarded by maxsilence=10, so an empty INBOX does not mean recording is broken — proven by temporarily setting maxsilence=0 and restoring. Adds **************** queue outcomes are otherwise invisible outside queue_log.

  19. · kamo-shared-library

    Count the extensions a third-party server does not charge for

    Queues, IVRs, ring groups and spares are the extensions nobody is assigned to, and under the third-party rule they are free - twelve of one production org's seventeen usable extensions. classify() dropped them silently, so the number existed nowhere and the stats panel could only assert that they were free rather than show it. Counted with the same dedup key the billable path uses, so the free number cannot double-count a row the charge would not.

  20. · kamo-shared-library

    Count billable phone extensions

    Adds the phone twin of the per-mailbox aggregator: PbxBillingClass with an exhaustive VoipProviderType switch and no default arm, OrgPbxBillingAggregator (no Spring stereotype - BillingService declares the bean), and the daily audit snapshot. One structural difference runs through everything downstream. org_email_providers is unique on org_id, so exactly one mail add-on could ever bill non-zero. VOIP_PROVIDER_INSTANCE is many rows per org, so an org can run KamoPBX and RingCentral at once and both add-ons bill in the same month. The breakdown is therefore one flat list carrying a class per extension, and countByClass() drives the unused code to zero through the ordinary path rather than a special case. VoipProviderType is EnumType.ORDINAL, so nothing is appended to it; the coverage test pins the order as well as the classification. FREE_PBX classifies as third party - the picker offers it as an on-premise PBX, which is the customer's own Asterisk. KAMO_PBX is therefore the only hosted type and, since no KAMO_PBX instance exists and KamoPBXProvider still throws from every method, PBX_HOSTING correctly bills nothing at all today. Both assignment records are read - **************** and MEMBER_VOIP_CONFIG - because the member phone-settings screen writes both, so their union is exactly what an administrator set. A number that decides money should not rest on one column a background sweep can zero, which is precisely what InstanceSyncService was doing to it. ExtensionBillingDay is keyed on the subscription as well as the day and code, so an org with two billing groups keeps two audit rows instead of one overwriting the other.

  21. · KlusterServices

    Hold-music queue ringing 8000 and 8080 simultaneously

    Eight hold tracks uploaded as the `kamocrm-hold` MOH class, shuffled. The random ordering comes from `random=1` on the row in FreePBX's `music` table, which is what generates `sort=random` into musiconhold_additional.conf — set it in the DB, not in the conf, or the next FreePBX reload overwrites it. Routing after the greeting: extension -> forwarded to that extension (unchanged) 1 or 2 -> queue timeout -> queue bad exten -> "invalid" prompt, then queue The queue is **************** in a loop, so both ring at once, first to answer wins, and the caller hears hold music instead of ringback throughout. Members are dialled as raw PJSIP endpoints rather than through from-did-direct ON PURPOSE: going through FreePBX's extension dialplan would apply each extension's ring timer and then hand the caller to that person's voicemail, which is the opposite of ringing indefinitely. The loop has no cap, as requested. The guard that matters is the ExecIf: only NOANSWER means we really rang for the full timeout. CHANUNAVAIL (nobody registered), CONGESTION and BUSY all return in milliseconds, so without a MusicOnHold pause the Goto becomes a tight spin. Measured with both members deregistered: 1 second of CPU and 24 log lines across 45 seconds in queue. Also raises WaitExten's grace window to play hold music, so the handover from greeting to queue has no silent gap. Verified live: PJSIP/8080 observed in Ringing state under **************** with the caller channel in Ring. Audio is not in the ConfigMap — greeting 1.1 MB plus 7.7 MB of tracks is well past the etcd object limit. Both sets live on the asterisk-lib hostPath and the conversion command is documented beside the globals that name them.

  22. · kamo-internal

    An emoji on a single message, and a message you can correct

    A member asked for both on the individual chat. They land on the same per-message menu the window already has, beside Copy and Unsend. Reactions render as a chip bar under the bubble — the emoji, how many people used it, and whether the viewer is one of them, with the chip itself as the toggle so reacting and un-reacting are one control rather than two. The tooltip names who reacted, resolved against the session member list the window already holds. A trailing "+" opens the picker where there is already a bar to hang it on; a message with no reactions yet goes through the menu. The picker opens as the six people actually use and expands into the existing searchable one on demand, because a 320px panel dropped into a 320px chat window is not a quick reaction. Editing swaps the bubble for a box holding what the SENDER wrote — originalText, not the translation a reader may be looking at — with Enter to save and Escape to cancel. Escape stops there rather than reaching the window chrome, which would have closed the whole chat on a cancelled edit. An edited message carries "(edited)" for everyone, which is what makes editing after somebody has read it honest; that disclosure is why edit, unlike unsend, is still offered on a message that has been seen. While the box is open the conversation is told somebody is editing — the "…is editing" indicator has existed on both ends since the typing work and nothing on web has ever raised it. Reference-counted and republished on an interval, because receivers clear a stale indicator after three seconds and a long correction would otherwise appear to have stopped. Both controls come off with the composer when the organization's chat policy freezes this member: they go through the same gate on the server, and offering a control that will be refused is worse than not offering it. Takes @kamo/chat-core 0.1.8 and @kamo/realtime 0.1.1.

  23. · MediaService

    React to a message with an emoji, and correct one you sent

    POST **************** sets or clears the caller's emoji and answers with the message's whole reaction set, so a client that raced itself converges on the server's state instead of on its own guess. Reacting is treated as speaking: same session-membership check and same organization chat policy as sending, because a member who may not reply here may not answer with a 👍 either. The emoji itself is validated, not trusted. It is rendered verbatim under somebody else's message in everyone's conversation, so the gate is whitelist-shaped and structural: split on the zero-width joiner, no segment may hold more than one pictograph. That takes a family or a skin-toned profession and refuses 👍👍, which is two reactions typed as one, along with text, lone joiners and half a flag. PATCH /api/media/messages/{id} corrects a message. Deliberately NOT gated on whether anybody has read it, which is the one rule unsend does have — see the shared-library commit for why the two differ. It refuses a blank body on a message with no files (blanking a message is unsending it, which has its own rule), refuses a message already delivered to an external social platform (nothing here can reach that copy), drops the message's cached translations, and files what it used to say. Also: MESSAGE_DELETED and ATTACHMENT_REVOKED now carry `id` as well as `messageId`. The relay projects every frame through ChatMessageDto, which has no messageId field, so both events reached the browser with id: null and every subscriber discarded them — an unsent message stayed on the other side's screen until they reloaded. The relay's javadoc now says out loud that it is an allowlist, since that is what made a silent drop possible.

  24. · kamo-shared-library

    Per-message emoji reactions, and a message its sender can correct

    Two things a member asked for on the individual chat: an emoji anyone in the conversation can put on a single message, and the ability to fix a message you have already sent. MEDIA_OBJ_REACTIONS is one row per (message, member, emoji), not a count per emoji. Every question the product asks of a reaction is about WHO: the chip is a toggle, so it has to know whether the viewer is already in it, and its tooltip names who reacted. The unique constraint is what makes the toggle safe under a racing double-click. Editing keeps MediaObjMsg.message as the current body, stamps DATE_EDITED, and files the body it replaced in MEDIA_OBJ_MSG_EDITS. That table is the answer to why editing is NOT gated on "has anybody seen it" the way unsend is. Unsend makes read words vanish, so refusing it after the fact is what stops a conversation being rewritten behind a reader's back; a typo, on the other hand, is almost always noticed after somebody has read it, and the same gate would refuse corrections exactly when they are wanted. Disclosure pays for it instead — every reader is told the message was edited, and what it used to say is kept rather than overwritten. ChatMessageDto gains the fields both events ride on. That DTO is not documentation: the NATS to STOMP relay in MediaService deserializes into it and re-emits a fixed field list, so a field it does not declare never reaches a browser.

  25. · KlusterServices

    Recorded greeting, dial-by-extension, 1/2 to operator

    Replaces the music-on-hold hold pattern with the recorded intro: - **************** plays the greeting AND listens at the same time, so a caller can dial over it from the first second. Playback() would ignore keypresses until the file ended — 70 seconds of being ignored. - 1 and 2 both go to the operator extension. - Nothing dialled by the end of the greeting goes to the operator, after a 4-second grace window so a caller who starts dialling on the last word can finish; without it the second digit arrives after we have moved on. - Direct dial matches _[3-9]XXX, not _XXXX. With _XXXX a single "1" is also a partial match for a 4-digit extension, so Asterisk must sit through TIMEOUT(digit) before it can rule that out and the caller hears four seconds of silence. The cost is that 1 and 2 are reserved, so no extension may begin with them — which the menu already required. Extensions renumbered 2523 -> 8912 (Briana) and 2000 -> 8000 (Erik). The WebRTC endpoint block is keyed by extension NUMBER, so renumbering silently strands an extension there: it still registers over WS, but with no DTLS/ICE the media never establishes and the caller gets a connected call with no audio. 8000 had no block at all as 2000 — and it is now the destination for every caller who dials 1, dials 2, or says nothing, so it had to gain one. Greeting audio is not in this ConfigMap: at 1.1 MB it would blow the etcd object limit. It lives on the asterisk-lib hostPath and the conversion command is documented next to the global that names it. Verified live end to end: greeting plays (BackGround custom/kamocrm-main-intro); 1 -> from-did-direct 8000; 8912 -> its voicemail with all 3 messages intact; an unknown extension plays pbx-invalid then falls to 8000.

  26. · KlusterServices

    Inbound auto-attendant — music on hold + dial-by-extension

    Inbound calls had nowhere to go: the `incoming` table was completely empty, so even a delivered call would have dead-ended. Adds the attendant context plus the two tunables it reads. WaitExten's m() option is the mechanism — it plays a music-on-hold class AND collects digits simultaneously. Background()/Playback() stop at the end of the file and MusicOnHold() collects nothing, so neither can express "music until the caller dials". The IVR module is not installed on this box (no ivr_details table) and would want a recorded announcement rather than music anyway. Appended to extensions_custom.conf under a grep guard rather than written over it: kamo-asterisk-support owns a marker-delimited block in the same file and preserves everything outside it byte-for-byte. Also creates /var/log/asterisk/cdr-csv. cdr_csv logged an ERROR *and* a WARNING for every call leg because that directory is absent from the image. The MySQL CDR is the one we read so nothing was broken, but it buried the Asterisk log — the same log used to diagnose call faults. Verified live: attendant answers and holds in WaitExten(45,m(default)); an invalid extension plays pbx-invalid and loops back to the music; a valid extension reaches from-did-direct and rang PJSIP/8080 (ANSWERED, 20s). The ATTENDANT_TRIES counter is initialised at the label too — reaching `menu` without it left $[ + 1] a parse error that then corrupted the following GotoIf, sending the call somewhere unintended instead of failing loudly. The inbound route and Custom Destination rows live in the FreePBX MariaDB (persistent StatefulSet) and are not reproduced here.

  27. · InitializerService

    One shared calendar per org, one primary per member

    Provisioning had nothing underneath it, so two simultaneous first loads both inserted. Collapses what that already made — twenty duplicate rows across seven organisations and thirteen memberships, oldest wins, losers deactivated rather than deleted and anything pointing at one re-pointed first — then adds the partial unique index per calendar kind that makes a second row impossible. Partial because the invariant is on the PROVISIONED calendars: a plain unique index on the organisation would forbid a member's own, and one on the membership would forbid the extra calendars a member is free to create.

  28. · kamo-internal

    Review emergency access, and gate the last two surfaces

    The break-glass review endpoint had a client function that nothing called: every emergency chart access was recorded and there was no screen to read them on. A break-glass nobody reviews is an audit trail rather than a control, which is the opposite of why the grant is time-boxed and reason-bearing in the first place. It is a sixth tab on the records office, the compliance surface, and it shows the stated reason verbatim — that sentence is the whole substance of the review, and a summarised version of it decides nothing. Two gates that were missing. The break-glass button is now offered only to a role that holds BREAK_GLASS_PATIENT_ACCESS; showing it to somebody who cannot use it teaches them the refusal is arbitrary, when the honest answer is that their role does not include emergency access — so they get that sentence instead. And the disclosure accounting is its own segment behind VIEW_DISCLOSURE_ACCOUNTING: producing a copy for a patient and reading the ledger of everywhere their record has already gone are different jobs. That is the last of the 26 clinical rights to gate something a person can reach.

  29. · kamo-internal

    Raise and send a claim from the queue that found the work

    The revenue cycle read seven queues and could not raise a claim. Each write now hangs off the queue that surfaced the need for it rather than living on a screen of its own. "Never charged" is a list of procedures performed with no charge and no stated reason — nothing failed and nothing told anybody — so the useful thing beside each row is creating the charge. The row leaves the queue once it exists, which is the point of the queue. Sending is offered only on a claim still in DRAFT. Re-sending an accepted claim is how a practice gets paid twice and then recouped, and the payer's duplicate logic is not something to rely on noticing. The confirmation says the claim is a draft and to check it before sending, because the two steps are deliberately separate and "raised" alone would leave people believing it had gone to the payer. Both actions are absent without MANAGE_CLAIMS. Refusals carry the server's own sentence, which names a self-pay restriction or a missing permission — either is more use than anything this screen could invent.

  30. · kamo-internal

    Correct a patient's details

    Details could be entered once and never fixed. The panel sits under the summary rather than in a settings screen, because the moment somebody notices a misspelt name is the moment they are looking at it — and it renders nothing at all without EDIT_PATIENT_DEMOGRAPHICS. Only fields the user actually changed are sent. The server leaves an absent field alone, and diffing against what was loaded makes that true in practice: a screen that does not render every column then cannot silently empty the ones it omits. The re-index consequence is stated before saving, not after. A name or date change re-files the patient for duplicate checking and for records requests from other practices; without saying so, a married name looks cosmetic and the result is a patient findable only under the name they no longer use. The MRN is shown and not editable. It is how the outside world names this person, and renumbering breaks every reference a payer, a lab and a prior letter already hold.

உங்கள் உலகம் காத்திருக்கிறது.

நிமிடங்களில் இலவசமாகத் தொடங்கவும். நீங்கள் தயாராக இருக்கும் போது பொருட்களை சேர்க்கவும் - எந்த மாற்றமும் இல்லை, புதிய உள்நுழைவுகள் இல்லை.

எப்பொழுதும் இலவசம் — கடன் அட்டை தேவையில்லை