Changelog
What's new.
Every improvement to the platform, as it ships.
· MediaService
AI auto-tag + caption via org's providers (P3)
MaterialAiController POST **************** verifies ownership, mints a short-lived signed material URL, and calls AIService's internal vision endpoint (org's own providers, LIGHT tier) via AiVisionClient. Lenient tag parsing; fail-closed if no vision provider configured.
· kamo-shared-library
AiProvider cost tier + vision flag (two orthogonal capability axes)
Adds costTier (LIGHT/MEDIUM/HEAVY via new AiProviderTier enum) and supportsVision boolean to AiProvider, so integrations can pick how expensive a provider to use and which support vision. Repo finder for active vision providers. Columns added to AI_PROVIDERS via ALTER.
· EmailService
Server-side "display external images" consent
Replaces the client-local (localStorage) remembering of the external-image choice with a durable per-member record. New RemoteImageConsentService (fail-soft reads so message fetch still works before the table exists) + POST /api/email/remote-image-consent to record consent. The full-message DTO now carries remoteImagesAllowed (looked up by member + Message-ID), populated in the single-message, thread, and shared-mailbox fetch paths so the client seeds its gate from the server.
· kamo-shared-library
EmailRemoteImageConsent entity + repository
Member-scoped record that a member has chosen to display external (remote) images for a specific message, keyed by member_id + normalized RFC Message-ID (unique). Follows the email_metadata member-scoped pattern (FilterRuleEntity). Backs the server-side "display external images" preference so the choice persists across devices and sessions. KamoInitializer creates the table on its next run.
· InitializerService
Add INVESTOR_PRICE column to rate_locks + loan_pricing_snapshots
@Order(80) idempotent ADD COLUMN IF NOT EXISTS (DECIMAL(9,4), matching the price-to-100 grids) so the MLOS RCE can record the true non-par investor execution price on the binding lock-of-record and its bound quote instead of par-by-omission. Additive nullable; mirrors RecordingTranscriptMigration.
· kamo-shared-library
Record the true non-par investor price on RateLock + LoanPricingSnapshot
The binding rate-lock-of-record and its bound quote carried only rate + lockDays; discount/origination points were left null, so the core entities read PAR while the real priced number **************** price-to-100 with lender margin baked in) lived only on the MLOS side-ledger MlosLockRecord.lockedPrice. Add an INVESTOR_PRICE column (DECIMAL(9,4), matching the shipped price-to-100 grids) to both entities + their DTOs + **************** It is the INVESTOR execution price, deliberately NOT decomposed into borrower points (that nets out margin and needs the fee model — decomposing now would misstate borrower points, a Reg-Z/ECOA disclosure error). Points stay null until the fee model lands; the raw price is now preserved on the lock-of-record, not just the side ledger. Populated by the RCE (MLOSCapitalMarketsService) from finalPrice; column materialized by KamoInitializer RateLockInvestorPriceMigration (ADD COLUMN IF NOT EXISTS).
· kamo-internal
Let admins connect RingCentral on a member's behalf
On another member's phone settings, an admin with manage-VOIP rights (canManageVoipConfig) now sees Connect/Reconnect/Disconnect for the member's RingCentral account instead of a read-only "they must connect it themselves" message — for the common case where a sysadmin holds the member's RingCentral credentials. The connect copy makes clear you sign in with THAT member's credentials, and the target memberID is passed through (server enforces the right).
· VOIPService
Allow admins to connect RingCentral on a member's behalf
The per-member RingCentral flow was self-serve only, so an admin managing another member's profile saw "they must connect it themselves". Sysadmins commonly create the member's RingCentral account and hold their credentials, so they should be able to connect on the member's behalf. - OAuthController member start/status/disconnect now accept an optional target memberId. Acting on another member requires MANAGE_EXTENSIONS or MANAGE_VOIP_SETTINGS (mirrors the profile-edit gate); self is always allowed. - start also verifies the target member is actually assigned that instance (the instance is org-scoped, so this confirms same-org). - Whoever completes the RingCentral login still determines the identity; the assigned-vs-connected extension guard flags a mismatch.
· kamo-internal
Usage analytics + Insights view (P2)
Records **************** events and an Insights dialog with org totals + the most-used materials (by downloads + previews).
· MediaService
MaterialAnalyticsController — record events + aggregates
POST **************** (record) + GET /analytics (per-material counts by type + org totals).
· kamo-shared-library
MaterialEvent entity+repo (usage analytics)
Append-only usage events **************** with a per-(imgId,type) aggregate query. Table MATERIAL_EVENTS (targeted CRDB DDL; KI auto-scans).
· kamo-internal
Approval workflow UI + load-all filtering (P2)
Status control in the drawer **************** status badge on tiles, and a status filter in the toolbar. Also loads all material pages up-front (capped) so search/tag/ campaign/status filters cover the whole library, not just the first page.
· MediaService
MaterialReviewController — status map + set-status
GET /api/media/materials/reviews (org-wide imgId→status map) + POST to set a material's status (validated, reviewer-stamped, upsert).
· kamo-shared-library
MaterialReview entity+repo (approval status side-table)
DRAFT/PENDING/APPROVED/ARCHIVED per material, keyed by imgId (imaging schema untouched). Table MATERIAL_REVIEWS (targeted CRDB DDL; KI auto-scans).
· kamo-internal
Per-member RingCentral connect + softphone web-phone v2 fixes
Softphone SDK fixes (ringcentral-web-phone is v2.4.0, not the v0.x the adapter targeted — the old wp.userAgent.* usage threw on init): - Rewrite RingCentralPhoneSdk for the v2 API: single sipInfo object, start()/call()/dispose(), inboundCall/outboundCall events, early outbound wiring, synthesized mute/hold/muted/unmuted, and adoption of the SDK's per-call audio element as #rc-remote-audio so AV volume/device routing works. - PhoneSdk.SipInfo: add outboundProxyBackup + stunServers. - useSoftphone: clear the stuck incoming-call banner on a remote hangup during ring; don't tear down the SDK mid-call on credential refresh; guard against auto-answer + call-forward double-accepting. Per-member RingCentral connection: - PhoneSettingsTab: self-serve "Connect RingCentral" card on the member's own profile (admins get read-only status), OAuth redirect handling, and an extension-mismatch warning. - voipApi: member OAuth start/status/disconnect. - useSoftphone + SoftphonePanel: needsMemberConnection gating with a "Connect your RingCentral account" prompt instead of a registration error.
· VOIPService
Per-member RingCentral OAuth identity + softphone SIP fixes
Softphone SIP credential fixes: - getSipCredentials: map outboundProxyBackup + stunServers (web-phone v2 needs them for WSS failover + ICE/media); throw ProviderException (502) instead of a bare RuntimeException (500) that SipController's catch missed; guard against a wrong-extension SIP identity. Per-member RingCentral connection: - RingCentralTokenResolver: resolve/refresh a member's own token with single-flight (RC rotates refresh tokens), needs-reconnect on a dead token, and revoke-on-disconnect. - OAuthController: **************** — memberId from the session only (self-serve), VoipCalling scope, PKCE; connect callback records the connected extension. - SipController + PhoneProviderFactory: the softphone registers as the member's OWN extension; returns a 200 needsMemberConnection discriminator when unconnected. - MemberVoipConfigController: expose connection status; revoke on reassignment. - New-table reads degrade gracefully if member_voip_oauth_token isn't migrated yet.
· kamo-shared-library
Add MemberVoipOAuthToken for per-member RingCentral connections
Per-member OAuth token keyed (memberId, provider, instanceId) backing extension-scoped RingCentral operations — softphone sip-provision plus the member's personal call log / voicemail / presence. Distinct from the org/instance OrgOAuthToken (account-scoped). Requires a KamoInitializer run to create ****************
· kamo-internal
Collections/campaigns UI (P2)
Left-nav Collections section (create/select/delete), campaign filter on the collection view, and a collections editor in the detail drawer (add/remove material, create-and-add). Wired through /api/materials/campaigns/* + materialsCampaignsApi; workspace joins the item map client-side like tags.
· MediaService
MaterialCampaignController — CRUD + item map + add/remove
/api/media/materials/campaigns: list/create/update/delete, GET /items (org-wide imgId↔campaignId map), POST /add + /remove with org-ownership checks.
· kamo-shared-library
MaterialCampaign + item entities/repos (collections)
First-class named collections grouping materials across folders. Tables MATERIAL_CAMPAIGNS + MATERIAL_CAMPAIGN_ITEMS (targeted CRDB DDL; KI auto-scans on next run).
· kamo-internal
Tags UI + access-level control (P2)
Tags (org-scoped, cross-cutting): tag chips on tiles, a tag filter bar (AND across selected), and a tag editor in the detail drawer (assign/remove existing, create-and-assign new). Wired through /api/materials/tags/* proxies + materialsTagsApi. Also adds a Visibility (access-level) control to the drawer using the existing access-level endpoint.
· MediaService
MaterialTagController — tag CRUD + assign/unassign + assignment map
/api/media/materials/tags: list/create/delete tags, GET /assignments (org-wide imgId↔tagId map the workspace joins client-side), POST /assign + /unassign with org-ownership checks.
· kamo-shared-library
MaterialTag + assignment entities/repos (org-scoped tags)
Flat, cross-cutting tag axis over folders for marketing materials. Tables MATERIAL_TAGS and MATERIAL_TAG_ASSIGNMENTS (created via targeted CRDB DDL; KI auto-scans on next run).
· kamo-internal
Managed public links UI (P1) — create/expiry/revoke/view-count
Detail drawer gains a 'Public links' panel (ShareLinkManager): create a durable link with an optional label and expiry (1h/1d/7d/30d/never), copy it, see its view count, and revoke it. Backed by the new MediaService registry endpoints via /api/materials/links proxies. The one-shot quick-copy link stays for fast social/email sharing.
· MediaService
Managed public-link endpoints (create/list/revoke) + tracked serve
Adds POST/GET /api/media/materials/links and DELETE /links/{id} backed by MaterialShareLink, and resolves managed tokens on the public GET first (checking revoked/expiry and bumping viewCount) before falling back to the legacy HMAC verifier. Random opaque tokens, org-scoped ownership checks, TTL clamped to <=1y.
· kamo-shared-library
MaterialShareLink registry entity + repo (managed/tracked public links)
Backs revocable, expiring, view-counted public links for marketing materials — persisted so links can be listed, revoked, and counted (vs. the stateless HMAC one-shot tokens). Auto-scanned by KI (@EntityScan com.kamo) and MediaService; needs a KI run to create the MATERIAL_SHARE_LINKS table.
· kamo-internal
Add right-click / long-press context menu to conversation-view rows
Completes the list action menu across both view modes. Conversation rows now open the same shared menu on right-click and touch long-press with thread-level actions: Reply / Reply All / Forward (operating on the thread's latest message, fetched on demand and composed from the active mailbox), Report Spam / Not Spam and Delete (applied across the whole thread), Mark read, and Star / Unstar.
· kamo-shared-library
DecisionGate.appliedThreshold exposes the actual confidence bar
The gate internally applies max(tenant override, safetyFloor, stage floor) but callers only had the pre-gate tenant wall. appliedThreshold returns the true bar so a decision-of-record can persist the threshold-IN-EFFECT (examiner- reproducible) instead of a value the gate silently raises. Additive; evaluate() unchanged. DecisionGateTest 14/14 pass.
· kamo-internal
Enterprise Materials workspace (P0) — folders, search, bulk, real preview, shared email composer
Replaces the flat 200-item grid with a full document workspace over the existing imaging backend (which already supports folders, trash, sharing, history, bulk for MATERIALS): - Folder sidebar (create/rename/delete, drag-to-folder) + All / Trash views - Search, sort, grid<->list toggle, paginated 'load more' (removes the 200 cap) - Bulk select + bulk download (ZIP) / email / move / delete - Right-hand detail drawer: real preview, inline rename + description, move-to-folder, internal sharing (member, allow-edit, expiry) and activity history - Email now reuses the shared MessageTool composer via the global tool:open event (From/mailbox, CC/BCC, rich body, signatures, drafts) instead of a bespoke form - New imaging proxy routes: folder create/[id] rename+delete/move-doc, rename, description, restore, access-level - Removes the superseded MaterialsLibrary; all Canva features (create/import/autofill/ schedules) carried over. imgIds handled as precise strings throughout.
· kamo-internal
Overhaul /messages — iframe rendering, list context menu, threading, races, a11y
Implements the audit of the /messages page end-to-end. Reported issues: - Reply/Reply All/Forward buttons now show text labels; removed from the more-actions dropdown (they're adjacent buttons) while the new list menu keeps them. - Message-list rows now open the shared action menu on right-click and touch long-press **************** rows gain a focus ring (j/k), selection checkbox (x), and scroll-into-view. - Email now renders in a sandboxed auto-height iframe (srcdoc) instead of inline dangerouslySetInnerHTML. This isolates email CSS/layout, which fixes the reading-pane "bounce" and the mid-drag text-selection loss, closes the app-wide CSS-leak, and adds a default "external images blocked" tracking-pixel gate. Correctness/bugs: - Full-compose Reply/Reply-All now set In-Reply-To/References (unified DraftMessage type) so replies stay threaded; quick-reply threads References + parent id, HTML- escapes the body, and sends from the active shared/member mailbox. - Forward carries the original attachments; reply/forward resolve the full message (fetching body for list-row actions) instead of quoting whatever is open. - Race guards (latest-wins) on loadMessages / message select / thread load / conversation load; poll no longer yanks paged users to page 0; WS reload filtered to the active folder; searchPage resets on query change. - ThreadDetail expand state keyed by folder+uid (cross-folder collision); markRead only when unread; retry uses the folder prop. - Compose: flush typed-but-uncommitted recipient on Send/Draft; attachment dedupe + 40MB aggregate cap + functional updates; drop the redundant client OTK. - Proxy: forward Range + caching validators for attachment/inline streaming; close the multipart carve-out on guarded provisioning paths. - Email WebSocket: real stop after max retries, exponential backoff, stable-connection retry reset; memoized context value to cut consumer re-render churn. Quality: unified **************** a11y (aria-labels, aria-pressed, keyboard-operable rows/toggles); i18n (folder names, list dates, pagers, ICU plural); removed dead code and phantom keyboard-shortcut entries.

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