Changelog
What's new.
Every improvement to the platform, as it ships.
· kamo-shared-library
The eight IoT notification kinds, appended (IoT SP00)
· kamo-shared-library
IotRights, the one owner rule every IoT gate applies (IoT SP00)
· kamo-shared-library
An SI member never holds the six IoT management rights (IoT SP00-D4)
· kamo-shared-library
The IoT Devices rights (370-401) under VIEW_IOT_DEVICES, and SHOW_TOUR_IOT (IoT SP00)
· kamo-shared-library
IoT Devices app (40), inside the PHI boundary and never automatic (IoT SP00)
· KlusterServices
Api.* carve-out for vendor hooks and the Alexa relay with a webhook-sized limit; IoT secrets documented (IoT SP00)
· KlusterServices
Default-deny egress for kamowsiot and its disruption budget (IoT SP00)
· KlusterServices
NATS login svc_iot, svc_media iot.live.> grant, IOT_EVENTS work queue (IoT SP00)
· kamo-shared-library
The letter a triggered sign-in detection rule sends
SUSPICIOUS_SIGNIN_DETECTED joins the canonical templates. SecurityService's SuspiciousDetectionService sends it when a tenant-configured detection rule (failed-login threshold, geo anomaly, rapid IP switching, off-hours access, concurrent sessions) triggers with NOTIFY_ADMIN_EMAIL among its responses; until now that response only wrote a log line. Every attacker-influenced value uses its escaped twin (TemplateRenderer does no escaping). Carries the rule, the outcome, the account id, the address, the country and the responses applied; never credential material. SuspiciousSigninTemplateTest pins registration, seeding and the variable contract. EmailService seeds it once rebuilt onto this library.
· kamo-internal
Branch Types moves to Platform Administration
Branch Types (the security models child organizations are created under, with their default security roles, apps and home screens) was a tab on every organization's own Settings -> Account. The owner wants only the platform organization to set them, so it is now Platform Administration -> Configuration -> Branch Types, opened by the new MANAGE_BRANCH_TYPES platform right. SecurityService enforces the same right on the endpoints behind it (securityservice cc6639ef). - platformRights: the right, its tab slug and its colour; rightsVisuals gets its icon. - The tab, its menu link and its landing card now sit with the other platform tabs. The account page loses the tab, and the branch-label loader that existed only to title it. - **************** now redirects to the new tab, so old links and bookmarks still land somewhere. - SecurityRoleManager stops loading every security model when the subdomain dialog opens. Nothing rendered the result, and it would now be a 403 for every tenant. Strings: kamo-translation-dictionary 9f084371.
· SecurityService
Branch types and security models are the platform's to edit
Branch types, the security models behind them (default security roles, app availability, home screens, commerce markets, subdomain config) and the master model used to be editable by any organization from its own Settings -> Account. The owner wants only the platform organization to set them. So these endpoints now require the new MANAGE_BRANCH_TYPES platform right: - SecurityModelController and SecurityModelMarketController, whole class - MasterModelController, whole class - BranchTypeController's /{id}/usages PlatformRightsResolver answers nothing inside a tenant, so this closes them to every organization but the platform's with no second check. BranchTypeController's list and /creatable stay open, because the org-creation wizard reads them. A tenant's existing models keep governing its child orgs; they just can't be edited from the tenant any more. Needs kamo-shared-library 0542057b (MANAGE_BRANCH_TYPES).
· kamo-shared-library
MANAGE_BRANCH_TYPES
Branch types, and the security models behind them, move from every organization's Settings -> Account to Platform Administration -> Configuration. This is the platform right that opens that tab, and the right SecurityService will enforce on the model endpoints. Appended at the end like every right before it. AiPlatformRightsTest pinned the two AI rights as "the last two", so it now pins their ordinals instead, which is what append-only actually needs.
· SecurityService
Email the accept-window audience when a new lead sits unclaimed for two minutes
LeadAvailableEmailSweep (every 15 s, singleton lease) finds leads that entered the pool 2-15 minutes ago and are still unclaimed, per org via ix_leads_org_created, and sends LEADS_AVAILABLE to everyone the accept-lead window would offer them to. A lead accepted within the two minutes has left the pool and nobody is written to. LeadAvailableEmailAudience applies the window's own rules per lead: Spend/Receive holder, active, a person (not SI, not a fake account), status AVAILABLE, accept gate clear, credit product (the window never opens for free-for-all), org credit not suspended, allotment active, credit or infinite balance, under Max Daily Spend, licensed in the property state. The letter names last name and state only. Redis keeps it to once per lead per member (written before the send: at most once) and one letter per member per two-minute quiet period; leads due while a member rests ride in their next letter, so a bulk import is one digest rather than a letter per tick. Needs shared-lib 9671cc7a and EmailService 0cdeb88 (template).
· EmailService
Preview the new-lead-available letter
Adds the LEADS_AVAILABLE arm to sampleVariables, built through the same shared-lib LeadsAvailableEmail helper SecurityService sends with, so the admin preview shows the letter that goes out. Rebuilding also picks up shared-lib 9671cc7a, which registers the template EmailService seeds and sends.
· kamo-shared-library
A letter for an offered lead nobody accepted in two minutes
LEADS_AVAILABLE goes to everyone the accept-lead window offered a new lead to when it is still unclaimed two minutes later. It names the lead by last name and state and nothing else; LeadsAvailableEmail builds the variables (escaped body, raw single-line subject, plural wording, first ten rows then "and N more") so SecurityService's sweep and EmailService's preview share one contract.
· kamo-internal
Tours start by themselves at sign-in, a tour controller steers them, and every app on the App Shelf starts its own
- Sign-in: the tours with something new now START on the launchpad, once per sign-in — no offer to click through. God mode no longer stops them (it did, for the owner), and tours are gated on the member's real rights, never god mode's "everything". - The controller (GuidedTourDock), bottom-right above everything the tour dims: a ring for progress across every queued tour, the current tour's steps as segments to jump to, Back / Skip step / Autoplay / Skip tour / End all (asked twice), a chapter list of every tour and step, and Minimise — the page comes back and the tour waits as an orb that resumes where it was. Keyboard: arrows, S, P, Esc. - Autoplay: each step moves on after a reading time (4.5-16 s by length), held while the pointer rests on the card; the card's bar and the controller's ring count it down. - The finish (GuidedTourCelebration): a burst of the organization's colours and where to retake tours, only when a tour ran to its end. - The card: animated step changes, steps as clickable dots, keeps clear of the controller (placement.ts avoid), and stacks above it as a sheet on a phone. - App Shelf: a tour button on every app row (shown with a dot until the tour is taken) and a "Take the … tour" button in each app's panel header. - Branch Types: a new branch type starts with every Show Tour right Granted for its human role types (SecurityService fefd98c2 did the same for every existing one). All run logic is pure in runModel.ts (next/back across tours, skip tour/all, jump, progress, reading time) and tested; GuidedTourHost.test.tsx drives the stage, keyboard, autoplay and controller end to end.
· SecurityService
Every Show Tour right is on by default — for every role, every branch-type template, and everyone already here
The first release seeded each tour only to roles already holding a right in its app and left every branch-type template at Not Specified: 2,416 role rows held no tour, existing members were never offered them, and every organization created from a branch type would have started with none. Owner's rule: tours are on for everybody, however long they have been a member. - seedSettingFor: a tour right arrives GRANTED on every role. - templateSeedSettingFor: GRANTED in every human branch-type template (not ARTIFICIAL — an SI member never opens a browser). - enableTourRightsOnce: ONE TIME, switches every unspecified tour right on, on every role and every human template; explicit denies are left alone. Marked done by a row under member id 0 in member_ui_preferences, so an administrator's later Not Specified sticks.
· kamo-shared-library
ExpirePendingForKey and findFirstByPublicKeyAndStatus for the enrollment fixes
KamoCameraService retires a connector's older pending codes when it hands out a new one (an old code read from a log could otherwise be claimed into another organization, leaving the owner's claim refused), and answers a status poll from the ACTIVE site holding the key. Query methods only; no schema change.
· kamo-internal
Every email address opens a Write popup, and every phone number — settings included — opens Call / Text
Email addresses, project wide. <EmailAddress> is the email twin of <PhoneNumber>; pressed, it opens one design of popup: - Write opens the email composer window addressed to them — nothing is sent until Send. - The second tile follows the records: Chat for a colleague, Call (number on the dialpad, never dialled) when a number is on file, Copy otherwise. - Who the address is in the org's own records (colleague, contact, lead — the mailbox's correspondent lookup, once, on open) with Open lead / Open contact / View profile; personal provider or company domain; the colleague's local time; a typo check ("gmial.com" → gmail.com, Write to that instead); send-only noreply@ boxes can't be written to; Copy, Use my mail app; E / C / Y keys. The lead view routes Write through its opt-out guard; a surface can pass a composeWarning (Write then takes a second press). Phone numbers, now truly everywhere — including the organization's own: Behavior & Labeling's main and toll-free numbers and contact email, member phone settings, text numbers, phone-system numbers and providers, voicemail callers, Contact Sales, the member card, the mail correspondent card, the hex head's extension, softphone CRM/contacts/history, report PHONE columns, AI calls console and consent history, quote preview (icon-less, so it prints as text), the media player's caller line, and no-name call titles on the lead timeline. Shared engine **************** ContactPopover (placement, arrow, focus trap, keys, outside press), ContactHeader and parts, ContactTrigger, and ContactPopupHost — mounted beside the tool dock, it draws the open popup away from its trigger, so one opened inside a hover card or popover survives the card closing as the pointer moves into it. One popup at a time across both kinds; a route change closes it. Triggers carry GRID_OWN_CLICK for AG Grid rows. Left plain on purpose: values inside a picker option, select, label or a row that is itself a button (selection UIs); inside a translated sentence; the live-call screen and quick-dial buttons; the SMS/mail clients' own conversation lists and address plates; email-template placeholders. The "Use my device" tel: link now carries the extension (;ext=). mailboxMemberCards counted every dialog trigger as a member card; it now skips the address triggers.
· kamo-shared-library
SHOW_TOUR_KAMO_CAMERAS (369), and an enrollment's code hash is never serialized
The Show Tour block (338-368) was written while KamoCameras (334-337) was landing, so the newest app security group had no tour right and kamo-internal's guided-tour catalog test (one tour per app group) failed against it. 369 is GRANTED and a standalone root like the other 31; counts: 355 rights, 78 standalone, 120 roots, 32 tours. 370 is next free. CameraEnrollment.codeHash gains @JsonIgnore: **************** (added to main after the camera entities landed) flags it. Nothing serializes enrollments today — the service answers with explicit maps — but a code hash is credential material. Suite: 6659 tests; the remaining failures (CharterStampTest legacy golden, AiAutonomyModeTest approval vocabulary) also fail on origin/main and are not cameras.
· kamo-internal
The Cameras guided tour (SHOW_TOUR_KAMO_CAMERAS, 369) — finding the menu, a camera, the grid, saved layouts, and the two Settings tabs
· kamo-internal
Every phone number on screen opens a Call / Text popup
Any number kamo-internal shows — a lead's, a contact's, a borrower's, a customer's, a vendor's, an applicant's, a colleague's, a voicemail caller's — is now a <PhoneNumber>. Pressed, it opens one reusable popup: - Call opens (or raises) the softphone with the number on its dialpad and stops there. Nothing is dialled until the member presses Call in the phone. - Text opens or raises the SMS thread with that number, titled with the person. - Around them: the number's country and local time (area code for US/Canada, country elsewhere) with a nudge outside 8 AM–9 PM, recent calls with the number from the history the phone already holds, live line status, a two-press confirm when the record says it is not OK to text, extension handling, copy (national and international), hand-off to the device's own dialler, C / T / Y shortcuts, focus kept in and returned on Escape, reduced-motion aware. Wired into the lead view (personal + work, beside the AI-voice consent chip), mortgage agent phone, lead communications timeline, contacts, the calendar's lead panel, account view, loan originator, commerce applications/bookings/requests/ service, pipeline applications/bookings/retail customers (grid + detail), careers applicants, team members grid, POS vendors/customers, voicemail tables and the licensing regulator line. Two softphone open traps fixed at the tool:open chokepoint (openInStore): - tool-core MERGES a re-open's props into the single softphone, so a hex head's `autoDial: true` outlived its call and would dial the next number put on the dialpad. `autoDial` is now scoped to the request that carries it. - tool-core raised a MINIMIZED softphone without the new props, so it came back with the old number. The props are now handed over. `prefillSeq` lets the same number be put on the dialpad again.
· kamo-internal
KamoCameras settings — Site Setup (enroll a connector in three steps, site cards), Cameras (grid with on/off and audio, drag-and-drop groups organiser), site detail (overview, discover, cameras, camera systems, advanced go2rtc settings with history, diagnostics), camera and group pages with a shared Security tab, menu and landing entries
Pure rules in app/lib/cameraSettings.ts with tests: enrollment code formatting and validation (SiteKeys alphabet), connector download pick per OS/arch, install commands, the managed api:/rtsp: YAML guard, access-rule set sort/diff, discovery fields and discovery-kind -> source-kind mapping, password masking for display and in logs, groups organiser moves, config apply state with versions read as numbers (Java longs arrive as strings).
· kamo-internal
The camera viewer — Cameras nav menu, the live cameras tool window and its maximized navigator
- Nav tool 'cameras' (app + VIEW_CAMERAS): saved layouts, groups (each expanding to its cameras), ungrouped cameras with a status dot, Open viewer, Manage cameras for either admin right; live via useOrgChanges('cameras'); a failed load is one retry row, never 'no cameras'; empty state explains that an administrator chooses who may watch. hasAnySettingsRight += **************** pinned-app launcher cameras -> cameras. - Tool window 'cameras' (max 6, 1000x690): WebRTC wall over one-round-trip non-trickle signalling; grids auto/1/2x2/3x3/4x4/1+5/1+7; sub stream in the grid, main for an expanded tile or the only tile of a maximized window; only the selected tile is ever heard (MUTED/UNMUTED/AUTO=focused); muted autoplay with a click-for-sound fallback; capped exponential reconnect; no session for a minimized, covered or long-hidden window, a covered tile, an offline site or a switched-off camera; site offline resumes over realtime; add/remove/drag-swap, Save layout (update / save as new) and delete; group windows follow their group; props are the view (tool:patchProps) and the title follows. - Maximized navigator kind 'cameras': CameraSwitcherColumn (layouts / groups / cameras, active row by sessionGuid, + opens a new maximized viewer), threaded through the width store, panel, tab strip, dock and tests. - Pure decisions in app/lib/cameraViewer.ts with tests; stream lifecycle tested under jsdom.
· kamo-internal
Guided onboarding tours for every app, offered at sign-in and retakeable from the App Shelf
One tour per app security group — 31, from Customer Relationships to E-Signatures — each gated on the app being on, the member being able to open it, and the app's new "Show Tour" right (SHOW_TOUR_<APP>, shared-lib 582704f2). Every STEP is gated again on the right behind what it points at, so a member is never walked past a feature they do not have. At sign-in (once per session token, on the launchpad) the member is offered the tours with something new for them: steps already seen or skipped in ANY organization are left out (SecurityService /api/security/guided-tours keeps one person's history across memberships). Start runs the chosen tours in turn; Not now asks again next sign-in; Don't offer these marks them skipped. The App Shelf offers each app's tour under the app, and a Guided Tours collection lists every tour with its status, so any of them can be taken again at will. The overlay (GuidedTourHost): navigates to each step's page, waits for its anchor, scrolls it into view, dims the rest with a spotlight and a pulsing ring, places an anchored card with an arrow on whichever side has room (a bottom sheet on a phone), keeps the page inert except on click-to-continue steps (where Next does the click), follows the element every frame, falls back to a centred card when the element never appears, and supports Back/Next/arrow keys/Escape, focus, and reduced motion. Pages gained data-tour anchors only (attribute-only edits, 66 files). Copy is in the dictionary under guidedTours.* (kamo-translation-dictionary 52b1229c), written for non-technical readers, white-label, and saying SI rather than AI — the catalog test enforces both, plus that every right, anchor, page and copy key a tour names exists. E-Signatures, Retail, Rentals and Procurement now have a security group (each holds its Show Tour); the TS rights mirror carries all 31 tour rights.
· SecurityService
Remember guided-tour steps per person across organizations, and seed every Show Tour right
GET /api/security/guided-tours answers which tour steps the signed-in PERSON has been shown or skipped, as the union over every membership of the same user — a step seen in one organization is seen in the next. POST /guided-tours/steps marks steps on the signed-in membership's guidedTours row in member_ui_preferences (no new table), read-merge-write in one transaction with @RetryOnDbConflict so two tabs cannot erase each other. Rules in GuidedTourProgress: seen outranks skipped, the first date is kept, ids are validated and bounded. Its own path rather than a ui-preferences key, so a running tour publishes under guided-tours, which nothing listens to, instead of members. RoleRightsSyncService seeds each SHOW_TOUR_<APP> on its first boot to whoever holds any right in that app, its nested apps or (for a nested app) its parent; E-Signatures, which has no right of its own, seeds from Documents. The two member-tracking rights are never a source.
· kamo-shared-library
A Show Tour right for every app security group (338-368)
SHOW_TOUR_<APP>, one per app group — thirty-one, E-Signatures included — each a standalone root filed under its app, labelled "Show Tour", and GRANTED by default so a role created from now on offers its members every tour. It decides only whether the member is walked through that app's guided tour at sign-in and offered it on the App Shelf; each tour step is separately gated on the right behind what it shows, so the right grants no data and no capability. RoleRightType.tourRightFor(app) / isTourRight() find them by name; RoleRightHierarchyTest pins that name, app and shape agree. Counts: values 323 -> 354, standalone 46 -> 77, roots 88 -> 119. Next free id is 369.
· kamo-internal
Foundation — KAMO_CAMERAS (39) and rights 334-337 mirrored, isKamoCameras, BFF proxies, camerasApi, openCameraWindow
· kamo-shared-library
A deleted meeting's guests get the organisation's cancellation letter
The invitation and the update were branded letters; the cancellation was a one-line "has been cancelled". Add calendar.meeting_canceled beside them: the same letterhead, the meeting struck through with when it was scheduled for, the METHOD:CANCEL .ics that removes it from the guest's calendar, and no join button for a room nobody will be in. Seeded with the other canonical templates, so an existing organisation gets it on first use.
· MCPGatewayService
An offered SI's send says what it was in the result's _meta["kamo.collab"] — purpose, urgency, regarding, intent source, outcome (SEND, or DEFER with deliverAfter), the colleagues reached with kind and person flag (the gate's own recipients when it judged them, else the plan's with CollabCallFacts.kindOf and the org graph), the thread and a consult's id; written before the key is DONE so a replay carries it, on delivered and deferred INTENT calls of SIs IntentOffer offers; content and structured...

Your universe is waiting.
Start free in minutes. Add products whenever you are ready — no migration, no new logins.
Free forever — no credit card required