Nenda kwenye maudhui
KAMOUniverse

Mabadiliko

Nini kipya.

Kila kuboresha jukwaa, kama ni meli.

  1. · AIService

    Point at YugabyteDB and use PostgreSQLDialect

    CockroachDB has been replaced by YugabyteDB. Connection strings move from cockroachdb-public:26257 to yb-tserver-service:5433, and the Hibernate dialect from **************** to PostgreSQLDialect.

  2. · KlusterServices

    Unique_rowid() shim and non-public schemas in YugabyteDB

    The shim is a sequence seeded at the production maximum plus a margin. The seed carries two requirements, not one: it must exceed every existing id or the first insert after cutover collides, and it must exceed 2^53 because ids above that serialize as strings and the API contracts already assume it. Verified: returns > 2^53 and > production max, 1000 calls all distinct, and `INT8 DEFAULT unique_rowid()` works verbatim as a column default -- which is the point, since it lets all 520 columnDefinition strings in the donotmodify shared library stay byte-for-byte unchanged. Also pre-creates the five non-public schemas. Hibernate does not create schemas unless hbm2ddl.create_namespaces=true, which KamoInitializerService does not set, so without these the same 34 tables would go missing again.

  3. · KlusterServices

    Unique_rowid() shim and non-public schemas in YugabyteDB

    The shim is a sequence seeded at the production maximum plus a margin. The seed carries two requirements, not one: it must exceed every existing id or the first insert after cutover collides, and it must exceed 2^53 because ids above that serialize as strings and the API contracts already assume it. Verified: returns > 2^53 and > production max, 1000 calls all distinct, and `INT8 DEFAULT unique_rowid()` works verbatim as a column default -- which is the point, since it lets all 520 columnDefinition strings in the donotmodify shared library stay byte-for-byte unchanged. Also pre-creates the five non-public schemas. Hibernate does not create schemas unless hbm2ddl.create_namespaces=true, which KamoInitializerService does not set, so without these the same 34 tables would go missing again.

  4. · KlusterServices

    Deploy single-node YugabyteDB 2026.1.0.1 alongside CockroachDB

    Both engines now run side by side on k1m1; CockroachDB is untouched and remains the rollback path. Three things that were not obvious: - The Helm chart version is not the Docker tag. Chart 2026.1.0 maps to app version 2026.1.0.0-b118, so `image: yugabytedb/yugabyte:2026.1.0` is ImagePullBackOff. Pinned to 2026.1.0.1-b1, newest patch of stable 2026.1. - hostNetwork puts the pods on k1m1's real interfaces, where two Yugabyte defaults are already taken: 9000 by MinIO and 9100 by node_exporter. The tserver webserver moves to 9010 and its RPC to 9110. Both host services verified unaffected afterwards. - TLS is not optional. All 31 consumer connection strings carry sslmode=require, so YSQL without encryption would refuse every client at cutover. Certs are created out of band, with the cockroachdb-public alias name in the SAN list because consumers keep using it through cutover. Deliberately not wired into CI: adding a deploy step edits .forgejo/, which triggers a full reconcile that reverts live drift.

  5. · KlusterServices

    Deploy single-node YugabyteDB 2026.1.0.1 alongside CockroachDB

    Both engines now run side by side on k1m1; CockroachDB is untouched and remains the rollback path. Three things that were not obvious: - The Helm chart version is not the Docker tag. Chart 2026.1.0 maps to app version 2026.1.0.0-b118, so `image: yugabytedb/yugabyte:2026.1.0` is ImagePullBackOff. Pinned to 2026.1.0.1-b1, newest patch of stable 2026.1. - hostNetwork puts the pods on k1m1's real interfaces, where two Yugabyte defaults are already taken: 9000 by MinIO and 9100 by node_exporter. The tserver webserver moves to 9010 and its RPC to 9110. Both host services verified unaffected afterwards. - TLS is not optional. All 31 consumer connection strings carry sslmode=require, so YSQL without encryption would refuse every client at cutover. Certs are created out of band, with the cockroachdb-public alias name in the SAN list because consumers keep using it through cutover. Deliberately not wired into CI: adding a deploy step edits .forgejo/, which triggers a full reconcile that reverts live drift.

  6. · kamo-internal

    Split the access log and its detection rules into sub-tabs

    The rules that decide what gets flagged were an accordion below the grid — collapsed by default, so the configuration was something you had to know was there, and expanding it fought the grid for the same pane. They are two views of one subject, and each wants the whole height. Sub-tabs now switch between them, matching the Tabs pattern the OAuth2 tab next door already uses. The rules tab only exists for a member holding MANAGE_DETECTION_RULES, and the active view is derived rather than corrected by an effect: checkRight answers false until useUserInfo settles, which would otherwise leave a member standing on a tab that then vanishes. Rules are fetched on entering the tab, as they were on expanding the accordion, and the panel carries no heading of its own now that the tab names it. Also swaps the edit button's icon: it was a Plus, under an "Edit rule" tooltip.

  7. · kamo-internal

    Back-to-home arrow on the main workspace headers

    Carries the settings-page arrow across the rest of the app: Accounts, Leads, Commerce, Marketing (all four tabs, via the shared layout), Knowledge Base, HR, Network, Document Manager, Calendar, Contacts and Notes. The button moves out of components/settings to components/BackToHomeButton now that it is app-wide chrome rather than settings chrome. Marketing and HR own one gradient hero for every tab beneath them, so the arrow lands in their layout and each tab inherits it. Commerce and Notes render their header from a single-use component (PipelineView, NotesHeader), which is where the arrow goes for those two.

  8. · kamo-internal

    Explanatory panels on every administrator bar link

    Somebody who created their organization ten minutes ago meets nine links they have no way to read. Hovering or focusing one now unfurls a panel that answers, in order: what this screen is, three things you can do there, and where to find it again through Settings once the bar is no longer new. The panel extends out of the band rather than floating near it — it grows downward from the band's edge with a caret welded to its entry, and the entry stays lit while it is out, so the two read as one object. Content settles in a short stagger behind it. Reduced motion keeps the crossfade and drops the travel; reduced transparency drops the blur. Two details earn their place. Blocks and Logs run short on the bar but the panel heads them with their real names, Access Blocks and System Access Logs, because that is what somebody will be hunting for in Settings later. And every panel closes with the trail back to the screen, so learning it once makes the shortcut optional rather than load-bearing. Opens on hover after a beat, instantly once any panel is already out, and instantly on keyboard focus; Escape closes it. A touch pointer never opens one — a tap is just the link, with no panel left stranded behind it.

  9. · kamo-internal

    Tighten the administrator bar — org label, short audit names, no gap

    Three changes to how the bar reads at nine links: The team-members entry now wears the org's own plural label from Behavior & Labeling, so an org that calls them Loan Officers gets Loan Officers here. It is the one entry with no label key — the name is the org's, not a translation. Access Blocks and System Access Logs shorten to Blocks and Logs under their own keys rather than the settings menu's. Renaming the shared keys would have renamed those tabs everywhere they appear; in this group, next to those icons, the 'access' half of each name was doing no work. The gap between entries goes away so each link's own padding is the separation and the hover fields meet edge to edge — a strip rather than a row of chips.

  10. · kamo-internal

    Back-to-home arrow left of every settings page title

    Settings pages had no way back to the launchpad short of the nav rail. SettingsView's optional backHref now defaults to home and renders unconditionally, so the 20 pages built on it pick the arrow up for free; the six pages that draw their own header get the same control inline. Nested editors (markets, products, plans, email provider, webhooks, condition templates, the agent studio) already carry a breadcrumb back to their parent and are left alone, as is Leads settings, whose arrow deliberately returns to /leads.

  11. · kamo-internal

    The rest of essential setup on the administrator bar

    Security Roles, Departments and Job Titles join the configuration group, left of the audit pair. They are the three Essential Setup tabs the bar was missing — Behavior & Labeling was carrying that topic alone, so the one screen an administrator reaches from here was the one that happens to be its first tab. Each reuses the icon and label key the settings landing already gives it, and all three are covered by the bar's existing CONFIGURE_SYSTEM gate.

  12. · kamo-internal

    Access blocks in the administrator bar, audit links to the trailing edge

    Access Blocks and System Access Logs are the two screens on the bar that answer 'who got in and who did not' rather than configuring anything, so they group together and ride the trailing edge of the band, away from the configuration links. An auto inline-start margin does it — no spacer element, and the pair simply follows the rest once the row wraps on a narrow viewport. Both are already covered by the bar's MANAGE_ACCESS_RULES gate, and Access Blocks reuses the icon and label key the settings landing already gives it.

  13. · kamo-internal

    Name and edit address books, and rebuild the page around them

    Adding a book POSTed a hard-coded placeholder name with no prompt, and nothing anywhere could rename one — so every book an org had was called the same thing, permanently. Books and groups are now created and edited through a dialog with a name, description and accent, renameable and removable from a row menu. Groups were never fetched at all: the sidebar rendered from a list that only grew when you created one, so a group vanished on reload. Contacts were cached per book and merged in the browser, so "All Contacts" showed only the books you had visited and sorting fought the cache. Both now come from the service, with the scope (book, group, query, order) driving one ordered, counted request. The rest of the page follows the console idiom the rest of the app uses: - Sidebar: books with their contact counts, groups nested with member counts, row actions for rename / add group / export / delete - List: server-side search and ordering, select-all and shift-range selection, infinite scroll, skeletons, an A–Z rail, and empty states that differ for "no contacts" and "nothing matches" - Selection bar: move, add to group, export, delete. The per-row checkboxes have been there all along with nothing to call - Detail panel: everything on file, each fact copyable and actionable, and one shared editor with the create dialog so the two cannot drift - Import reports what it skipped, so a partial import does not look like loss The contacts proxy turned every 204 into a 500 — a successful delete reported failure — because it parsed an empty body as JSON. No org-level option on a new book: listBooks returns every book in the org regardless of owner, so offering the choice would promise privacy the read path does not implement.

  14. · EmailService

    Persist and serve the whole contact, not just its name

    The REST layer serialised the JPA entities directly. A Contact has no mapping to its emails, phones, addresses, organisations, URLs, IMs or relationships — those are separate tables — so none of it was ever in a response, and Jackson silently dropped all of it on the way in. The create dialog collected seven sections and persisted none. Books and groups came back with their lazy associations rendered as null, which is why the sidebar could not attribute a group to a book. Underneath that, creating a contact could not succeed at all: CONTACT.OWNER_ID is NOT NULL and nothing set it, from the UI, from a vCard import, or from a CardDAV PUT. Import stored only the raw card and left FORMATTED_NAME null, which the column rejects; export read that same raw card back, so anything created in the UI exported empty. - **************** the wire shapes, with the counts the sidebar needs and the totals a pager needs - ContactDetailStore: batch-loads and replaces the seven satellite tables and group membership; deleting a contact no longer orphans them - ContactQueryStore: counts, an ordered/filtered/paged org-wide listing, and a search that also matches name parts, phone numbers and organisations - ContactVCardMapper: import and export go through one field mapping, so a contact survives a round trip. BDAY is written as a PartialDate — resolving it to an instant shifted the day for readers in another timezone - updateBook applies only the fields sent: renaming a book used to wipe its description and colour, and throw on the NOT NULL isVisible - deleteBook cascades; the org default is refused rather than breaking the check constraint every org-level book depends on - bulk delete/move/group endpoints — the list's checkboxes had nothing to call Book creation always sets an owner and ignores a client-supplied isOrgDefault: an ownerless book is only legal as the org default, and a second default would leave findOrgDefault choosing arbitrarily.

  15. · kamo-internal

    Administrator bar above the launchpad cards

    Five screens an org administrator opens constantly — team members, behavior & labeling, stats & costs, apps & features, system access logs — each two or three clicks deep through the settings menu today. They get one row across the top of the launchpad, below the masthead and above the cards. Each entry reuses the icon and the label key its destination already carries in the settings landing, so the same screen reads the same wherever it is offered. The band is the NavTop/NavPri org gradient with white links, which is what makes it read as chrome rather than a sixth card. Visibility is the Administrator (Full Access) role, expressed the only way the session exposes it: the three rights that role holds and no stock role below it does. All three are required, and they are exactly what the five destinations gate on, so the bar never offers a link that would refuse the member.

  16. · BillingService

    Read imaging usage from one deduplicated pass

    Follows the shared-library change to one charge per distinct file per organization. Both the summary and the daily snapshot writer take a single imaging pass and read every domain from it, rather than re-running the same grouping once per domain — eighteen domains meant the same query eighteen times for one page load.

  17. · kamo-shared-library

    Count each file once per organization, not once per upload

    Billing was per imgs record, so an organization that uploaded the same file five times was charged five times. The owner's rule is one charge per distinct file per organization: Kamo still stores the file once system-wide, each organization holding it is still counted, and re-uploading something you already have is not a new charge. Imaging figures now come from one pass per organization that collapses records to distinct files. Measured on production for KamoCRM: 52 live records become 28 chargeable files, 6,886,309,250 bytes become 5,441,174,177. A file held under two associations is counted in exactly one — the lowest-ordinal association it appears under — because counting it in both would bill it twice. Two production files are in that position, and with this rule the categories partition the organization's files and sum exactly to its total. Period figures key on when the organization FIRST held a file, so re-uploading something it already had is not a new charge in a later period. Generated previews are keyed by file for the same reason, and trashing one copy of a file held five times frees nothing. imagingUsage() serves current, lifetime and added from a single pass, because every imaging domain needs the same grouping and computing it per domain ran the query a dozen times for one summary.

  18. · kamo-internal

    Skip the upload when the platform already has the file

    Sending the same video to three people meant three full uploads. Dedup collapsed them into one stored object, but only once each transfer had finished — so the saving was storage, never the member's time or bandwidth. The composer now asks before it sends. A size probe first, because identical content must have an identical byte count: it rules out a match for almost every genuinely new file at the cost of one small request, so hashing is only paid for when it can pay off. Only then are BLAKE3 and SHA3-256 computed and offered to the server, which attaches the stored content directly. A miss falls through to a normal upload. Hashing streams. The existing helpers call file.arrayBuffer() once per hash, so a file is resident in memory twice over — fine for a document, fatal for a 3 GiB video. The new one walks the file in 4 MiB slices feeding both hashers, so peak memory is one chunk at any size, and yields between chunks because SHA3-256 is pure JavaScript and would otherwise freeze the composer for minutes. Dedup is an optimisation and never a gate: any failure in the probe, the hashing or the link falls back to uploading, which is always correct. The panel names the pause so it does not read as a stall.

  19. · MediaService

    Attach an already-stored file without uploading it again

    Forwarding a 2 GB video to three colleagues cost three full uploads. Storage deduplicated them into one object, but only after each transfer had completed and been thrown away — the member paid for every byte, three times, to produce a copy the platform already had. Two endpoints settle it before any bytes move. /attachments/probe answers whether a file of this size could exist at all, which is cheap and lets a client skip hashing for almost every genuinely new file. /attachments/link takes hashes the client computed itself and, when they resolve to stored content, creates the attachment outright; a 404 means 'never seen, upload it' and is an ordinary answer rather than a failure. The hashes are a claim, not an authorisation: they only ever resolve against content already stored within the caller's own dedup scope, and both endpoints require session membership like every other attachment route. Upload and link share one row-building path on purpose — whether the bytes travelled or were recognised, the attachment that lands in the conversation must be indistinguishable.

  20. · kamo-internal

    Show generated preview storage as its own line

    Thumbnails and preview images now count toward the organization's total, so the client should be able to see what they cost rather than find them folded into another number. A tooltip says what they are and that a multi-page document produces one preview per page, so the figure is explainable rather than mysterious.

  21. · SecurityService

    Measure the thumbnails and tracks generated from each file

    Object keys are content-addressed as **************** so the segment before the first underscore identifies the file a derivative came from. Sizes are summed per ImgDat — one physical set per deduplicated file — and billing attributes them through each imgs record pointing at it. This is the only way to answer the question. img_dats.THUMB_FILE_SIZE holds a single value for what may be dozens of page thumbnails, was populated for 5 of 84 rows in production, and never covered track objects at all — together understating real derivative storage by about 37%. Legacy objects keyed by a numeric id predate content addressing, match no file, and are counted for nobody; the count is logged rather than passed over in silence.

  22. · InitializerService

    Schema for measured thumbnail and track storage

    img_dat_derivatives records, per stored file, the real byte size of everything Kamo generated from it. It replaces img_dats.THUMB_FILE_SIZE as the source for that figure: that column holds one value for what can be dozens of page thumbnails and was populated for 5 of 84 rows in production. Run against production 2026-08-09; table and index verified.

  23. · kamo-shared-library

    Bill generated thumbnails to the organization

    Thumbnails and per-channel tracks are storage an organization's uploads cause, so on the owner's instruction they now count toward that organization's total rather than being absorbed by Kamo. GENERATED_PREVIEWS is a billed domain. Measured from the object store, not from img_dats.THUMB_FILE_SIZE. That column holds a single value for what can be dozens of page thumbnails ({imgDatId}_thumb_p0..pN) and in production was populated for 5 of 84 rows — understating real derivative storage by about 37% and omitting track objects entirely. img_dat_derivatives records what a sweep actually reads back. Attribution is per holding record, the same rule as the file itself: billing joins imgs to img_dat_derivatives, so two organizations holding one file are each billed for its previews and deduplication stays Kamo's saving rather than the customer's. Period figures date previews by the upload that caused them, so a preview falls in the same period as its file rather than whenever the sweep ran. The platform report's derivativeBytes now reads the same measured source, so it stops understating what image processing costs. Schema verified in production before this landed: img_dat_derivatives exists after a KI run.

  24. · kamo-internal

    Let a member keep a video or audio attachment, not just play it

    The player answers one intention. Wanting the file on disk is a different one, and video and audio were the two attachment kinds with no way to do it — a document has had a download button all along, and an image has one in its lightbox. Chrome buries a download in the video element's overflow menu; Firefox and Safari do not offer one at all, so the affordance has to be ours to be dependable. Both now carry a caption line naming the file with its size and a download button, using the same same-origin href + download attribute the document chip already uses. Also drops the always-KB size on the document chip. That stopped scaling the moment attachments could be gigabytes: a 2 GB file read '2130125 KB'. It now uses the same formatBytes the composer shows for that file before it is sent.

  25. · kamo-internal

    Show branding and Docstive document storage

    Both are written straight to the object store with no size recorded in the database, so they read as zero until a sweep measures them. They now have their own lines.

  26. · SecurityService

    Measure branding and Docstive document storage

    These are the only two things an organization stores that carry no size anywhere in the database — branding assets go straight into the public bucket under the organization's domain prefix, the docs workspace under its id — so both read as zero until something reads the object store back. Attribution runs backwards, prefix to organization. Deriving each organization's prefixes and scanning those would silently miss anything stored under a prefix nobody thought to derive; instead every top-level prefix is mapped back through widget, share, default, plus anything loose at the bucket root) lands in no customer's total. The members/ subtree is excluded deliberately. Avatars live under a fixed, organization-independent prefix by design, and counting the legacy per-domain copies would charge one organization for a file another organization's user owns. The trigger endpoint follows LosInternalApplyController: constant-time compare against the cluster secret, failing closed when it is unconfigured.

  27. · InitializerService

    Schema for object-store-only storage measurement

    object_storage_days records what a sweep reads back from MinIO for the two things that carry no size in the database: branding assets and the docs workspace. Bucketed by last-modified day so a billing window sums exactly. Run against production 2026-08-08; table and indexes verified.

  28. · kamo-shared-library

    Count branding and Docstive document storage

    Almost everything an organization stores records its own size somewhere — an imgs row, a kb_media row, a mailbox measurement. Two things do not. Branding assets (logos, favicons, background images, theme CSS and config) are written straight into the public bucket under the organization's domain prefix, and the docs workspace is written under its id. Neither leaves anything in the database saying how large it is, so both read as zero. object_storage_days holds what a sweep reads back from the object store, bucketed by last-modified day so an arbitrary billing window sums exactly rather than rounding to whole months. **************** supplies size and last-modified, deduped by key across nodes. Site replication puts the same object on several nodes, and counting it once per node would multiply an organization's bill by the size of the cluster. BRANDING excludes the members/ subtree deliberately. Member avatars live under a fixed, organization-independent prefix by design — a user can belong to several organizations, so an avatar is not the property of any one of them — and the per-domain copies are legacy. Counting them would charge one organization for a file another organization's user owns. Schema verified in production before this landed: object_storage_days exists with its uniqueness constraint after a KI run.

  29. · kamo-internal

    Show text message attachment storage

    MMS attachments live inline in the database as data: URIs rather than as files, so they had no size record and were never counted. They now have their own line.

  30. · kamo-shared-library

    Count text message attachments

    MMS attachments are stored, and were not counted. They are also not what they look like: VoipMessageService encodes the attachment as a data: URI and writes it inline into VOIP_MESSAGES.ATTACHMENT_URL, so they never become MinIO objects and there is no file to stat — the measurement is the stored character length, via the conversation's org. Base64 runs about a third larger than the bytes it encodes, and that inflated form is genuinely what occupies the database, so that is what gets counted.

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