Changelog
What's new.
Every improvement to the platform, as it ships.
· SecurityService
Say which locales each entry is really translated into
The marketing site's sitemap now lists every language version of every entry, and each entry page names the others as its hreflang alternates. A translation row is not proof of one: TranslateService answers directions it has no provider for with the English source, so every ta row, and the sw and th rows of older entries, repeat the English title word for word. Naming those would point search engines at pages that canonical back to English. - **************** returns, per entry, the locales whose title or description differs from the English, compared in the database in batches of 1,000 ids. - /public/sitemap carries `locales` per entry and takes order=oldest, so the sitemap's changelog files fill up and then never change. - /public/entry/{slug} carries `locales`, and `translated` now means a real translation rather than a row.
· kamo-internal
Void an organization from Platform -> Configuration -> Organizations
The Organizations tab gains Active and Void sub-tabs, and each organization's drawer a way to void it — or, on the Void tab, to restore it. - Active / Void sub-tabs above the directory, counted from the server's activeOrgs / voidedOrgs (which follow the search, whichever tab is open). The tab lives in the URL as orgStatus=void, like the rest of the console's view; it is a place, not a filter, so Clear keeps it. - The drawer's Void section (never on the platform org) opens a confirmation that spells out what voiding does: everyone inside is signed out, it leaves /network, sign-in and every list, its creator is treated as never having created it, and nothing is deleted. - A voided org's drawer leads with "Voided <date> by <operator>" and Restore; its trial and platform-product controls are switched off. - The server's refusals — platform org, the org you are signed into, live sub-orgs, still being charged — are shown in the dialog, in words. - POST **************** through a relay route (SecurityService enforces MANAGE_ORGANIZATIONS). Backend: kamo-shared-library d2c09630, securityservice 6c8d789, billingservice 072ca5f, mediaservice db90b81; English in kamo-translation-dictionary deba6215.
· MediaService
The roster no longer names a voided organization
An organization voided from Platform -> Configuration -> Organizations (orgs.is_active = FALSE) does not exist anywhere but the console's Void tab. findOwnedOrgsByOwnerIds — the roster's own copy of "which orgs does each executive own", kept for the org id beside each title — now carries the same live-org filter the shared findOwnedTitlesByOwnerIds and findDistinctOwnerUserIds gained in kamo-shared-library d2c09630. Pinned in Exec2ExecQueryParseTest, which also parses it against the entity model.
· BillingService
Active and Void tabs in the platform org directory
The console's Organizations tab now splits into Active and Void sub-tabs. A voided organization is orgs.is_active = FALSE (set by SecurityService's void endpoint); the directory is the one place still listing one — on the Void tab, so an operator can find it to restore. - GET /api/billing/platform/orgs takes status=void; anything else, including no status, lists live orgs only. Every older caller — the commerce wizard's target-org search among them — asks with no status, so none of them can be shown an organization that no longer exists. - Rows carry voided, voidedAt, voidedByUserId and a hydrated voidedByName (one batched lookup, only when the page has voided rows). - Aggregates follow the tab, and add activeOrgs / voidedOrgs, which ignore the tab (both tabs need both numbers) while honouring every other filter. - Sub-org counts leave out voided children. - The nightly storage snapshot and the platform storage report skip voided orgs; history is kept and the writer's gap handling catches a restored org up. Verified the generated statements read-only against the live database: Active 40 rows / counts 40 + 0, Void 0 rows.
· SecurityService
Void an organization, and let no way in reach a voided one
Platform -> Configuration -> Organizations can now void an organization. The void is orgs.is_active = FALSE; this commit adds the action and makes every way INTO an organization refuse a voided one. The action — POST **************** and /restore, MANAGE_ORGANIZATIONS (the tab's own right): - OrgVoidService refuses, changing nothing: the platform org, the org the operator is signed into, an org with live sub-orgs, and an org still being charged (a live PAID subscription with a price, targeting it or paid by its accounts). Otherwise it sets the flag, records voided_at / voided_by_user_id, and revokes every session its members hold. Restore clears all three. Nothing is deleted; members and data come back as they were. - 200 with the new state, 404 unknown, 409 with a reason the console translates. The ways in: - KSessionService.createSession — the waist every *** is minted through — refuses a voided org via VoidedOrgGuard (uncached PK read, fails open), next to the synthetic-account gate. - my-networks leaves voided orgs OUT: the home screen counts its isOwner rows to decide whether a member has created an org, so a member whose only created org was voided is offered the create-your-first-org flow again. all-networks follows via the shared getAllOrganizations(). - enter-as and cross-org impersonation answer 404, as for a missing org. - The session poll ends a session inside a voided org (backstop for one minted before the flag landed). - Mobile device exchange, post-verification auto-login, the network-directory search and the unscoped mailbox / work-email sign-in lookups skip voided orgs. - Platform overview count, storage sweeps and the fail-open impact report read the live enumeration. **************** already applied to the live database.
· kamo-shared-library
A voided organization is left out of every listing and host lookup
Platform operators can now void an organization from Platform -> Configuration -> Organizations. The void is orgs.is_active = FALSE — the flag sign-in and host resolution were already checking — so this library change makes the listings that never checked it agree: - findAllLive(): the org enumeration minus synthetic-owned AND voided orgs. **************** the choke point behind the org list, all-networks and the diagnostics count, now reads it. findAllExcludingFakeOwned() stays so services built before this still compile. - findAllByVerifiedDomain: a voided org's domain no longer resolves, so it stops accepting sign-ins and registrations. - findOwnedTitlesByOwnerIds / findDistinctOwnerUserIds: the Exec2Exec roster no longer names voided organizations as anybody's. - Organization.voidedAt / voidedByUserId: who voided it and when, for the console's Void tab. Never serialized. Columns applied to the live database before this commit **************** in securityservice). - Organization.isVoided(): reads the record state, never stored twice. - The general org PATCH no longer accepts recordState: void and restore are the only writers of is_active, so a settings save that echoes the row back cannot quietly restore a voided org.
· kamo-internal
Topic titles in the bar's dropdowns sit on a darker plate of the brand
A menu's own name at its top and every section heading (Support's "Online Support", "Bugs & Enhancements", "Training"; Docs' "Create new") now sit on a rounded plate of the organization's primary, darker, in full white — so a title reads as a label over its rows rather than one more row to click. The hover highlight goes lighter, the titles go darker; the two never look alike. The plate is translucent black (TOPIC_PLATE), which over the ground IS the ground mixed toward black: same hue, whatever the brand. A fixed "primary at N%" would have come out lighter than the ground under a menu at the bar's left end, where the seam ramp starts from the bar's 60% mix. Measured in a real browser over 11 brands at both ends of the bar: every plate is 0.73-0.80 of the ground beside it on each channel (no hue shift), white on it is 7.4:1 or better, and the glide never lands on a heading. A guard in navTopMenuIsWhite.test.ts pins both titles to the plate.
· kamo-internal
Faces on messages only in conversations of three or more
A face beside every message tells people apart, and between two people the side a message is drawn on already does that - so the corner face now appears only when three or more people are in the conversation (showsFaces), and never while the count is unknown, so a face is never shown and then taken away. - Chat: counted from the roster the window already holds, which follows people joining and leaving live; a two-person chat does not even ask for faces. - Support, Social, Exec2Exec: the server's count, now on the faces answer (MediaService e722c90) - each person once however many seats they hold, a social contact included. Name/attribution insets and the row headroom that make room for the face apply only when it is there. - SMS and AI Chat are always two parties (one member's number and one other; the member and the assistant), so their face code is removed entirely - back to exactly what they were - along with useOwnPhoto and MessageAvatar's glyph, which only they used.
· MediaService
The faces answer says how many people are in the conversation
GET **************** now carries `participants`: the console puts a face beside each message only when three or more people are in a conversation, since between two, the side a message is drawn on already says who wrote it. Counted from the seats, not the authors - somebody who wrote and left keeps a face on what they wrote without being in the conversation; less anybody no longer eligible for a live conversation (the chat roster's own rule, which always keeps the viewer); once per PERSON, because one person can hold several seats (an Exec2Exec customer is seated from each organization they wrote from); plus a social conversation's contact, who has no member row at all. A web-chat visitor's seat is the org's SYSTEM member, so it counts once, as them.
· kamo-marketing
Generate the sitemaps per request, and give every change its own page
/sitemap.xml was a metadata file prerendered at build time, in a container that cannot reach KBService or SecurityService, so every deploy shipped a sitemap with no help-center article in it until the first ISR refresh. And /api/sitemap kept a second copy of the page list that had drifted from it. - /sitemap.xml is a sitemap index generated per request. It lists /sitemaps/pages.xml (hand-written pages, collection pages, blog posts and every knowledge-base article, with hreflang) and /sitemaps/changelog/<n>.xml (5,000 changelog entries each). /api/sitemap and /api/sitemap/changelog/<n> are the JSON renderings of the same module, app/lib/sitemap-source.ts. - The hand-written page list lives once, in app/lib/sitemap-pages.ts, and guard:sitemap fails the build when a static route is neither listed nor excluded with a reason. - /changelog is paged on the server: ?page=N, with ?project= and ?type= answered by SecurityService, where the old filters only hid entries among the 50 on screen. Each page is its own canonical; filtered views are noindex, follow. - Every entry has its own page at /changelog/<slug>, linked to the entries either side of it. It is canonical to English where the locale has no translation yet, and an older spelling of its address 308s to the current one. The homepage strip links to these pages. - Stats and filters are read on the server. The /api/changelog/stats route they came from had been serving the build container's total of 0.
· SecurityService
Give every public entry an address, and page the list on the server
The public changelog served entries with no identifier, so the marketing site could neither link to one nor list them in a sitemap. Each entry now carries a slug - its English title in URL form, then twelve characters of its commit hash - and: - GET **************** returns one entry with the entries either side of it. It is found by the hash prefix alone, so an entry whose title has changed still resolves at its old address, and the caller redirects to the current one. - GET /api/changelog/public/sitemap returns every entry's slug, title and date in chunks of up to 5,000, reading three columns per row. - GET /api/changelog/public accepts `project` and `type` filters, which can only narrow the public list, and orders on (dateCommitted, id): commits sharing a timestamp no longer swap pages between two requests. A page is capped at 100 entries. The queries live in PublicChangelogService rather than CommitLogRepository, so none of this needs a shared-library release.
· kamo-internal
Every message wears its author's face, on the bubble's corner
In every chat popup tool window - Chat, Support, Exec2Exec, Social, SMS, AI Chat - each message now carries a 16x16 photo that overlaps the bubble's top-left corner for a message on the left and its top-right corner for one on the right. Every message, not only the first of a run: the Chat window's old face in the header line is gone, and name/attribution lines are inset so the face never sits on their first letter. Initials only when the person has no photo. The face is three-valued (MessageAvatar): a URL is a photo, tried as its _sm variant and then as the full-size file (small variants are optional server-side; older uploads lack them); null is initials; undefined - not known yet - is a blank disc, so somebody WITH a photo never flashes as initials while it is looked up. Where the photos come from. The windows used to read only the viewer's own member directory (ACTIVE team members of the signed-in org), so plain Members, the whole other side of a support ticket or an Exec2Exec conversation, and anybody who had left a group were drawn as initials with a photo on their profile. Now one store per conversation (app/lib/chat/sessionFaces.ts) reads MediaService's new GET **************** - everybody on the conversation AND everybody who ever wrote in it, from their own profile, on a host the console may load - through a route file of its own (/api/media is not a proxied prefix). A newcomer's first message re-asks once; somebody the server still does not cover, or everybody while an older MediaService cannot answer, falls back to the directory lookup the windows used before. The rows that must never wear the face of the member they are stored under: - a web-chat visitor (stored under the org's SYSTEM member, the owner) gets the visitor's initials; - a social contact (stored under the connection owner) gets the photo their provider gave, through the image proxy. SMS: sent texts are the viewer's own (a conversation belongs to one member's number); received ones wear the contact's photo from the directory the header reads, their initials, or a phone mark for a number nobody is saved under. AI Chat: the member's photo on what they asked; the assistant, which has no profile, wears the AI sparkle. Rows in the 4px-packed lists with no name line above get 4px of headroom: measured in real Chrome, the face otherwise sat 1-2px from the timestamp of the message before.
· kamo-internal
The Signature Requests tab, and a colleague's save in the template editor
Signature Requests, between Templates and Common in the Docs manager: every document the collection's templates sent out for signature, in a grid paged on the server and split into three buckets (Not Executed, Partially Executed, Fully Executed), each showing how many are new to this member. The tab shows the total that is new. Looking at a bucket clears its count for this member only, and the tab opens on the first bucket holding something new. Columns: the document and who sent it, the signers (a ring per signer in the colour of where they stand), x of y signed, status, sent, last activity and fully executed. Each row can nudge everyone still waiting, download the executed document, the certificate of completion or both as a ZIP, and open the request. The request drawer adds a nudge per signer, what happened when, and withdrawing the request with a reason. The template editor now registers that it has a template open (a heartbeat to **************** so a colleague's save or restore of that file reaches it, and it says who made the change. Its live topic was built from the member id as a JS number, which rounds ids past 2^53, so the save notice never arrived. It now uses the id string.
· ESigService
Signature requests for the Docs manager, with new counts per member
The Docs manager gets a Signature Requests tab: every document a collection's templates sent out for signature, in three buckets by signatures (nobody, some, or everybody has signed), each with how many are new to this member. /api/esig/requests lists a bucket one page at a time, sorted and searched in the database. /counts gives each bucket's total and new count, and /seen records that a member looked at a bucket. What each member has seen lives in esign_request_views **************** applied by hand), keyed by member, collection and bucket. A request is new in a bucket when it entered that bucket after the member last looked there. Per request: fetch it, nudge the signers still waiting (a re-invite with a reminder line, at most once an hour per signer), withdraw it, and download the executed document, the certificate of completion, or both as a ZIP. Reading needs VIEW_DOCUMENTS; nudging and withdrawing need EDIT_DOCUMENTS. A request is only reachable through a template in a collection the member can see, at their clearance.
· DocsService
A save reaches every colleague with the template open
A save of a template's file told only the member who saved it, so a colleague with the same template open kept placing fields over pages the file had moved on from. The template editor now says when it has a template open: POST **************** every 30 seconds while it is, DELETE when it closes. OpenEditorRegistry keeps that in Redis as one sorted set per document (member -> lapse time, 95 s), so an editor that vanished without closing drops out by itself. A save publishes one frame per member with the template open, each on that member's own topic, and says who made the change. The watch checks that the member can read the document, the way every read does. A restore now tells colleagues too, with the design version it brought back, so their editor reloads the fields as well as the pages.
· MediaService
A face for every message in every chat window
GET **************** answers, for one conversation, the photo to draw beside each message: everybody on it now AND everybody who ever wrote in it (a roster forgets people who left; their messages do not), each listed in memberIds whether or not they have a photo, so the console can tell "no photo: initials" from "not asked about yet". Photos are Exec2ExecFaces.memberPhotos - the member's profile photo, else the person's own - built on the viewer's live theme host, the one the console's img-src allows. A social conversation also names its contact (not a member at all) as `counterpart`, with the provider's photo. Why the server: the console's only source was the viewer's own member directory - ACTIVE team members of the signed-in org - so plain Members, the whole other side of a support ticket or an Exec2Exec conversation, and anybody who had left a group were all drawn as initials with a photo on their profile. The per-member lookup it could fall back on refuses other orgs. Who may ask is who may read the transcript (a signed-in member, a session that exists): a face says less than the messages beside it. Also: the history read now tags what a social contact wrote as SOCIAL_VISITOR under the contact's name, as the live frame always has. Those rows are stored under the connection owner's member, so a reloaded conversation filed the contact's words under the owner's name, on the owner's side when the owner was reading - and would have put the owner's photo beside them.
· MediaService
A meeting link in the conversation, always on the platform's Kamo Meet
POST **************** lets either side of an Exec2Exec conversation add a meeting link, behind the same gate as sending a message. The meeting is created as the PLATFORM organization, on Kamo Meet, on the platform's live domain (meet.kamocrm.com/<room>), whoever asks - a client's own white-label meet host or Zoom would put a meeting with KamoCRM's executives on a company KamoCRM is merely talking to - with the caller as its creator. It is then posted as the caller's message through Exec2ExecService.post, so it reaches both sides live, raised, badged and pushed; a passcode, when the platform's rooms need one, goes beside it. **************** resolves Kamo Meet for an org whatever provider it has chosen (its meet settings still apply, another provider's credentials do not), and **************** creates through it; the existing create path is unchanged. No START_MEETING right is asked of the caller: that right is about their own organization's meetings, and this one is not theirs.
· kamo-internal
Add a meeting link to the conversation - always the platform's Kamo Meet
Both sides of an Exec2Exec window now carry the camera the other chat windows have: Add a meeting link. The meeting is created by MediaService as the PLATFORM organization, on the platform's Kamo Meet address (meet.kamocrm.com/...), whichever side presses it - never the client's own white-label meet host or provider - and posted as the presser's message, so it arrives in both windows live, raised and badged like any message. Links in the Exec2Exec transcript are now clickable, through the same linkifyPlainText every other chat window uses (exported from MessageList rather than copied).
· kamo-internal
The desk's filters, faces and presence; an Exec2Exec badge that burns
Interaction Center > Chats > Exec2Exec gets two filters under the sub-tabs: Org Owner (All | Org-less | Org Owner) and Order By (Last Message | Latest Sign In) with an ASC/DESC switch. Remembered for the browser session like every place in the panel, and a fresh session opens on All | Latest Sign In | Desc. Somebody with no date sorts last either way. Each row now shows the client's own photo (initials only when they have none), whether they are online, when they last signed in and when the conversation was last spoken in - in the member's own time zone - beside the unread count. The desk re-reads itself once a minute while it is visible, so presence does not go stale. The Exec2Exec window draws each KamoCRM executive's photo from their KamoCRM member profile in its participants strip, and the client's photo in the desk's header, carried from the roster row or from the frame that raised the window. NavTop's Exec2Exec control, for a client and for the desk alike, burns while anything is unread: a comet of gold, white and sky light racing round the bolt, a gold shockwave, the bolt kicking every couple of seconds and the count beating. Under reduced motion it holds still and stays lit, and the count is spoken in the button's name. The counts were already live over the unread WebSocket.
· kamo-internal
Files and Templates, version history, and versioning settings
Members asked for documents that keep their history the way their organization chooses, and for templates that stand on their own instead of the Originals/Modified split. The services already hold it (one record per document, img_versions, a mode per org and per category); this is the web app over it. Docs manager - Files and Templates tabs replace Originals and Modified. A template is a document with a template layer, made from a file (or another template) by "Add Template Layer" and separate from it from then on. - Row actions, in the order asked for. Files: Download, Edit file, Add template layer, Add to binder, Share, Send to trash. Templates add Edit template after Edit file. An action the member has no right to is not shown; one the document cannot support (a PDF in the classic editor, pages still being prepared) is shown disabled with its reason. - A Version column: a pill reading v4, with an amber dot when a Manual working copy has moved past its last snapshot, opening the history. - Share is offered in every collection, not only My Docs. Trash rows can be restored. Footer counts are the open tab's. Version history drawer - The selected version's pages on a stage (a deck of the versions under it, page by page, a template's fields drawn over them) and the versions on a rail, newest first, with what produced each one and who. - Compare slides the current state across any earlier version. - Take snapshot (Manual), Restore (confirmed in the words of the category's mode: what the restore becomes, and what it would discard), Name a version, Download any version, Upload a new version with progress. Template editor - Edit underlying file opens the template's own file in the classic editor in another tab. Each save there arrives over the saves topic; the pages are refetched and redrawn under the fields in place, with no full-screen reload and nothing unsaved lost, and the version pill moves to the new number. A chip says the file is open elsewhere and takes the member back to it. - The version pill and the history drawer replace the design-only version panel: a template's version is its file and its fields together, and a restore puts both back. - Page previews are keyed on the content id, so the browser cache can never show pages the document has moved on from; a PDF still being rendered is waited for rather than reported as an error. Settings - Document Management > General: the organization's default versioning (Automatic, Manual snapshots, Off), drawn as what each does to a history, and the categories that set their own. - A category's Configuration: follow the default or choose its own; categories the platform locks to Automatic say so. - The categories table gains a Versioning column. Dictionary strings landed first in kamo-translation-dictionary 9b83d282.
· MediaService
Faces on the desk — a client's photo, last sign-in and presence
The desk's roster now carries, for every client, their own photo (users.avatar_id, built on the operator's live theme host — the only one their console loads images from), when they last signed in anywhere on the platform (users.last_login, parsed once and sent in the shape of lastActivityAt), and whether any of their member rows holds a live socket. The participants strip gets each KamoCRM executive's photo from their KamoCRM member profile (member avatar, else the person's), built for the viewer's console; and the frame that raises a desk window carries the client's photo beside their name. All of it lives in one small Exec2ExecFaces component: loaded by id rather than off the lazy JOINED association (whose proxy has no photo path), one avatar read and one presence sweep per page, and every failure degrades to initials / offline rather than failing the roster.
· DocsService
Read a template's layer by design version
The version history draws a template's fields over each version's pages. It asked for them through a document version (/{imgId}/{n}/design), and a template's working copy has none: its fields were saved after the last snapshot, at a design version no document version names yet, so the current state could not be drawn under Manual versioning. GET **************** reads the layer by design version instead, behind the same gates as the rest of the history. A file has no layer and answers with none.
· ConversionService
Upload a new version of a document
The one way to change a file the document editor cannot open - a PDF above all, which is what most templates are. POST **************** takes the new file, scans it for malware and content-addresses it exactly as a first upload, and hands the change to **************** (kamo-shared-library 6d0523e8): the document keeps its id, name, shares, binders and template layer, and its category's versioning mode decides what is kept of what it held - a new version under Automatic, the working copy moving under Manual, an overwrite under Disabled. The PDF rendition and page thumbnails are made for the new bytes by the same pipeline an upload runs. EDIT_DOCUMENTS, not CREATE_DOCUMENTS: nothing new is created. The category's extension policy applies **************** is public for this), and the type is read from the bytes - a new version may be a different format, and the name's extension follows it. Refused for an EXECUTED legal package - the signed record HR issued to a member, which nobody gives a new version, not even its subject - through IssuedArtifactLookup, a read-only local repository over the shared assignment entity; and for the per-party mortgage collections, which the generic imaging endpoints never touch. esign.repos is now scanned here, because a version of a template records its layer's design version.
· ESigService
Saving a template's fields is a version of the template
A template is a document with a layer of fields over it, and a version of it is both halves: its bytes and that layer. Its underlying file's saves already reach the version history (kamo-shared-library 6d0523e8, docsservice 3ef26d1); this is the other half. saveDesign and revertDesign hand the change to **************** in the same transaction as the design write, so a version never names a design that was rolled back. What that writes is the category's versioning mode: under Automatic a new version of the template (one member's saves within ten minutes grow one version, so pressing Save after every field is still one version of the work), under Manual the working copy moves, under Disabled nothing. A layer keyed on something that is not a document id has no history to write, and saves as before.
· DocsService
Files and templates, edited in place, with the versions each category keeps
A document was two records: the upload, which nothing edited, and one modified copy derived the first time anyone opened it in either editor. The Docs manager showed the pair as "Originals" and "Modified", and members found it confusing. A document is one record now, and what each change keeps of the content it replaces is the category's versioning mode (kamo-shared-library 6d0523e8): Disabled, Automatic, or Manual. Editing - /open edits the document itself - a file, or a template's own bytes ("Edit Underlying File"). /new opens the document it created. Neither derives a copy any more. - A classic save goes through **************** a new version under Automatic (the WOPI token now carries a session id, so one sitting that autosaves is one version), the working copy moving under Manual, an overwrite under Disabled. - The save notice now fires for any document whose bytes actually changed - not for a re-sent file - and carries the version it wrote; a template editor open on the document redraws. - Save As makes a new FILE and the editor carries on in it. - A save still arriving for a classic copy left by the old model (a tab opened before the deploy) lands on its original instead of a row nothing lists. Templates - POST **************** ("Add Template Layer"): a new template sharing the source's bytes and nothing else from then on, in the same collection, folder and access level. Branched from a template it can carry that template's fields: signers are copied and every signerUid in the design is rewritten to its copy. Its layer is named after it, and its first version records what it was made from. Takes CREATE_DOCUMENTS - it makes a document. - /form-fillable opens a template only; a file answers 409 NOT_A_TEMPLATE. Versions - **************** - history; snapshot (with an optional name); rename; restore; and each version's download, PDF, page thumbnails and template-layer design. Same gates as the imaging byte paths: VIEW/EDIT documents, organization and clearance, the HR-issued package gate, the per-party mortgage collections, and the PHI access audit on every byte read. Version bytes are immutable, so they are cached as such, and a downloaded old version is named "Name (v3).ext". Listing and settings - The list is one query (DocumentListQuery, over a local specification repository): the old in-memory filter after paging could drop a rare kind - a template among 300 files - off the page. filesOnly / templatesOnly select the two tabs; rows carry the template flag, the content id and a version summary. The classic copies the old model left are never listed. - Settings: GET|PUT /imaging/settings/versioning for the organization's default; assoc-config and categories carry each category's override, what it resolves to, and whether it is locked. Writes take MANAGE_DOCS_SETTINGS, which the settings pages always required and these endpoints never checked. - Classic saves now key page thumbnails on the document's own dat, as uploads always did; the thumbnail readers look in both places, so documents saved before today still show their pages. Schema: **************** (applied). Data: **************** - run once this build is deployed: the 5 derived rows with a template layer become templates, the 2 live classic edits fold into their originals as v2, and every classic copy is retired.
· kamo-shared-library
Document versions, and the versioning mode an organization chooses
Documents were two records: the upload, which nothing edited, and one modified copy derived the first time anyone opened it to edit. Members found the pair confusing, and it fitted nobody's actual rules - one organization wants every edit kept, another wants one copy and no history, a third wants to freeze a state on purpose and keep working on top of it. A document is one record now, and what happens to its previous content when it changes is a versioning mode chosen per organization (img_org_assoc.versioning_mode) and per document category **************** which wins): - DISABLED: the change overwrites. One copy, no history. - AUTO: every change is the next version and the old one is kept. One classic-editor session is one version however often it autosaves; one member's template-layer saves within ten minutes are one version too. - MANUAL: changes overwrite a working copy on top of the last snapshot; a member snapshots to keep a state permanently and carries on editing on top of it. The upload is the first snapshot. Unset everywhere, a category is AUTO - the only mode that loses nothing, as the model it replaces never lost an upload. A category whose documents are records (overwrite off and not settable, e.g. Patient Charts) is locked to AUTO. - ImgVersion / img_versions: a version is a POINTER to the content-addressed img_dats row the document held, plus the template-layer design version on a template. Keeping one costs nothing until the bytes move on without it. - DocumentVersioningService: the one write path for "this document's content changed" - applyContentChange, recordTemplateLayerChange, snapshot, restore, recordBranch - so the modes mean the same whether the change came from a classic save (DocsService), a new upload over the document (ConversionService) or a template layer save (ESigService). Versions are written lazily: a document that never changed has none, and the first kept change writes v1 from the state before it, so an organization can turn versioning on at any time with no backfill. Restoring is itself a new version under AUTO; history is never rewritten. The e-sign repositories are ObjectProviders, since not every service scans them (SharedLibBeanSafetyTest). - Img.isTemplate() over the existing has_form_layer flag, and isRetiredDerivedCopy() for the classic copies the old model left behind, which no listing shows again. - **************** for version pills on a page of templates in one query. - **************** is public, so a new version meets its first upload's rule. The schema is **************** in docsservice, applied to the cluster before this commit: img_org_assoc is read on every upload, so the column had to exist first.
· kamo-internal
A Nav Logo Overlay and a Careers Logo Overlay
Behavior & Labeling gains two more logo overlays in the vocabulary the other six use - a token, resolved against the org's own palette: - "Nav Logo Overlay", directly under "Home Page Logo Overlay". Paints the simple logo at the top of the primary nav rail (NavPri) - whichever mark the rail shows, company or software - collapsed and expanded. In-app like its home page sibling, so it rides the org record through OrgContext: the org route's schema, Organization.fromJSON (which runs twice on the way to the browser) and OrgProvider's props all carry it, and it takes effect on save. - "Careers Logo Overlay", directly under "Register Site Logo Overlay". Painted by kamo-careers from the colour securityservice bakes into the theme config.json (10d4a9e), like the sign-in, sign-up and apps ones. That site re-reads the file hourly, and the hint says so. Also fixes the save path for three existing overlays: sign-in, sign-up and apps were stored but never copied back into the saved state, so their read-only rows showed the previous choice until a reload, and the next edit started from it. logoOverlayWiring.test.ts walks all eight overlays through the seven places each must appear (state, load, edit buffer, dirty check, PUT body, post-save sync, both views), checks both views list them in the same order, checks en.json has each name and hint - the i18n guard cannot see keys the page builds from a variable - and checks both of NavPri's ThemeLogos take the colour. Against the previous LabelManager it fails for nav, careers, and the three missing post-save syncs. Verified in Chrome with the real NavPri bundled over stubbed contexts: the logo box is unchanged and the silhouette takes White, Black and the org's lightest secondary, collapsed and expanded. Needs kamo-shared-library 7d80fa7b and securityservice 10d4a9e for the two columns to save; the dictionary carries the four English strings.
· SecurityService
Bake the careers site's logo overlay into config.json
kamo-careers paints the org's logo in its header and has no OrgContext; it reads the theme config.json on the server, the same file login, register and apps take their overlays from. So the careers overlay goes there too, as careersLogoOverlayColor: the TOKEN resolved against the org's palette here, absent when the org chose none - which is how every client reads "no overlay". Needs kamo-shared-library 7d80fa7b **************** which this build clones from main. The nav rail's overlay, added in the same library commit, is in-app and reaches the browser on the org record itself; nothing here bakes it. Verified: main and the whole test source set compile against that library; **************** 14/14 (two new: the key is carried, and an org that overlays only its sign-up site does not get a careers key), OrgThemeProvisionServiceTest 20/20.
· kamo-shared-library
A logo overlay for the nav rail and one for the careers site
Two more columns in the vocabulary home_logo_overlay established: a token, not a hex, so an org that rebrands keeps an overlay that matches its new colours. Both nullable with no backfill - null is NONE, which is what every org looks like today. nav_logo_overlay paints the simple logo at the top of kamo-internal's primary nav rail. Its own column because the rail is a darkened shade of the primary colour; the home page's white masthead rarely wants the same shade. Delivered through OrgContext like its home page sibling. careers_logo_overlay paints the header logo on kamo-careers. That site has no OrgContext, so securityservice bakes the resolved colour into the theme config.json, the route login, register and apps already take. patchOrganization is an ALLOW-LIST, so both names go there too, and a new test walks every overlay the settings page sends - not just these two - because a column the method does not name is accepted with a 200 and never stored. Proven by removing the two new lines: 3 of its 4 cases fail. The columns were added to the live orgs table by hand before this push (ddl-auto is none and InitializerService is not deployed): ALTER TABLE orgs ADD COLUMN IF NOT EXISTS nav_logo_overlay VARCHAR(24); ALTER TABLE orgs ADD COLUMN IF NOT EXISTS careers_logo_overlay VARCHAR(24);
· kamo-internal
A minimized Exec2Exec head runs a current of light round its rim
A line to KamoCRM's executives must never sit in the dock looking like one more chat. Its minimized head now carries an energy rim: a gold-to-white-to-sky arc racing round the hexagon, a thinner counter-current chasing the other way, and a pulsing gold aura behind them. Under prefers-reduced-motion it holds still as a bright gold ring. The effect is a tool-level property (hexHeadEffect on the Exec2Exec platform entry), re-injected with the icon on every spawn, so every Exec2Exec head gets it and no other head can.

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