Nenda kwenye maudhui
KAMOUniverse

Mabadiliko

Nini kipya.

Kila kuboresha jukwaa, kama ni meli.

  1. · kamo-internal

    Offer a member only the kinds they can receive

    The notification centre and its settings drew every kind to everybody. A member with no Leads app was shown a "Lead callback" filter that could only ever return nothing, and a mute switch for a kind they could not receive — and the same for the calendar kinds and meeting reminders. Both surfaces mapped one hardcoded array and read neither the organisation nor the member's rights. A kind belongs to an app, so it is gated the way that app's own door in the nav is gated: the organisation has the app AND the member holds the app's viewing right. The app is never written down — each kind names a RoleRightType, and the right carries its own ServiceType, which is also how this stays in step with EmailService's gate: both sides state the right and derive the app, so they cannot come to disagree about which app a kind belongs to. The two surfaces answer slightly different questions. The settings tiles are strict: muting a kind you cannot receive changes nothing, and offering the switch says otherwise. The centre's filter rail additionally keeps any kind the member has notifications of, because access can be withdrawn after the fact — a member moved off the sales team still has last week's callbacks, and hiding the filter would leave those rows reachable only under "Everything", which is not a filter at all. Hiding a switch never clears what it stored, so a member whose access comes back finds their mutes as they left them. Rights arrive a beat after first paint, exactly as they do for the nav rail, so the gate fails closed for that beat rather than flashing every kind. hasFeature() now takes anything carrying a name rather than AppType specifically: the same enum is mirrored twice, as AppType and as ServiceType, and a caller reaching an app through a right holds the latter. Widening the parameter is what stops it having to translate between two mirrors of one enum. Enforcement is not here — EmailService refuses to write the notification at all. This is what the member is offered.

  2. · EmailService

    A member is only told about apps they have

    A notification kind belongs to an app — a lead callback is Leads, a meeting reminder is Meetings — but raise() asked only whether the member had muted it. So a member with no Leads app was still written a lead callback whenever somebody else's process raised one for them, and the site drew a filter and a mute switch for a kind they could never receive. Gated now by the two questions that gate the app's own door in the navigation: does the organisation have the app, and does this member hold the app's viewing right. Suppressed the way a mute is — not written, not published, not counted — rather than written and hidden, because a row nobody may see is a row that surfaces the day the app is switched on, dated last March. The app is never named twice. Each kind names a RoleRightType and the right carries its own ServiceType, so this rule and the one the site states in **************** cannot drift into disagreeing about which app a kind belongs to: neither of them says. Two queries at most whatever the size of the fan-out — one for the right across every recipient, one per distinct organisation for the app. NotificationAccessRepository lives here rather than in the shared library for the reason MessageListIndexRepository does: only the notification hub asks the question in this shape, and a shared-library release is a change to every service that depends on it. The organisation comes off each member's own rights row rather than off the request, because the request's organisation id is optional and a fan-out can cross organisations. Fail-closed on a materialised table: MEMBER_RIGHTS_APPLIED stale for a member means that member quietly stops being told things, which is quieter than the bug it replaces but harder to notice. Every suppression logs at debug with the member, the kind and the right that was missing.

  3. · kamo-internal

    The console shows a reader what they may have, and no more

    Two things the /kb page reported to anyone holding VIEW_KB_ARTICLES. The corpus. The audience chips an author ticks in the composer — Members, Team Members, Public — were never enforced for a human reader; KBService served every active article in the org. That is fixed server-side (kbservice 30f2bf9): a member who is not a team member now gets public + member articles, a team member gets all three tiers, and only published ones. This side follows it. The rail. The stat tiles read Everything / Published / In review / Drafts / Archived / Mine / Pinned / Gone quiet, for everybody. Those last four are a report on how the organization's editorial work is going, and reading it took nothing but the right to read an article. railFilters() now returns what the reader can act on. A maintainer keeps all eight tiles. Anyone else gets Everything and Pinned, plus Mine only if they can create an article to own one. The workflow tiles are removed rather than zeroed — the server sends a reader published articles only, so each would sit at 0, and a tile that always reads 0 is still a statement about what exists made to somebody who was not shown it. Published goes too: it would be an exact copy of Everything beside it. "Needs attention" is maintainer-only for the same reason — every row in it is a maintenance instruction. Who counts as a maintainer comes from the overview, not from re-reading the session's rights here. The server already had to answer it to filter the list; answering it twice, in two vocabularies, is how a rail ends up offering a tile the server only ever fills with nothing. It defaults to the reader's shape when absent, so a cached bundle against an un-rolled service errs narrow. clampFilter covers the case the stored preference creates: the rail selection is remembered per member, so a demoted maintainer would return to a DRAFT tile that no longer exists, an empty list, and nothing lit to explain it. The composer now says when an article has been addressed to nobody. Three chips off is left to mean exactly that on the server rather than being widened behind the author's back, so this is the only place they will ever be told. The two audience strings that described the old behaviour were renamed, not edited — translate.ts only queues a key that is missing or still equal to English. A 404 on an article now reads as "Article not found" rather than "Failed to load". Hidden and missing are deliberately the same answer, and "failed" would send a member to support over a system working exactly as intended.

  4. · KBService

    Enforce the audience chips on every human read

    The composer has always offered an author three chips under "Visible to" — Members, Team Members, Public. They were written to the row and indexed into Qdrant so the assistant would respect them, and then no human-facing read ever looked at them again. Every article endpoint was gated on VIEW_KB_ARTICLES and nothing else, so a member holding only that right was served the organization's entire corpus: team-only material, unfinished drafts, articles waiting on review, and the archive. KbReaderScope is the whole gate, and the order of its rules is the design: org + still active never waived, by any right maintainer **************** see everything — a reviewer filtered by audience could be assigned an article to approve and then handed a 404 for it author CREATE is separate from EDIT, so an author who is not a maintainer must still reach their own draft PUBLISHED draft/in-review/archived are editorial workflow audience team member -> all three tiers; anyone else -> public and members. Cumulative, not exact-tier: an article opened to the public is not thereby hidden from staff aiEnabled is deliberately not consulted — it means "train the assistant on this", and requiring it would make the "Visible to" label false. Three chips off is left to mean what it says; the composer warns the author instead. Applied to /articles, /articles/tree, /articles/overview, /articles/{guid}, /versions, /media, /search and both pin surfaces. Notes: - The tree reparents rather than truncates. A team-only overview page with published children is an ordinary shape, and dropping the subtree with its parent would hide articles the author deliberately opened up. Kept pure and static so the rule is testable without a database; also guards a parent cycle, which the old walk would have recursed into. - Lists filter before paging. Filtering a page the database already cut leaves the caller reading the total as the corpus size. - overview counts come from the filtered list, and it now carries the reader's scope so the console's rail is drawn from the same answer. - A hidden article and a missing one both answer 404. A 403 on a guid is an oracle for exactly what the flags hide, and getByGuid's throw is caught so the missing case cannot escape as a 500 and become that oracle instead. - Pinning is gated like the read it keeps; unpinning deliberately is not, since a member most wants to clear a pin whose article was narrowed away from them. Also closes two cross-org write holes found in the same controller: /reorder had no organization check at all — EDIT_KB_ARTICLES in any org could reorder and, through parentId, reparent another org's article into its own tree — and /restore/{versionNumber} had none either. Every other write here checks it. 68 tests green, 25 of them new.

  5. · kamo-internal

    A group per app, a console to grant them, and master notes as widgets

    Three changes that turned out to be one. THE HOME PAGE. "My Master Note" and "Our Master Note" were bespoke panes rendered straight out of page.tsx, gated on nothing, positioned by their own layout ids, and removable by nobody — so a member whose role withholds VIEW_NOTES had no Notes icon, no /notes page, and two notes on their home page anyway. They are registry widgets now, which is what the registry says it is for: one entry buys the grid, the drag, the ordering, the per-member show/hide, the settings screen and the five-right policy. There is no master-note code left in page.tsx. Their layout ids change from master:mine / master:ours to widget ids, so a member who had dragged those two panes somewhere finds them back at their default place once; a permanent two-pane mapping in the layout store would have been the worse trade. The pinned-note panes were already gated on the nav's answer; that answer now lives in one place (lib/notesAccess) rather than being spelled inline. THE GROUPS. A security group is an APP, shown only when the organisation has that app switched on — that gating is the whole reason groups exist. Apps have been a two-level tree since 2026-08-24 and the groups now follow it: POS is "Lines of Business" (was "Commerce") with Retail, Rentals, Service Work, Subscriptions, Procurement, Mortgage Lending, Personal Lending and Patient Records under it. The group list was hand-maintained and had fallen behind the catalog, which is worse than it sounds: a right whose serviceType matches no group cannot be granted in any editor and nothing says so. Meetings, Mortgage Lending, Personal Lending and Games were in that state — 19 rights, ungrantable however the app was licensed. It is now a COVERAGE test: permissionAppSections.test.ts reads RoleRightType.java and fails if any app with rights has no group, and reads ServiceType.java's parent(...) line to pin the nesting. Two more of the same shape, found on the way: the job-title and department editors dropped the five Access Security rights and CONFIGURE_SYSTEM entirely (carved out of General into groups neither editor rendered), and the member tab titled its groups with the raw enum name — "POS" here, "Commerce" three screens away. Every editor now builds its groups through permissionGroupTree, so none of them can hold a different taxonomy again. THE UI. Four screens rendered ~280 rights as a stack of accordions — one column about nine screens tall, where finding "can this role refund an order" meant opening accordions in turn. And the stack is flat, which the apps no longer are. So it is a console: a rail of groups, nested as the org's apps are, beside a panel showing one group. Each rail row carries a granted-against-total meter for its whole subtree, so the shape of a role is legible before anything is opened — a Support Agent four-fifths of the way through Patient Records is visible from the rail. Search spans every group with per-group hit counts; the rail is sticky, arrow-key navigable and becomes a scrollable strip below md; reduced motion is respected. One thing that had to be fixed rather than inherited: splitting commerce into child groups broke the cascade, because denying ACCESS_COMMERCE under "Lines of Business" would no longer nuke VIEW_SUBSCRIPTIONS under "Subscriptions". RightsSection takes a `context` prop now — every right in every group — so the cascade works on the whole tree while only one group renders.

  6. · SecurityService

    Seed the master-note widgets from VIEW_NOTES

    The two master-note panes shipped with no right at all — "My Master Note" and "Our Master Note" were on every member's launchpad unconditionally — and this is the deploy that puts them behind a grant. Without a seed every organisation on the platform would lose both panes on the next restart and have to go and switch them back on, which is exactly the "a new right silently REMOVES access that already worked" case NEW_RIGHT_SEEDS exists for. VIEW_NOTES is the lineage rather than "everybody", because it is the population that KEEPS the notes: GET /api/notes/master takes VIEW_NOTES now, so a member without it has nothing for the widget to show, and seeding wider would put an empty pane on the launchpad of exactly the members the change is about. ADDED seeds here, unlike the picture frame's — these two panes are already on every launchpad and already hold the member's own writing, so arriving switched off would read as the platform having deleted them. OPTIONAL and MOVABLE seed so a member can now do what they never could: take one off, or put it where they want it. REQUIRED does not seed, as for every other widget: pinning something to every member's home page immovably is a decision an organisation makes.

  7. · kamo-shared-library

    A security group per app, and the master notes as widgets

    Ten new rights, 278-287: NOTES_MASTER_MINE_WIDGET and NOTES_MASTER_OURS_WIDGET, each with the four policy children every home widget carries. Both hang off VIEW_NOTES — the right that gates the DATA — because a widget parented anywhere else is a way round the gate on what it displays. MANAGE_ORG_MASTER_NOTE (228) moves from a standalone root with no ServiceType to a child of VIEW_NOTES tagged NOTES. Its old comment argued that the organisation master note was a launchpad fixture KBService served without any notes right, so tagging it NOTES would hide the right from orgs that could still see the note. That premise is gone: GET /api/notes/master now takes VIEW_NOTES like every other read in NotesController. The rest is the same correction applied to three apps that had been filed under their parent, or under nothing at all. The permission editors used to render one flat section per top-level app, so a right tagged with a CHILD app could not be granted anywhere — which is why Service Work's five rights carried POS and why the nine calculator modes carried no ServiceType and sat in the General drawer with forty unrelated rights. The editors render the app TREE now, so: - the nine CALC_* modes take ServiceType.CALCULATOR - the twenty subscription rights and MANAGE_BILLING_PROVIDER take SUBSCRIPTION - Service Work's five take SERVICE Orders, pricing, tax and quotes deliberately stay on POS: a retail sale, a rental and a service job are all orders against the same price list, so they are the commerce spine rather than any one vertical, and an org running three verticals grants them once. No enforcement changes. getServiceType() is read only by RoleRightDTO and MemberSecurityController when they serialise a right for the editors — it decides which group a right appears in, never whether a check passes. Counts in RoleRightHierarchyTest updated in the same commit, as they must be: values 268->278, children 194->205, standalone 39->38 (MANAGE_ORG_MASTER_NOTE leaving), roots 74->73.

  8. · KlusterServices

    Swap the voicemail greeting to KamoCRM-LeaveVM

    24.9s, down from 27.9s. Uploaded alongside the previous recording rather than over it, so rolling back is a one-line change to KAMO_VOICEMAIL_GREETING 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 199274 bytes of u-law (24.91s at 8000 B/s), the global resolves to custom/kamocrm-leavevm, and a test call ran Playback to completion and went on to VoiceMail with no file-resolution errors in the log.

  9. · kamo-login

    Let a signed-in member reach the sign-in form, and back out of the workspace picker

    Two halves of the same problem: the login page was a one-way street. It validated any existing *** cookie on load and, when the session was still good, redirected straight into the workspace it belonged to. That made the form unreachable for anyone already signed in — a member of several organizations could not open a second one, because every attempt to come back here bounced them into the first, and someone signed in as the wrong account had no way to reach a form to correct it. The cookie is now left alone rather than consulted or cleared: the tab already using it keeps working, and signing in here simply issues a new session, which is what makes several organizations open at once work at all. Nothing is lost by not redirecting. `?next=` and the paid-signup checkout hand-off are both applied on the normal sign-in path, so those journeys still end where they should — they just ask who you are first. planCheckoutUrl() in particular must not be called on load, because reading it consumes the one-shot cookie it is built from; a branch that read it and then did not redirect would silently eat a buyer's plan selection. The workspace picker gains a way out, for the same reason: it is the first screen that shows which account you actually authenticated as, so signing in as the wrong one is noticed there or not at all. Escape does the same thing. Both are inert while a selection is in flight, whose token is already being spent. Backing out discards that token rather than carrying it to the next attempt — it is single-use and belongs to the authentication just abandoned. Also drops the "Checking session…" spinner and its state, which would otherwise have sat there claiming to check a session the page no longer looks at.

  10. · kamo-shared-library

    The credit note, the only correction path for a finalized invoice

    B3: Kamo owns everything before finalize() and the provider owns everything after, and every provider on the roadmap treats a finalized invoice as immutable. So an invoice that was wrong is never edited and never deleted — a credit note is raised against it and the pair is what a set of accounts and an auditor both read. Nothing here writes to the invoice. Its own number series, from DocumentType.CREDIT_NOTE and its own counter row. A credit note that consumed an invoice number would leave a gap in the invoice series that nobody can explain years later. There is no DRAFT state: a credit note is raised as a completed act, so it is numbered and stamped in the same transaction that creates it. Figures are POSITIVE — a credit note for 50 credits 50. The sign lives in what the document is, not in its numbers; storing it negative means every reader has to know which convention this table follows, and the first one that does not makes an invoice total go up when a customer is refunded. sumCreditedForInvoice COALESCEs to zero: a null arriving where a number was expected is how a cap silently becomes no cap. Observed failures, verbatim. RED (before the entities existed), from CommerceInvoiceServiceTest: **************** cannot find symbol **************** cannot find symbol (CommerceCreditNote, CommerceCreditNoteRepository) GREEN: Tests run: 81, Failures: 0, Errors: 0, Skipped: 0 in InvoicingService, plus SharedLibBeanSafetyTest 3/3 green in an isolated export — the new package carries no stereotype at all.

  11. · kamo-shared-library

    The document counter, and a number that cannot fork

    Every existing document number on this platform is a check-then-insert race with no unique constraint behind it: quote numbers are COUNT(...)+1, subscription invoice numbers load every row for the org into memory and take .size()+1, loan numbers pick a random value and fail open by returning null. Two concurrent writers read the same figure and both write a number one higher. An invoice number cannot be corrected after the fact — the customer already has the PDF — so this series is minted under a row lock instead. A table and not a sequence: there is no CREATE SEQUENCE anywhere on this platform, and declaring a @SequenceGenerator would make one exportable to KI's ddl-auto: update, which would create it START WITH 1 and hand out ids below 2^53 into a unique_rowid() schema. A sequence also cannot reset per year or per tenant, which is the shape an invoice series has. findForUpdate is the only read on the repository — it extends Repository, not JpaRepository, so there is no unlocked findById to reach for by accident. insertIfAbsent uses ON CONFLICT DO NOTHING rather than SuppressionService's catch-the-violation idiom. That idiom owns its whole transaction; this one runs inside the caller's, and a unique violation aborts a PostgreSQL/Yugabyte transaction outright, so the catch would read as handled while the invoice INSERT failed anyway. Observed failures, verbatim. RED (before the classes existed): package **************** does not exist symbol: class DocumentCounterRepository / DocumentType / DocumentNumberService MUTATION (@Lock(PESSIMISTIC_WRITE) removed from findForUpdate), after the fake was made to model a real read/write round trip and to hand each reader its own instance: **************** Expected size: 32 but was: 7 in: ["INV-2026-0007", "INV-2026-0005", "INV-2026-0006", "INV-2026-0003", "INV-2026-0004", "INV-2026-0001", "INV-2026-0002"] **************** [findForUpdate must instruct the database to lock the row] Expecting actual not to be null GREEN after revert: Tests run: 43, Failures: 0, Errors: 0, Skipped: 0 Honesty note recorded in the test's javadoc: with a bare Thread.yield() the concurrency test stayed GREEN with the lock removed, and it stayed green with read latency alone. It only bites once the fake hands each caller its own entity instance — which is what two persistence contexts actually do — and puts latency on the write. No database is involved in these tests; what they prove is that the service routes its read through a finder the database is told to lock. The database half is pinned by the annotation assertion and by UX_DOCCOUNTER_ORG_TYPE_YEAR.

  12. · kamo-shared-library

    The invoice entity, with every tax field and no tax engine

    Kamo's own invoice, written only by InvoicingService. Nothing on it is supplied by a caller: every figure is derived from the lines on every write, which is the opposite of **************** where subtotal/tax/total are taken verbatim off the request and a total is derived only when two of the three are null — so a caller can post a populated order with total = 0. UX_INVOICE_ORG_NUMBER is the BACKSTOP, not the mechanism. The row lock in DocumentNumberService is the mechanism; this exists so that a future bug which bypasses the lock fails as a loud constraint violation rather than two customers holding the same invoice number. INVOICE_NUMBER is nullable because a DRAFT has none, and Postgres treats NULLs as distinct so drafts coexist under it. The four classification enums (SERVICE_LINE_TYPE / PROPERTY_CLASS / WORK_CLASS / CONTRACT_FORM) are carried even though nothing computes tax — B5: Kamo stores every tax field and computes none. They are NULL on every ServiceLineItem in production because the one writer, **************** sets only base-class fields. TAX_CLASS_ID is a UUID and never a Long: Offering.taxClassId is a Long against TaxClass's UUID key, a structurally impossible reference and a large part of why the tax tables have never been readable. Currency is on the header and never on a line; ServiceJob has money columns and no currency at all, so a job-derived invoice takes it from the org. AMOUNT_PAID / AMOUNT_DUE / PARTIALLY_PAID ship as SHAPE ONLY. Deposits and progress billing are deliberately out of BL2's scope: nothing in commerce models a deposit, milestone or retainage, and there is no partial-payment writer. There is no @PreUpdate immutability guard on a line, and that is deliberate: a callback fires at FLUSH, so the recompute finalize legitimately performs would be rejected by a guard that reads the parent's status at commit time. The guard lives in the service; the enforcement that survives bulk JPQL is the REVOKE. The repository extends Repository, not JpaRepository — a finalized invoice is a money event and the correction path is a credit note, not a delete. Observed failures, verbatim. RED (before the entities existed): InvoiceTotalsTest.java:[3,50] cannot find symbol CommerceInvoiceTest.java:[3,50] cannot find symbol (package **************** GREEN: Tests run: 62, Failures: 0, Errors: 0, Skipped: 0 — including the Spring context test, which builds the SessionFactory and so proves the BILL_TO_ prefix on EmbAddress does not collide.

  13. · kamo-internal

    Choose the KamoDesktop resolution, and stop hiding why a launch failed

    The remote desktop tracks the browser window and always has, so there was no way to have MORE desktop than window. A picker now sets a fixed size, stored per browser because it describes the screen you are sitting at — the same person on a laptop and a 4K monitor wants different answers. Read inside openKamoDesktop rather than at each call site, so all four launch points agree: the launchpad tile, the nav icon, the command palette and the platform settings buttons. "Fit my window" is the default and sends nothing at all, so anyone who never touches the picker sees byte-identical behaviour. The control is deliberately OUTSIDE the god check next to it. The two shared Windows/Master buttons are operator tooling and stay gated; the size of your own screen is not a privilege. Also fixes something found while reading the proxy: it caught every upstream failure and replaced it with "SSO link generation failed". SecurityService answers refusals with a sentence the person can act on — "You do not have a dev machine…", "The Linux dev machine is not responding, it is being restarted…" — and throwing all of them away is how a broken desktop becomes an unexplained one, which is the exact failure that endpoint was written to end. The upstream reason is now passed through.

  14. · SecurityService

    Let the caller choose a fixed KamoDesktop resolution

    The remote desktop was never locked to a resolution — it tracks the browser window, and always has. guacd honours the client's size and xorgxrdp resizes the X screen live; the session log shows it moving between 1920x1014, 1080x711 and 1280x676 as the window changed. Guacamole also already multiplies by devicePixelRatio, so a HiDPI screen gets its real pixel count. What was missing is any way to ask for MORE desktop than the window has. The SSO endpoint now takes optional width and height and passes them to the connection. resize-method is turned OFF when a size is given, and that is the whole trick: leave it on and the browser sends its own size on the next window resize, guacd obeys, and the chosen resolution is silently undone — the picker would look broken for reasons nobody could see. Without it the size holds and Guacamole scales the display to fit, with its zoom control for 1:1 and scrolling. A test pins that, because it is the one detail that makes this work. Bounds are clamped server-side rather than trusted: the caller is a browser, the token is signed, and the framebuffer is allocated on a VM everyone shares, so an unbounded value is a resource decision, not a formatting one. Width and height must be given together; half a size is a caller bug, and filling in the other half would hand back a desktop shaped like nothing anyone asked for. Omitting both keeps the previous behaviour exactly, so nobody who has not touched the new picker sees any change at all.

  15. · InitializerService

    The REVOKEs three entity javadocs already claimed existed

    BillingWormGuard, BillingDispatch and BillingSyncIssue each cite "a REVOKE UPDATE, DELETE in the KamoInitializer migration". No such migration existed — the statements were applied by hand when BL1's and MS1's DDL landed, so the live database is correct but the state was not reproducible. Entity @PreUpdate/@PreRemove guards do not fire for bulk JPQL or native SQL, so without these the append-only guarantee on a money event and on the record that a consumer texted STOP is convention rather than enforcement. Also carries the two partial indexes, which @Index cannot express.

  16. · kamo-internal

    The billing proxy route, and MANAGE_BILLING_PROVIDER's four TS mirrors

    Task 8, the kamo-internal half. Six mirrors of the rights architecture; four of them live here. 3. app/lib/rightsHierarchy.ts MANAGE_BILLING_PROVIDER -> MANAGE_SUBSCRIPTION_SETTINGS 4. app/types/.../RoleRightType.ts the constant (277) and the ALL array entry 5. permissionAppSections.ts NO functional change needed — see below 6. SecurityRoleManager.tsx its inline duplicate of 5, kept identical Mirrors 5 and 6 turned out to need no code: they map ServiceType -> section, not right -> section, and the POS/"Commerce" row already exists. Because MANAGE_BILLING_PROVIDER carries ServiceType.POS it is grantable the moment the enum ships. Both comments were updated to name it, and updated IDENTICALLY, because permissionAppSections.test.ts pins the duplication rather than the fix — SecurityRoleManager.tsx does not import that file, and two copies that disagree is how one editor grows a section the others lack. THE ROUTE PREFIX. The plan specified the relay at /api/security/billing/provider and the route file at **************** Those do not match, so shipped as written the whole feature would 404 in the browser while every service was healthy and every build was green — the exact failure the plan itself warns has happened twice on this platform. The route ships at **************** instead, following the payroll precedent, and covers every future billing relay with no new file. billingRelayRoutePrefix.test.ts now reads the Java @RequestMapping and checks a catch-all actually covers it, so the mismatch cannot come back as a code review someone forgets to do. The route does NOT call .json() on a body-less response — a 204 from disconnect would otherwise throw and surface as a generic 500, making a successful call look broken. Pinned by the second test in that file. RED (test edited first, before any mirror was touched): FAIL rightsHierarchyParity > every edge matches RoleRightType.java exactly AssertionError: expected { onlyInTs: [], …(1) } to deeply equal { onlyInTs: [], onlyInJava: [] } FAIL rightsHierarchyParity > has the same number of children as Java expected [ 'CREATE_CONTACTS', …(192) ] to have a length of 194 but got 193 FAIL rightsHierarchyParity > matches the audited totals expected [ 'CREATE_CONTACTS', …(192) ] to have a length of 194 but got 193 FAIL rightsHierarchyParity > parents the billing provider right on commerce settings AssertionError: expected [] to deeply equal [ Array(2) ] - [ "MANAGE_SUBSCRIPTION_SETTINGS", "ACCESS_COMMERCE" ] + [] Tests 4 failed | 12 passed (16) RED for the route, with the directory named EXACTLY as the plan specifies **************** FAIL billingRelayRoutePrefix > has a catch-all route covering the relay mapping AssertionError: no catch-all route file covers /api/security/billing/provider — every endpoint under it 404s in the browser: expected false to be true FAIL billingRelayRoutePrefix > does not call .json() on a body-less response Error: ENOENT: no such file or directory, open **************** Tests 2 failed (2) GREEN, both files: Tests 22 passed (22) Proven able to fail, by mutation, observed verbatim: C. RIGHT_PARENT entry for MANAGE_BILLING_PROVIDER removed (TS drifts from Java, which is the whole reason the parity test reads the Java source): 4 failed | 12 passed — including "every edge matches RoleRightType.java exactly" with a non-empty onlyInJava D. The 204 guard replaced with an unconditional `NextResponse.json(await response.json())`: FAIL billingRelayRoutePrefix > does not call .json() on a body-less response AssertionError: expected 'import { NextRequest, NextResponse } …' to contain 'if (!text) return new NextResponse(nu…' Both reverted. VERIFICATION npm test — all 12 guard scripts pass, then Test Files 286 passed (286) Tests 3750 passed (3750) npx tsc --noEmit over the WHOLE repo — one error, and it is not mine: **************** error TS2552: Cannot find name 'useNavCounts' That file is ` M` in another session's working tree (+55 lines vs origin/main) and is untouched here. To prove these changes are themselves clean, tsc was re-run over a tree built from origin/main plus ONLY these seven files: zero errors except `Cannot find module '@/messages/en.json'`, which is an artifact of `messages/` being gitignored and so absent from a git-archive tree. Committed with `git commit --only` on explicit paths. This checkout carries dozens of other sessions' modified and STAGED files; `git show --stat HEAD` lists exactly these seven.

  17. · SecurityService

    The relay to InvoicingService, and the right that gates it

    Task 8. SecurityService is the authorization boundary; InvoicingService is ClusterIP with no ingress, so this relay is the only way in. InvoicingServiceClient is TimecardServiceClient renamed. Timeouts are explicit and non-negotiable — connect 5s, read 95s: a bare RestTemplate has NONE, and this service issues every session on the platform from the same thread pool, so one wedged downstream would take authentication down with it. The read budget is generous because a connection test makes a real outbound call to a payment processor. Downstream status and body pass through VERBATIM; a 502 carrying Stripe's own sentence is a thing a person needs to read. BillingProviderRelayController is mapped at /api/security/billing/provider, two tiers: MANAGE_BILLING_PROVIDER (277) save, test, disconnect MANAGE_SUBSCRIPTION_SETTINGS (103) catalog, matrix, get, health, runs The connection TEST takes the WRITE right, not the read one, deliberately: a test is an outbound call made with stored credentials, and letting anyone who can view the page trigger it hands them a way to probe the org's payment processor. The catalog is gated rather than open because it enumerates which billing systems the platform integrates with. SecurityService runs anyRequest().permitAll() with hand-rolled per-handler auth, so a handler that forgets its guard is not weakly controlled — it is uncontrolled and reachable from the public internet. Every handler here resolves orgId/memberId from the SESSION and refuses before relaying. PREFIX CORRECTION. The plan specified the controller at /api/security/billing/provider AND the kamo-internal route file at **************** Those do not match, and a relay prefix with no covering catch-all 404s in the browser while every service is healthy and every build is green — the exact failure the plan itself warns has happened twice on this platform. The route ships as **************** instead, following the payroll precedent (route at .../payroll/, controller at **************** and kamo-internal now carries a test that reads this file's @RequestMapping and fails if no route file covers it. mvn test: Tests run: 1697, Failures: 0, Errors: 0, Skipped: 1 — BUILD SUCCESS Run in an ISOLATED tree: git archive of origin/main plus these two files, its own target/ and its own -Dmaven.repo.local. This checkout is shared and currently carries dozens of other sessions' modified and STAGED files; building in place would have shared target/ and produced phantom errors, and its result would not have been about my change. Nothing of theirs was touched (`git commit --only` on these two paths; `git show --stat HEAD` lists exactly two files). Compiled against a shared-library jar built into a PRIVATE maven repo, never `mvn install`. Note that SecurityService@origin/main does NOT compile against kamo-shared-library@origin/main at all — it needs **************** and **************** which exist only in another session's UNCOMMITTED shared-lib working tree. That is pre-existing and unrelated to this change, but it is the reason the jar used here was built from the shared-lib worktree rather than from its origin/main.

  18. · kamo-shared-library

    MANAGE_BILLING_PROVIDER (277), a child of commerce settings

    Task 8, mirror 1 of 6 (the Java enum) and mirror 2 (the parent(...) line). Id 277 was re-derived from origin/main immediately before allocating it, NOT taken from the plan: git show **************** | grep -oE '\(\s*[0-9]+\s*,' ... count: 267 max: 276 missing up to max: [1, 2, 19, 64, 72, 73, 74, 75, 76] Every gap below the maximum is one of the nine already-burned ids, so 277 is the first free one. MANAGE_PRICING(276) is the previous last constant. This matters more than it looks: RoleRightsSyncService seeds at boot and its Collectors.toMap throws on a duplicate id, which stops EVERY service booting. Parent is MANAGE_SUBSCRIPTION_SETTINGS, mirroring MANAGE_PAYROLL_PROVIDER under MANAGE_HRS_SETTINGS. Deciding WHERE an organization's invoices and payments are transmitted is a commerce-settings act. Not a child of an invoice VIEW right — anybody who can read invoices must not be able to redirect them. Not a root — a role that loses commerce settings would keep the redirect grant live. ServiceType.POS because the permission editors filter with right.serviceType === section.serviceType and there is exactly one commerce section. A right whose serviceType matches no section cannot be granted at all, and nothing says so. Counts move by exactly two, as the shape predicts: total 267 -> 268 and children 193 -> 194. Neither root bucket moves, because MANAGE_SUBSCRIPTION_SETTINGS is itself a child of ACCESS_COMMERCE and so never appears in getRoots(). RED (test edited first — counts bumped and the placement case added, before the enum constant existed): [ERROR] **************** cannot find symbol symbol: variable MANAGE_BILLING_PROVIDER location: class **************** [ERROR] **************** cannot find symbol [ERROR] **************** cannot find symbol GREEN: Tests run: 13, Failures: 0, Errors: 0 (RoleRightHierarchyTest) Tests run: 17, Failures: 0, Errors: 0 (with RoleRightTypeHrTabRightsTest) Full library suite: Tests run: 2289, Failures: 0, Errors: 0, Skipped: 0 Both tests proven able to fail, by mutation, observed verbatim: A. **************** ...) -> **************** ...): **************** expected: MANAGE_SUBSCRIPTION_SETTINGS but was: VIEW_SUBSCRIPTION_INVOICES B. parent(...) line deleted entirely — three tests red at once, which is the whole point of pinning counts as well as placement: **************** expected: MANAGE_SUBSCRIPTION_SETTINGS but was: null **************** expected: 194L but was: 193L rootsAreASensibleSubset Expected size: 74 but was: 75 Both reverted. Built and tested in an ISOLATED tree (git archive of origin/main + these two files, own target/, own -Dmaven.repo.local). This checkout is shared with other sessions and currently carries ~7 dirty files that are not mine; a red suite run here would have been someone else's TDD red phase, not a regression. Those files were left untouched.

  19. · kamo-internal

    `?bug=<id>` opens that report's drawer

    Where the new status-change email lands. It has to name the report rather than the tab: the status filter defaults to New, and that email is by definition about a report that has just stopped being New — so a link to the bare tab would put an empty list in front of somebody who was just told their report moved. The drawer already fetches the row by id, so this works whatever the current filters happen to show. The param is cleared the same way `?report=1` clears itself, so a refresh does not reopen a panel somebody deliberately closed.

  20. · kamo-internal

    Glimmering unread cards, a live badge, and per-status counts

    THE BADGE IS ABOUT YOU NOW, NOT ABOUT THE QUEUE. It counted every NEW report platform-wide, so it only ever fell when somebody moved a status — reading the queue did nothing to it. It counts what is still untriaged AND unread by you. ONE PREDICATE, THREE SURFACES. isNewToViewer() in systemBugCounts.ts is status === 'NEW' && !viewed, and the nav badge, the card's glow and the "Viewed" tick are all drawn from it — MediaService computes the number from the same two facts. A badge saying 3 above five glowing cards is a bug report, not a queue. Status is checked as well as viewed on purpose: reading is not triage, so a report a colleague has already picked up stops glowing at everybody, including the people who never opened it. LIVE OVER THE SHARED CHAT STOMP CLIENT, NOT A NEW SOCKET. NavCountsContext subscribes to /topic/system-bugs/changed, whose frame deliberately carries NO count — see systemBugCounts.ts for why this one topic is a contentless broadcast rather than the per-member subject the legal and training badges use — so the handler re-seeds and every session gets its own number back from an authenticated request. The subscription is gated on canManage from that seed, so the great majority of sessions, whose count is zero by construction, never subscribe at all. The seed on mount, on reconnect and the 60s tick stay load-bearing: the subject is core NATS with no stream. THE GLIMMER animates box-shadow and nothing else. opacity on a Paper fades the title along with the glow, and transform: scale silently defeats the hover lift, because a running animation's transform beats a rule's. The sweep is a separate ::after at a deliberately different period so the two drift rather than blink in lockstep, its white stop is kept under 60% so the title stays readable, and it is pointerEvents: none — without that it would eat clicks for a third of every cycle and present as a card that "sometimes doesn't open". Under prefers-reduced-motion both animations stop and the static ring remains, so the card still says what it was saying. MARKING READ LIVES IN THE DRAWER, not the card's onClick: every route into a report ends there — a card, a ?bug= deep link out of a status email, whatever comes next — and marking on the click would have covered only the first. Somebody arriving from an email would read the report and watch it keep glowing. The click does an optimistic local flip so the glimmer stops instantly; the drawer owns the write, and only after the detail actually loads, so a 404 never records that somebody read a report they were never shown. The status filter now carries a count per status, from a request that applies every filter EXCEPT the status — the number beside an entry is a promise about what picking it will show. Needs the MediaService and shared-library changes, and KamoInitializer's system_bug_view migration, to have landed first.

  21. · SecurityService

    Tell the reporter when Send To AI moves their report

    MediaService now emails a reporter whenever their bug or enhancement changes status, but a dispatch applies a status of its own — usually In Progress — through **************** in this JVM, without ever touching MediaService. Left as it was, the one transition that says somebody has actually started work would be the one the reporter never heard about, and a notification stream with a hole in it is one people learn not to trust. Both sides build their variables through com.kamo.z.shared.support. SystemBugStatusEmail, so the two letters are the same letter. The previous status is captured before the dispatch runs: by the time audit.complete has committed in its own REQUIRES_NEW transaction, the row no longer remembers where it came from. The notice goes out whether or not the hand-off to the dev machine confirmed, for the same reason the status is applied either way — the row now says what it says and the reporter can see it, so an email that only matches the screen on the happy path is worse than none. Read and send are separate beans. A @Transactional method invoked on `this` goes around Spring's proxy and runs with no transaction at all, which with no open-in-view means the LAZY reporter.getUser() throws; crossing a bean boundary is what makes the annotation apply. The send then happens outside the transaction so a network round trip never holds a database connection. Every failure path is never be turned into a 500 by an email.

  22. · EmailService

    Preview variables for the bug-status-changed template

    The admin preview renders a template against a sample variable map, and this one has no arm — so an org administrator opening SYSTEM_BUG_STATUS_CHANGED would see a letter with holes where the button, the callout and every status colour go. The real values come from **************** which pre-renders the conditional blocks because TemplateRenderer cannot branch. The preview therefore has to carry finished HTML for them rather than plain strings.

  23. · MediaService

    Email the reporter when their bug or enhancement changes status

    Triage was silent. Somebody filed a report and then had to keep opening the tab to find out whether anyone had looked at it — which is the same as not being told. applyStatus now reads the status the row held BEFORE the write and hands both halves to the notifier, because the reporter's letter is about the move: told only where it landed, they cannot tell an advance from a reversal. Everything the letter needs is resolved while the transaction is still open — the reporter's address, name and timezone, and the org's domain all hang off LAZY associations — and the send happens after it commits. The two halves cannot be swapped: holding a transaction open across an HTTP round trip pins a database connection for its duration, and on this database a read after a write inside one transaction can abort it outright. Nothing in the after-commit path writes to the database, which is what keeps it clear of the trap where a @Transactional method called from afterCommit silently discards everything it writes. No email for a status set to the value it already held, for a report with no reporter, or when the reporter IS the person who moved it — a platform administrator can file a report and then triage it, and telling them about their own click is how a notification stream teaches people to ignore it. An off-boarded member is not written to either, through the same predicate the missed-message notices use. A failed send is logged and dropped rather than retried. Unlike a missed chat message there is no ledger behind this, because the report itself is the durable record: the status is on screen the moment the reporter looks.

  24. · MediaService

    Per-viewer read state, a live badge, and per-status counts

    THE BADGE WAS ABOUT THE QUEUE, NOT ABOUT YOU. /new-count counted every NEW report platform-wide, so it could only fall when somebody moved a status — reading the queue did nothing to it and there was no way to work through it. It now counts what is still untriaged AND unread by the caller. One predicate, three surfaces: a report is new to you when its status is NEW and you have no system_bug_view row for it. countByStatusUnviewedBy answers the badge, SystemBugDTO.viewed dresses the card, and the console's isNewToViewer draws the glow from the same two facts. A badge saying 3 above five glowing cards is a bug report, not a queue. Reading is deliberately NOT triage. Opening a report marks it read for you and leaves it NEW for everybody else — a shared flag would let the first reader clear it off the whole team's list. LIVE, over a CONTENTLESS BROADCAST. The legal and training badges each own a per-member NATS subject carrying their number; that shape does not fit here, because filing a report changes the count for every MANAGE_SYSTEM_BUGS holder at once and PlatformRightsResolver answers "does this member hold it", never "who holds it". So system.bugs.changed says only that the queue moved and each console re-reads its own count over the authenticated seed. Because the payload holds no number, no title and no id, it needed no new guarded prefix in StompDestinationAuthz — every entry there exists to stop one member reading another's per-member topic, and there is no such thing on this one. Every announce is AFTER the @Transactional method returned, never inside it: announcing from within would tell every console to re-read a row a rollback then took away, and the re-read would land on the old number with nothing to correct it. POST /{id}/viewed is idempotent — the console fires it on every open, from every tab — and the unique index on (bug_id, member_id) is what enforces that, not the pre-check, which two tabs race straight past. GET /status-counts feeds the numbers beside each entry of the status filter. Every predicate of the listing is applied except the status one, because the number next to an entry is a promise about what picking it will show. Needs KamoInitializerService's system_bug_view migration to have run first.

  25. · SecurityService

    Announce the queue change when a report is dispatched to AI

    A dispatch is the only thing that takes a report out of NEW from OUTSIDE MediaService, so without this it was the one transition the console's badge learned about only on its next sixty-second tick — filing, triage and voiding all already announce. The subject is deliberately MediaService's own (system.bugs.changed) rather than this service's security.* convention: MediaService owns both ends of that badge, and its SystemBugStompRelayController is what relays it. A second subject would mean a second relay for the same fact. Carries no state, and must not — the frame is broadcast to every session, so the count comes from each console's own authenticated re-read rather than the wire.

  26. · kamo-shared-library

    Per-viewer read state for system bug reports

    SystemBugView is per VIEWER, not per bug — a firstViewedAt column on SystemBug would let whoever opened a report first clear it off every other manager's badge. memberId is a raw Long rather than a @ManyToOne: every query is "for this one member id", and an association would load a Member per row for a question that never looks at one. SystemBugRepository gains two queries: countByStatusUnviewedBy — the nav badge's number. Deliberately two-sided: status alone could only fall when somebody triaged, and read-state alone would leave a bug that is actively being worked still glowing. NOT EXISTS rather than a LEFT JOIN ... IS NULL, so the planner can stop at the first match. countGroupedByStatus — the counts beside each entry of the status filter. Every predicate of findVisible is repeated verbatim except the status one, because the number next to an entry is a promise about what picking it will show. COUNT DISTINCT for the same reason findVisible spells out its count query: the LEFT JOIN to sites yields a row per site a bug names. SystemBugDTO.viewed is about the CALLER, unlike every other field on the record — two members looking at the same report are told different things on purpose. No version bump: the estate stays on one kamo-shared-library version. Needs KamoInitializerService's system_bug_view migration to have run first.

  27. · kamo-shared-library

    The canonical letter a bug reporter gets when their report moves

    A member who files a bug or an enhancement had no way to learn what happened to it short of opening the tab and looking. This adds the template that tells them, and the one builder both senders use. SystemBugStatusEmail is deliberately static and free of Spring, because two services move a status: MediaService when an operator picks one in the triage drawer, SecurityService when Send To AI applies one as part of dispatching. A reporter told about one kind of move and not the other learns not to trust the emails at all, so the two have to produce the same letter — and the only way to guarantee that is for them to call the same function. The letter names both halves of the transition, not just where the report landed: told only the new status, a reporter cannot tell an advance from a reversal. It also names who moved it, stamps the time in the reporter's own timezone (a bare server LocalDateTime is UTC with no offset, so rendering it as-is is an hour that is wrong for almost everybody), and carries a per-status sentence that differs for a defect and for a request — "won't fix" on an idea answers a question nobody asked, which is why the enum's own label was renamed to "Won't Fix / Denied". TemplateRenderer cannot branch and does not escape, so everything conditional is pre-rendered here: the status colours (the same ramp the console draws its chips with), the Awaiting Info callout, and the call-to-action, which collapses cleanly rather than shipping a button pointing at "https://internal./…" when an org has no resolvable domain. Values a person typed ship twice — tag-stripped for the subject, HTML-escaped for the body — the convention ChatEmailNoticeService set. The deep link names the report rather than the tab on purpose. The tab's status filter defaults to New and this email is by definition about a report that has just stopped being New, so a link to the bare tab would put an empty list in front of somebody who was told their report moved.

  28. · InitializerService

    System_bug_view — who has read which report

    Per VIEWER, not per bug, and that is the whole reason it is a table rather than a first_viewed_at column on system_bug: triage is worked by several people at once, and a column would let whoever opened a report first clear it off everybody else's badge. Deliberately not a status either. "Somebody has read this" and "somebody has triaged this" are different facts — folding reading into status_name would move a report out of NEW because a passer-by opened it, with no way back. The two axes cross: a report is new to you when its status is NEW AND there is no row here for you. The unique index on (bug_id, member_id) is load-bearing rather than tidiness: opening a card fires the mark every time, from every tab, so the index is what makes the write idempotent instead of unbounded. Lands before MediaService needs it — this service has no CI, so the DDL has to be run here first or the reads 500 on a relation that does not exist.

  29. · BillingService

    Per-server lines on the extensions summary

    The phone server's extensions tab showed the organization-wide figure and nothing else, so an empty KamoPBX server displayed "21 billable extensions — $63.00 / month" — the 21 being on a FreePBX server elsewhere in the same org. /api/billing/extensions/summary now carries an `instances` list: one line per live server with its class, its billable count, its free count, the rate and its own monthly total. The organization totals are unchanged and still returned. Built from the INSTANCE list, not the breakdown. A server with no extensions contributes nothing to a breakdown and would otherwise be absent from its own screen — which is precisely the case that started this. Free counts come from the per-instance map added in kamo-shared-library a5b131d1, and the class rules are what make the two provider kinds read differently: a hosted server charges every active extension whether anyone is assigned or not, so its free count is always zero; a third-party server charges only assigned ones and reports the queues, IVRs and spares as free. Shared library pushed first — consumers clone its main at build time, so a service commit needing a new method is only buildable once that method is on the default branch. 256 tests pass.

  30. · VOIPService

    Keyword interception at step 0 on the RingCentral rail (MS1 Task 4)

    Today a STOP to a number no MEMBER_VOIP_CONFIG row owns hits the "No member found for to-number" branch and vanishes — and that branch is EXACTLY where an opted-out consumer's number ends up, because a campaign sends from a campaign number and nothing writes campaign numbers into member configs. Step 0 sits after the externalMessageId dedupe (so a RingCentral retry does not reprocess — Decision M1) and before the member and instance lookups' early returns (so an unowned number is not dropped). A keyword returns without storing a VoipMessage: "STOP" sitting unanswered in a member's inbox is not a customer message. Only the member-config READ moved above step 0, so no second query is issued. The `return` it guards stayed below the interceptor, which is the whole point. RESOLVING THE ORG ABOVE THE POINT IT IS RESOLVED — the plan flagged this unsolved. Task 2's org-from-to-number resolver is NOT reused. It scans **************** which is the wrong inventory for this rail: a RingCentral number lives on **************** or **************** and will not appear in a bulk-text carrier row. Reusing it verbatim would search a table that cannot hold the answer and report "unattributable" for every RingCentral STOP — indistinguishable from the real number-inventory gap, so a plain wiring bug would hide inside a known limitation. The approach is reused, the table is not. Its author's "unreliable" judgement carries over for the same underlying reason: there is no number inventory. The only ownership records are two nullable, unindexed columns, and an org receiving on several numbers through one instance is unrepresentable. This is a scan of a small table, not a lookup. Order: member config (so an attributable message is attributed identically whether or not it is a keyword) → active VoipProviderInstance matched on **************** WHEN THE ORG CANNOT BE RESOLVED, plainly: SmsKeywordService writes the SmsInboundReceipt with a NULL org and writes NOTHING else. No ConsentRecord, no AgentContactState. So the STOP is recorded as evidence that one arrived, with the number logged at WARN, and the consumer IS STILL SENT MESSAGES BY THAT TENANT. That is strictly better than today's silence and it is not compliance. It closes when ADD5/MS3 builds the per-number inventory. Two orgs claiming one number resolves to NEITHER (logged at ERROR). Attributing a revocation to the wrong tenant is the failure C1 forbids and is worse than not recording it, because it also stops a tenant who was never told to stop. TDD — observed failures, verbatim. Red before implementing: [ERROR] Tests run: 8, Failures: 0, Errors: 8 <<< FAILURE! [ERROR] **************** » IllegalArgument Could not find field 'keywordService' of type [null] on target object **************** or target class [class **************** Green after: 8/8; full suite 322/322. Mutation — moved the interceptor BELOW the member lookup's early return, i.e. back to the placement that makes it useless: [ERROR] Tests run: 8, Failures: 5, Errors: 0 <<< FAILURE! Wanted but not invoked: Actually, there were zero interactions with this mock. [ERROR] **************** Reverted; 322/322 after revert. DDL HAS NOT LANDED — see the Task 3 commits. Built against a shared-lib jar in a PRIVATE -Dmaven.repo.local; ~/.m2's 1.6.0 jar verified byte-identical throughout (md5 ****************

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