மாற்று பதிவேடு
என்ன புதிது.
அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.
· kamo-shared-library
Line, note, vendor, sign-off and clock writes on a job
SW6b Task 3, library half. Eight new operations on ServiceJobService, plus the wire records for them and the three reads the detail page needs. Three things the server owns and no client may send: - The totals. labourTotal, materialTotal and total are recomputed from the job's lines on every line write, and a line's own total is quantity x unitAmount. LineInput has no total member and JobInput no longer has the job's three. They were on JobInput when SW6a shipped because there was nothing to derive them from; leaving them once there was would be a second door straight past the rule. **************** is what the far side of that door looks like -- it takes a subtotal off the request AND adds its line totals to it, so a client sending both gets a figure that matches neither, which is the failure QuoteTotals' javadoc exists to name. - The sign-off timestamp. signOff stamps signoffAt when the flag goes true and clears it when it goes false, so the pair can never disagree. SignoffInput carries a boolean and free text and no time. - The clock. clockVisit stamps actualStart/actualEnd AND moves the visit's status with them -- ON_SITE in, COMPLETED out. A clock-out with no clock-in is an IllegalStateException naming the visit's state (409 at the controller): an end with no beginning contributes zero to totalLabourMinutes, so the symptom would be a time sheet short by a day with nothing saying so. A second clock-in is refused too; a second stint is a second visit. Every child is resolved with its job uid AND the org as predicates in ONE query -- findByUidForJob on the line, vendor and visit repositories. Resolving by uid and org alone would make DELETE /{jobUid}/lines/{lineUid} reach any line in the org through a URL that looks entirely correct. JobView gains lines/notes/vendors: null in the list projection (not loaded), always filled in the detail projection (empty means none). Loading them per row would read four tables to paint a grid that shows none of it. Mutation-tested: 9 mutations of this file, each producing a distinct named failure -- dropping the recompute, adding line totals on top of the stored figure, dropping the clock-out guard, not moving the visit status, not stamping the sign-off, not clearing it, and resolving each of the three children by uid alone.
· VOIPService
Implement KamoPBX extension, user and device management
The provider advertised extensionManagement, userManagement and deviceManagement as true while implementing none of them, so the UI would have shown buttons that throw 501. Capabilities now match behaviour. An extension in KamoPBX is three documents — a user, a device owned by that user, and a callflow mapping the number to the user. Creating only some of them yields either a number that rings nothing or a handset nothing routes to, so all three are created together and a partial failure rolls back what was made. Deleting an extension removes only the callflow. A member may own several devices, and deleting someone's registered handset because a number was removed would be a far worse surprise than an orphaned document. 253 tests pass, 5 new covering create, rollback, validation, delete scope and capability honesty.
· kamo-shared-library
Job lines, notes and vendors so the work-order tabs can persist
Labour and parts become ServiceJobLine rows on the job rather than ServiceLineItem rows, because OrderLineItem.order is optional = false: a ServiceLineItem cannot exist without an Order, and a technician records labour long before anything is billed. Making that parent nullable would let a PurchaseLineItem exist outside an order too, so the mapping test pins the invariant instead. ServiceJobNote is authored and append-only - no updatedAt, createdAt updatable = false and stamped in @PrePersist. ServiceJobVendor is the many-side the Vendors tab has always displayed; role stays free text because no vendor-type vocabulary exists to freeze into the schema. Every repository reaches the org through an EXPLICIT join of the job. The implicit l.job.organization.id renders as an INNER join and silently drops rows whose association is null; ServiceJobLineMappingTest asserts the shape rather than trusting it.
· kamo-internal
A failed clinical load must not render as an empty result
Two of these shipped before anyone noticed, and the guard found two more in the newest panel the moment it ran — failing to load what a patient's other practices have already sent read as "nothing received yet", and failing to load the partner list read as "no exchange partners are set up", which would send somebody to add partners that already exist. That is why this is a build guard and not a review note. The failure is silent by construction: no error, no log, nothing on screen, and it looks exactly like the healthy empty state. Nobody will ever report it as a bug. On a chart an empty list is a statement — "no measurements", "nobody overdue", "no allergies" — and people act on it. The same distinction already runs through the module on the server side: "they have nothing" versus "we could not ask", and 503 rather than an empty list from the exchange transport. This enforces it on the client. Scoped to the clinical surfaces, because other verticals have their own conventions. A catch that sets an error, logs, rethrows, returns, or reloads passes — and so does one carrying a comment explaining why the silence is right, which PatientCommsPanel relies on: its sibling timeline reports the same outage and a second banner would tell the reader nothing new. Chained into npm test.
· SecurityService
Expose platform settings through the system-config API
toDto is an explicit allowlist and updateConfig takes a typed body, so the seven new SYSTEM_CONFIGURATION columns would have been invisible and unsettable — the platform settings card would have rendered empty and saving would have done nothing. The administrator password is never returned. It is the master credential behind every tenant account, so a GET yields the sentinel "***" when set, and sending "***" back means keep — otherwise saving any other field on the form would overwrite the credential with the literal string. Gated by MANAGE_VOIP_SMS alongside the existing VoIP fields, so MANAGE_DOCUMENT_AI cannot reach it.
· VOIPService
Decrypt the KamoPBX credential; test the refusal path
The master credential is stored as ciphertext by the platform settings UI, so it must be decrypted before use — handing ciphertext to the API as a password would present as a bad credential and send whoever debugs it the wrong way. Two new tests cover the refusal path that previously surfaced a raw Kubernetes DNS error to an org admin: no master account id, and no administrator credential. Both assert the message names the settings page. 248 tests pass.
· KlusterServices
Swap the inbound greeting to KamoCRM-Main-Intro-3
Third take, 40s (down from 60s). Uploaded alongside the previous two rather than over them, so rolling back to either is a one-line change to KAMO_ATTENDANT_INTRO plus a dialplan reload — no re-upload. 48 kHz stereo source converted to the 8 kHz mono Asterisk requires. Verified live: checksum matches the local file, Asterisk decoded it to 319914 bytes of u-law (39.99s at 8000 B/s), and the CDR shows BackGround custom/kamocrm-main-intro-3.
· InitializerService
Per-seat storage, and a flat Enterprise pool
2 GB per seat on Free, 4 on Starter, 5 on Business. Enterprise stops scaling with seats entirely - a flat 150 GB for the organization - which "N GB per seat" cannot express, so that row carries a prose override the buying surfaces now prefer over the number. Feature unit is GB/seat. The billing unit is a seat throughout; headcounts stay users, being people rather than units.
· kamo-internal
Platform settings card, and stop showing FreePBX copy
Three problems on the KamoPBX phone-server page, all mine: - The PHONE_SERVER_ID card was ungated, so every provider — KamoPBX, RingCentral and Teams included — was told to paste the id into kamo-asterisk-support's config on a FreePBX host. It is FreePBX-only and now renders only there. - KamoPBX had no provider-specific content at all. An org saw a name, a number, an active toggle and that FreePBX instruction. It now gets a card saying Kamo runs it, its SIP address, provisioning state, and what to do next. - There was NO platform configuration for KamoPBX anywhere, which made the provider unusable: nothing could set the master account or credential. A card on ?tab=voip-sms now owns them and warns while unconfigured. Dictionary keys pushed first (messages/ is gitignored here).
· VOIPService
Read KamoPBX platform config from the database
Adding a KamoPBX phone server failed with a raw Kubernetes DNS error — 'I/O error on PUT ... **************** — shown to an organization admin who can neither read nor act on it. Two causes, both fixed: - The provisioner attempted the call with no platform configuration at all. It now checks first and refuses with an actionable message naming the settings page, instead of failing open into a DNS error. - Settings were @Value-only, so there was no way to configure KamoPBX without a redeploy. They now come from the SYSTEM_CONFIGURATION singleton, which the platform settings UI writes; the @Value defaults remain a fallback so the service still boots before the migration has run.
· kamo-shared-library
KamoPBX platform settings on SYSTEM_CONFIGURATION
KamoPBX is Kamo's own platform, so all of its configuration is platform-wide: base URL, master account id, account name, admin credential, SIP realm suffix and WSS URL. They sit on the SYSTEM_CONFIGURATION singleton beside the existing VoIP tunables so an operator can set them from the platform settings UI instead of needing a redeploy to change an environment variable. The admin password column is AES-256-GCM ciphertext and carries @JsonIgnore: this row serializes straight to the platform settings UI, and without it the master credential for every tenant would be on the wire.
· InitializerService
Seed KamoPBX platform defaults
Hibernate ddl-auto adds the SYSTEM_CONFIGURATION columns; @ColumnDefault only applies to newly inserted rows, so the existing singleton needs backfilling. Seeds the non-secret defaults only. The master account id and admin credential are deliberately left NULL — that is what makes the provisioner report 'not configured yet' and refuse, instead of failing open or surfacing a Kubernetes DNS error to an org admin who can neither read nor act on it. Every statement only fills a NULL, so an operator's UI edits survive redeploys.
· kamo-internal
The work-order grid reads real service jobs
Every row in this grid was a dead link by construction. generateMockWorkOrders minted ids String(8000 + i), the row click pushed /commerce/service/8001, and the handler declared @PathVariable UUID id — so the id failed conversion before any handler ran and the detail page rendered its not-found state. Fifteen rows, fifteen dead links, on a screen that looked finished. Both surfaces now read ServiceJob through **************** The proxy exports GET, PUT and PATCH — exactly what ServiceJobController maps, and neither commerce proxy beside it was a safe template: quotes exports GET/POST/PATCH/DELETE with no PUT, service-task-books exports GET/POST/PUT/DELETE with no PATCH. Copy either and one save answers 405 raised by Next itself, with nothing in the server logs, while every read keeps working. serviceJobWireContract.test.ts pins the verb set against the controller's annotations in both directions, and compares every JobView/VisitView/AddressView record component to the TypeScript member by name. That name comparison is the second fix. posApi.WorkOrder declared serviceName, serviceId and engagementId against a DTO sending serviceOfferingName, serviceOfferingUid and engagementUid. The three never matched, so the detail page's subtitle and its "Service" summary row were undefined for the life of the screen and fell back to an em dash — which reads as "nothing recorded" rather than as a typo, which is why nobody found it. Deleted 489 lines of a detail Dialog that could not open: detailOpen was initialised false, setDetailOpen(true) appeared nowhere in the file, and selectedOrder was independently always null. Six handlers fed it and nothing else and went with it. Its Notes tab was the only one in the codebase, so it was mined out first — **************** Departures worth knowing: - No technician name anywhere. ServiceJobApi carries visits[].assignedMemberId and nothing else, and no endpoint resolves a batch of member ids to names — memberDirectory is a paged search whose own note forbids holding the staff list. The column is gone rather than showing an id dressed as a person. - "Time Attended", not "Labor Hours". The server sums totalLabourMinutes from each visit's actual window because priced labour hangs off an Order with no link to a job. Rendered with formatLabourMinutes; no float-hours field on either side. - trade replaces the ten free category strings, which were never a vocabulary anything could round-trip: "Plumbing Repair" is PLUMBING plus WorkClass.REPAIR. - Bulk status now issues one PATCH per job and reports partial refusals, because the status machine checks each transition. The old handler set the field locally on all of them and reported success. - Labour, parts, time entries and vendors stay local useState. No entity backs any of them; SW6b builds those. Dictionary landed first: kamo-translation-dictionary 947e585.
· kamo-marketing
Plan cards show what a tier ADDS, not everything it has
The cards listed every included feature - 16 on Free, 30 on Starter, 52 on Business, 57 on Enterprise. Four cards of wildly unequal height, each a wall of text, and between them a near-complete restatement of the comparison table sitting directly underneath. The plans are cumulative, so the only question a card has to answer is "what does this get me that the cheaper one doesn't". Each card now leads with the seat line (the one figure that does not accumulate), says which tier it builds on, lists at most six differentiators, and sends the rest to the comparison table via "and N more features". Business drops from 52 lines to eight, the four cards come out near-equal, and the table stops being a duplicate and starts being the destination. Feature order now follows the catalog's own display_order. It was sorted alphabetically by category name, which put "AI & Intelligence" above "Core Platform" and buried what actually sells a tier. CONTRAST: white on the olive accent measured 3.78:1 at the gradient's dark end and 3.01:1 at its light end - both under the 4.5 that 14px semibold labels need, which is why the buttons and badges read washed out. Every accent surface now uses the page's own background colour as ink: 4.53:1 and 5.68:1, with the green kept at full strength rather than muddied to rescue white text. Fallback and JSON-LD seat counts corrected to 25 / 100 to match the catalog; they still said 5 / 25 from the same confusion the card headline had. Verified with a full production build, which caught two typed holes tsc --noEmit did not.
· kamo-internal
Pull a patient's history from the practices they used to attend
The outbound half of exchange, on the records tab beside the release surface — records leaving and records arriving are one person's job, and somebody who has just sent a summary to another practice is the person most likely to want theirs. Three steps shown as three steps, because that is what the standards are: ask whether they know this person, ask what they hold, fetch the ones worth having. An "import everything" button would hide the middle decision and pull a decade of duplicates on a patient who has moved practices twice. The match is never auto-picked: a wrong one reads another person's history into this chart. Retrieved and reconciled never look alike. A document from another practice is their assertion — it sits attached and readable, and nothing in it reaches this chart's problem, medication or allergy lists until a clinician records what they took AND what they left. The empty state says so, because "received" quietly reading as "applied" is how a resolved 2019 condition becomes an active problem. A practice with no network onboarding is told once, plainly, instead of being given a button that always fails — and "they have no record matching this patient" is labelled as their answer rather than a failure to ask. Those two must never look alike: the first is a clinical fact somebody may stop looking because of. Also caught by the i18n guard and fixed: I passed a {partner} value to a message with no such placeholder, and the value was the remote patient id anyway.
· SecurityService
The outbound half of TEFCA — ask, don't only answer
Everything built so far answers other people's queries: discovery, document query, document retrieve, the match-review queue, the accounting. A practice could be ASKED and could not ASK — and asking is the direction a clinician actually needs, because a new patient's history is at whichever practice they used to attend. ExchangeRequest has carried a direction column the whole time and only INBOUND ever wrote to it. An outbound query is an ACQUISITION, not a disclosure. The PHI travels toward us, so nothing here touches the disclosure ledger — recording it there would put a line in a patient's §164.528 accounting saying their record went somewhere it came FROM. Discovery reads demographics from OUR record, never from the caller. An endpoint that accepts typed-in names is a way to ask the network "do you have anybody called this", which is a different and much worse capability than "fetch my patient's history". Every step needs VIEW_CHART and a care relationship, discovery included: which practices hold records for someone is itself information about them. A retrieved document is filed UNRECONCILED and stays that way until a clinician says what they took from it. Another practice's summary is their assertion, not this practice's record; auto-merging one is how a resolved condition from 2019 becomes an active problem and a discontinued drug reappears on a list somebody prescribes against. Reconciling requires a note, because "reconciled" alone leaves the next reader unable to tell whether the outside history is in the chart or still only in the attachment. Failed queries are recorded as firmly as successful ones, in their own transaction. "We asked and they refused" is what somebody investigating a gap in a patient's history needs, and rolling the row back with the failure would erase exactly the case worth keeping. THE TRANSPORT IS A DELIBERATE SEAM. Under TEFCA a Participant authenticates with an X.509 certificate issued under the Common Agreement — obtained by signing the Participant agreement and completing QHIN onboarding, not by putting a string in a database. This platform has no encryption at rest for a secret of that kind and no QHIN relationship to obtain one from, so inventing a token column and calling it TEFCA would produce something that looks federated and connects to nothing. What ships is a transport that refuses every call with a sentence naming the missing onboarding, and 503 rather than an empty list — "no documents found" is a clinical fact a clinician may act on and "we could not ask" is not, and a system that answers both with an empty list teaches people to trust an absence that was never established. Retrieved documents get their own bucket and their own storage line, keyed tenant-first like the exports, because nothing deletes them and a practice that queries the network accumulates every summary it was ever sent. 16 tests over everything above the seam.
· kamo-shared-library
A line for documents retrieved from other organizations
Its own domain rather than a share of DOCUMENTS, for the reason bulk exports got one: a practice that queries a health information network routinely accumulates every summary it was ever sent, and nothing deletes them — a retrieved document is the evidence of what this practice was told and when, which is what a later reader checks when the chart and the outside history disagree. Billed even though the bytes came from somewhere else. The storage is real, it is held for this practice, and it is held because this practice asked for it.
· VOIPService
Provision a KamoPBX account when a phone server is created
Adding a KamoPBX phone server now creates the underlying account and writes the account id and realm back to configJson. Any configJson the client sent is DISCARDED rather than merged: there is no legitimate field for it to carry, and accepting one would be a way to point an org at another tenant's account. A provisioning failure leaves the row behind marked not-connected with the reason, rather than deleting it — the id may already have an account created against it, and the operator can retry from the instance page. The client is a shared bean so the token cache is reused; constructing one per call would re-authenticate against KamoPBX on every request. kamo.pbx.* are platform settings with blank credential defaults, so an unconfigured platform reports 'not configured' instead of failing open. 246 tests pass.
· kamo-internal
KamoPBX is selectable
The provider is implemented in VOIPService, so the coming-soon gate comes off. Description no longer names the upstream project and says what actually matters to the buyer: Kamo runs it, and there is nothing to sign up for. Surgical edit only. This array now also carries billingClass and feeds lockedReason's entitlement gating from concurrent work, so it is left in place rather than extracted — two sources of truth for the provider catalogue would be worse than a long file. Dictionary key providersBullet1 was pushed first (c822cfeb); messages/ is gitignored here so a green local build proves nothing about production.
· SecurityService
Right-gated service job API
**************** — list, get, update, and PATCH status. Copies ServiceTaskBookController's idiom: orgId(req) or 401, a right check or 403, a String path variable through parseUuid or 400, and one failed() helper splitting the service's IllegalArgumentException into 404 for a missing row and 400 for everything else. A missing job is a 404 to match every sibling handler — the detail page renders its not-found state off that code, and a 400 there would hide 'no longer exists' behind 'your request was malformed'. A refused status transition is a well-formed request against a job that exists, so ServiceJobStatusMachine's message naming both states reaches the client as a readable 400. Reads take VIEW_SERVICE_WORK and writes take MANAGE_SERVICE_WORK, which is a CHILD of VIEW in the rights tree — so a write that settled for VIEW would be a silent downgrade that still looked gated in review. The gate test calls every write with a view-only session to prove it does not happen. The test drives the real handlers rather than grepping the source. ServiceTaskBookRightsGateTest says in its own javadoc that a source scan 'cannot prove the check runs before the work'; BaseLosController resolves the session from a request attribute, so a MockHttpServletRequest carrying a rights list is enough to read the real status code back — and to assert the service was never reached, which is the half a scan cannot see: a handler that acts and then checks returns 403 having already written the row. Mutation-tested: the manage right swapped for view, a right check deleted outright, the right check moved after the service call, failed() flattened to always-400, PATCH /status turned into PUT, and the session's org replaced by a literal. Each produced a distinct failure naming the handler at fault.
· kamo-shared-library
An org-scoped read/write API for service jobs
ServiceJob has been a mapped entity with repositories and passing tests and no way to read a row. This gives it the header the finished work-order UI already renders, so the grid can stop generating its own. Shaped on ServiceTaskBookApi/Service: String ids and ISO-8601 strings on the wire, orgId first on every method, every read mapped to a view INSIDE the transaction because ServiceJob.visits is LAZY, and the tenant predicate in the JPQL rather than a check after the load. ServiceJobRepository already carried all four queries, so it is untouched. Two shape decisions the DTO had to make: - totalLabourMinutes is an Integer, never float hours. A 20-minute task is 0.333 hours and three of them are not an hour; ServiceLineItem.labourMinutes argues the case for the column and it holds on the wire too. It is summed from the visits' actual windows, which is the only labour a job can reach: ServiceLineItem hangs off an Order with no link to a job. Time attended, not time billed. - trade is not the UI's category. Ten free composite strings against fifteen ServiceTrade constants, not 1:1 — 'Plumbing Repair' is PLUMBING plus WorkClass.REPAIR, and the work class lives on a line item. The wire carries the trade and the client renders a label. Status moves through ServiceJobStatusMachine and nowhere else, which is why JobInput has no status member and StatusInput is its own record. A status reachable through the general update would be a second door onto the column, and the second door is the one nobody checks. Tests are behavioural, not source scans: the repository is a Proxy whose findByIdForOrg honours the org and whose findById deliberately does not, so an unscoped read is caught as a leak rather than as a break. Mutation-tested — assertTransition deleted, assertTransition moved after setStatus, the org-scoped query swapped for findById, the minutes converted to hours, the status filter moved out of SQL, getVisits() left unread, \@Transactional removed, and a 'status' component added to JobInput. Each produced a distinct failure naming the property it guards.
· kamo-login
Reveal the preselected workspace, and give the list the org's scrollbar
Two things about the workspace picker, both about the same list. The workspace a member used last is preselected and named on the button, because this screen is a confirmation rather than a survey. But nothing scrolled to it. A member of twenty organizations whose usual one sits fourteenth opened a list showing a highlighted row they could not see, under a heading, above a button naming a workspace that appeared nowhere on screen. Whether the preselection was visible came down to where that org happened to fall in the list. It is brought into view once, on open, and only when it is genuinely out of sight — a row already on screen stays where it is, because scrolling a list that needed no scrolling reads as the page moving under you. Centred rather than flush to an edge, so the rows above and below still say "this is a position in a list". `scrollTop` rather than `scrollIntoView`: that scrolls every scrollable ancestor, and on a sign-in screen it can take the page with it. The list is now `position: relative` so a row's `offsetTop` is measured against the list itself — exact, and unlike a bounding rect it is not shifted by the rows' entrance transform. The scrollbar is the second thing. Every accent on this screen is mixed from `--color-primary`, which the theme stylesheet has already set to whichever organization the screen is branded as: the selection rail, the row tint, the "Last used" pill, the button. The scrollbar was the one piece of chrome still rendering in the browser's default grey, which on a strongly-branded tenant is the only grey on the panel. It is tones of the same token now — a translucent thumb that deepens on hover, a faint track, both rounded. Written as a <style> element rather than the inline styles the rest of this component uses, because a scrollbar is only reachable through pseudo-elements. Both vocabularies are there and neither is a fallback for the other: WebKit and Chromium read the pseudo-elements and ignore `scrollbar-color`, Firefox reads `scrollbar-color` and has no pseudo-elements.
· VOIPService
Implement the KamoPBX provider
Replaces the stub. KamoPBX is Kamo's own platform, so the organization holds no credential at all: KamoPBXAccountProvisioner creates the account when a phone server is added, and KamoPBXClient mints a short-lived child-scoped token per call from Kamo's single master credential. Auth uses the child account's OWN api_key, read with the master token and exchanged — documented and schema'd. The undocumented as:{account_id,owner_id} claim is deliberately not used: it fails open, returning the full-privilege PARENT token with no error when owner_id is omitted. scopedToken() asserts the returned token is scoped to the child and rejects a parent-scoped one as the privilege escalation it is. Also fixed from the stub: - getCallHistory returned null, which NPEs any caller that iterates. - CDRs are per-leg, so history is collapsed on interaction_id or one call shows up two or three times. - callResult is pinned by test to exactly ANSWERED/NO ANSWER/BUSY/FAILED; anything else silently makes missed-call counters read zero, so unknown hangup causes collapse to FAILED rather than passing through. Platform settings (base URL, master account, admin credential, WSS URL) are @Value-injected and never per-org. 246 tests pass, 29 of them new.
· KlusterServices
Apps, media, edge manifests and network policy
- config.ini carries a [zone] block from day one so a second region is a config edit, not surgery. Secrets are substituted by an entrypoint at container start rather than baked into the ConfigMap. - ecallmgr is co-located with FreeSWITCH in ONE pod: raw Erlang distribution, shared cookie, no TLS, and splitting them crosses the 1500/1420 MTU boundary. - Kamailio advertises 47.181.8.87 while listening on 192.168.4.36. - Cilium is default-deny and toEndpoints:[{}] is namespace-scoped, so the explicit kamo/kamovoip -> kamopbx-apps:8000 rule is what stops Phase 3 failing as a silent timeout.
· kamo-internal
**************** and lock the Enterprise card
Registers the right client-side - list, colour, icon - and adds it to the coverage test's non-tab set with a note on what it actually gates, since it guards an action across two services rather than a Configuration tab. The order wizard rendered every active plan, Enterprise included, with a price read straight from the catalog. The server now withholds that price and refuses the subscription outright, so the card was going to show a plan it had no number for and could not buy. It is disabled with a note instead of letting somebody walk the whole wizard and meet the refusal at the end. The server is the boundary either way - EnterprisePlanGuard in BillingService, and POSController for the price. This only saves the wasted trip.
· BillingService
Refuse a self-serve Enterprise subscription
Enterprise is negotiated. Hiding the price is presentation; this is the boundary. A request naming the Enterprise plan is refused unless the caller holds **************** so guessing the plan code and posting it directly gets the same answer as reading the console. Fires only on a CALLER-SUPPLIED plan code. The nightly reconcilers amend live subscriptions through the same update path with an empty body and no HTTP request at all - guarding a null would stop billing every negotiated customer. The rule is "you may not NAME this plan", not "this plan may not be touched". Identity is resolved exactly as PlatformAdminInterceptor resolves it, and the verdict is left to the shared PlatformRightsResolver, so this cannot drift from what SecurityService decides about the same person. The refusal names no permission. A customer gets the thing they can act on - contact us - not a hint that a right exists and they might obtain it.
· SecurityService
Withhold Enterprise pricing from the catalog
Enterprise is negotiated with KamoCRM directly, so its rate is not ours to publish. Every surface that quotes a plan price reads this one endpoint - the public pricing page through the APIService proxy, the in-app order wizard and the registration funnel - so the prices are dropped from the payload here rather than asking three clients each to remember to special-case one plan. An internal (public-proxy) call holds no platform right and is always redacted. A session call is redacted unless the caller holds **************** The plan row itself stays: a reader still needs to see what Enterprise IS and be told to talk to us. Only the numbers go. This is presentation. EnterprisePlanGuard in BillingService is the boundary that refuses the purchase - somebody who guesses the plan code and posts it directly gets the same answer as somebody reading the console.
· kamo-internal
Show what the chart says before the prescription is written
The server now refuses a prescription that conflicts with a recorded allergy, a drug the patient is already on, or a patient nobody has ever asked about allergies — and returns what it found. This renders it. A card below the form rather than an inline error, because the prescription is intact and the next action is a clinical decision, not a correction. High-risk warnings are marked: "penicillin" and "penicillin, high criticality" are not the same warning. The override field asks the question — "why are you prescribing this anyway?" — and its helper says what the answer is for. Prescribing over a recorded allergy is a clinical decision somebody may need to defend later, and the reason is what defends it. The button stays disabled until there is one. "Don't prescribe" sits beside it. A warning with only one way out is a warning people learn to click through. Warnings are cleared on every fresh attempt unless a reason is being carried, so an acknowledgement typed for one drug cannot be reused against a different one.
· KlusterServices
CouchDB and RabbitMQ on k3m1
CouchDB q=8 / n=1, verified live. q is fixed at database creation and KamoPBX creates a database per account plus monthly MODBs, so q is sized now for the eventual 3-node cluster; n can be raised later by editing shard maps, q cannot. Auto-compaction enabled — upstream ships it off and KamoPBX's write pattern fragments badly. Three deployment bugs found and fixed live: - Mounting the ConfigMap over /opt/couchdb/etc/local.d makes it read-only and the entrypoint dies writing docker.ini. Uses subPath for the single file. - A fresh Longhorn volume is root-owned; couchdb runs as uid 5984 and exited 1 with no log output at all. Added fsGroup plus a chown initContainer. - rabbitmq-diagnostics needs more than the default 1s probe timeout; the server was healthy (9s startup) but the probe failed it. RabbitMQ runs as kamopbx, not the shipped guest:guest with loopback_users off.
· kamo-shared-library
Tell "the bytes are gone" apart from "storage is down"
openRange collapsed every node failure into one IOException, so a caller could do nothing with it but return 500. A browser's <img>/<video>/<audio> cannot render a 500 — it gets one bit, "failed", and latches on it — so an attachment whose object was never written answered a member's play button by going quiet. The failure reached us only because someone said "it sent but they can't play it". The two causes deserve opposite answers. Content absent is permanent and worth saying plainly; storage unreachable is transient and worth retrying. Raise ObjectNotStoredException only when the WHOLE fleet answered and every answer was NoSuchKey: a node that could not be reached might be the one holding the object, so a mix stays an ordinary IOException. Telling a member their file is permanently lost during a partial outage is the one mistake here that cannot be walked back. It extends IOException, so all seven existing call sites keep working untouched and opt into the distinction by catching it first.

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