Changelog
What's new.
Every improvement to the platform, as it ships.
· kamo-shared-library
A domain is claimed by proving it, not by typing it
Three rules, and they only make sense together. **Naming a domain claims nothing.** An FQDN was globally unique here, so the first organization to enter "acme.com" locked out every other one — including the company that actually owns acme.com, which then had no way in at all. Someone who has proved nothing must not be able to deny a domain to someone who can. Duplicate rows across organizations are now legal; a repeat within ONE organization is still refused, because that is a mistake with no meaning rather than a competing claim. **Only a verified domain routes.** findByDomain now requires ownership_verified on the top-level row, so a host goes to the organization that proved control of its DNS rather than to whoever typed it first. Without this pairing the first rule would be an open door: claim somebank.com and wait. An alias inherits its parent's verification — the TXT challenge is only ever issued for the apex — and that is read off the explicit parent join, never as d.parent.ownershipVerified, which Hibernate emits as an inner join over the whole query and would drop every apex row. **Verifying takes the domain off everyone else.** setOwnershipVerified revokes the same FQDN wherever else it is held, so there is at most one holder at a time. This is not a courtesy: two verified rows are both routable, and the resolver would hand the traffic to whichever the database returned first. An organization that verified a domain last year and has since let it lapse would keep receiving the mail of whoever owns it now. The loser's DNS verification goes with it, because it describes a hostname that no longer points at them. Allowing duplicates then breaks every lookup that assumed one row, and one of those was not a crash but a hijack: upsertDomain matched on the domain NAME and then reassigned whatever it found to the calling organization, so an upsert would have quietly taken another organization's domain away from it. It is organization-scoped now. The rest — findByDomain, updateSslInfoByDomain, transferDomainOwnership — went through a single holderOf(), which prefers the verified row and otherwise takes the lowest id, so a domain's owner cannot change between two requests. Left as Optional bindings they would have thrown **************** on data the schema now permits.
· SecurityService
Carry the per-member-type home-screen defaults
Read on POST/PUT /security-models and PUT /master-model, and emitted from /applied-model's settings so the shell can decide where a member lands on "/". A separate applyHomeScreenDefaults call rather than three more positional arguments on a service method that already takes fifteen. Absent keys are left alone rather than reset, so a client that posts a security model without knowing about this field cannot silently hand every member of the model their launchpad back — or take it away. /applied-model normalizes on the way out as well as in. REQUIRES_CHILD_ORG_ OWNERSHIP withholds a home screen, so a null column on a row written before this shipped, or anything unrecognised, reads as the launchpad. The client's own fallback is the same value, so the two cannot disagree.
· kamo-shared-library
A security model says where each member type lands on "/"
Three columns on ChildOrgSecurityModel — DEFAULT_MEMBER_HOME_SCREEN, **************** DEFAULT_ORG_OWNER_HOME_SCREEN. Each holds HOME_SCREEN_DEFAULT or REQUIRES_CHILD_ORG_OWNERSHIP; the second withholds the launchpad until that member owns an organization of their own. Three rather than one because the three member types are three different jobs: the same model routinely wants a plain member pushed into creating their own organization while its team members and owners go straight to work. The split mirrors APPLIES_TO_MEMBERS / _TEAM_MEMBERS / _OWNERS exactly. VARCHAR, not an ordinal enum — an ordinal relabels every stored row the day a third option is inserted anywhere but the end. setHomeScreenDefaults is a targeted setter in the shape of setAllowRootSubdomains rather than three more positional parameters on updateModel, which already takes fifteen. Null means "leave this one alone", so a caller that knows about one of the three cannot silently reset the others. normalizeHomeScreenDefault fails OPEN and is tested for it: only the exact string turns the requirement on, because the alternative takes a member's home screen away and a typo or a stale client must never be able to do that by accident.
· InitializerService
The three per-member-type home-screen columns on a security model
CHILD_ORG_SECURITY_MODELS gains DEFAULT_MEMBER_HOME_SCREEN, DEFAULT_TEAM_MEMBER_HOME_SCREEN and DEFAULT_ORG_OWNER_HOME_SCREEN. Each holds HOME_SCREEN_DEFAULT (the launchpad) or REQUIRES_CHILD_ORG_OWNERSHIP, which withholds it until that member owns an organization of their own. The DEFAULT clause is the load-bearing part, not the ADD COLUMN. The columns are NOT NULL and the estate is not empty, so the default is what backfills every existing security model in the same statement — and it is deliberately the permissive value, because defaulting the other way would take the launchpad away from every member of every model the moment this ran. Order(0) for the usual reason: the shared-library entity carrying these fields is loaded by every Java service, so an earlier runner would die on "column does not exist" before a late-ordered migration was ever reached. The SET NOT NULL statements repair the case where Hibernate's ddl-auto created the columns first, from the entity's columnDefinition, without the constraint. Each is preceded by its own null-clearing UPDATE, because SET NOT NULL is refused while any row still holds a null — the test pins that ordering.
· kamo-internal
One list of what the scan found, beside what you are using
The brand section of /network/create becomes three bands with the same shape: what your website has on the left, what your organization is using on the right. Logos. There used to be TWO rails over the same candidates, one per slot, ranked differently — so a company with four logos appeared to have eight, and the question was put the wrong way round. A member does not look at a rail and ask "which of these is my full logo"; they look at their own mark and say "that one is the full one". So it is one list of the site's logos, each row carrying both destinations — Set as Full, Set as Simple — and each row marked Vector or Image, because that is the one thing about a logo file you cannot see by looking at it and the one thing the simple slot cares about. Marked, never filtered: hiding rasters from the simple slot hid the company's own logo on exactly the sites the scan worked best on. The list steps with up/down arrows and says how many there are, so "is my logo in here" is answerable by looking. Backgrounds are back. The twelve-step wizard collected sign-in wallpapers and the one-page rewrite dropped them; the scan was already finding a company's hero imagery and then offering nowhere to put it. Same band: scanned imagery on the left, upload and the running order on the right, with the watermark choice below once there is something to stamp. Colours are adopted rather than offered. A finished scan sets the pair it found, and the pickers stay live. The guards are the whole risk, so they are pure and tested in scanAutoApply.ts: once per scan result, and never over a colour the creator has touched — including for a later scan of a different site. Also here: - "Who this is for" is absent entirely, heading included, until there is something in it to answer. It was rendering an empty card before a type was picked, and again for models that settle every compliance question themselves. - The live preview shows the FULL logo, top left of the content area above the mock text, where a sign-in screen puts it. The bar above it is the navigation bar and shows the mark — and the two slots are easy to mix up, so the preview is the only place a creator can tell which one they filled. - "Compact logo" is "Simple logo" everywhere it is read, here and in the Getting Started wizard. Internal identifiers are left alone. - "Short Name" is "Organization Name (Short Version)". - Website says plainly that you do not need one, rather than leaving the absence of a red asterisk to carry the message.
· kamo-internal
A per-member-type default home screen
A security model now says where each of its three member types lands when they open "/". Two values: HOME_SCREEN_DEFAULT is the launchpad everybody has always had, and REQUIRES_CHILD_ORG_OWNERSHIP withholds it until that member owns an organization of their own — "/" goes to /network/create instead, and the organization-network link leaves the top bar until they have one. Three columns rather than one, mirroring APPLIES_TO_MEMBERS / _TEAM_MEMBERS / _OWNERS, because the same model routinely wants a plain member pushed into creating their own organization while its team members and owners go to work. The rule is pure and tested in app/lib/homeScreenGate.ts: which column applies, and what counts as owning an organization (an owner of the org they are already inside has not created one — the same exclusion FirstOrgCta makes). It fails OPEN at every step. Only the exact string closes the gate; a null column, an unrecognised value, a failed applied-model fetch and an unreachable my-networks all read as "no", because taking somebody's home screen away on the strength of a request that did not answer is not something to do. One context, mounted above the navs and the page, rather than the hook in both places: NavTop and the launchpad need the same answer on every page load, and two copies would cost two round trips and could disagree for the length of one. The launchpad deliberately does not wait on it — holding every member's home page behind an /applied-model fetch to spare a handful of them a brief flash is the wrong trade.
· kamo-internal
Create My Free Org becomes a three-step widget
The widget embedded the whole of CreateOrgForm — five sections, the brand preview, the readiness meter — which is roughly 1,400px inside a pane of about 756px. HomeGrid's rows are minmax(unit, auto), and that `auto` is a safety valve rather than a ceiling, so a pane whose content cannot shrink grows the row: the "widget" rendered as one long form down the page. It is now the fast path it was always described as. Three steps — type, name and address, review — asking only the fields that actually block a create and sending everything else exactly as toWizardFormData has always sent it for a creator who skipped those steps. Branding, apps and compliance are finished afterwards in the new org's Getting Started wizard. /network/create is unchanged and still asks for all of it up front. The type step is a SEARCH, not a card grid. In the widget's dense layout the grid is one card per row, so a tenant with thirty branch types is eighteen hundred pixels of cards — the stretch, again, and it gets worse as the platform grows. Search makes the step a fixed height at four types or three hundred. Also here: - WidgetFrame takes an optional maxHeightPx, and this widget passes its own paneHeightPx(halfRows), so the pane is the size the member gave it and scrolls rather than growing. Optional, so no other widget changes behaviour. WidgetComponentProps gains halfRows to make that possible. - The alias preview now reads KamoCRM.com/o/<alias> on both surfaces that show it, through one helper instead of each building the string itself. DISPLAY ONLY — routing is by hostname and there is no /o/ route, so workspaceUrl and /api/org/current are deliberately untouched and a launch still lands on internal.<host>. A custom domain still shows as itself. - The /branch-types/creatable fetch and its parsing move to one shared module and hook, since both type-pickers need it and it also carries the parent's subdomain-branch context. That settles a live duplication: OrgTypePicker built BranchTypeData twice and the two copies had already drifted on array coercion. - workspaceUrl moves out of CreateOrgForm so the widget can build a workspace link without importing the whole page form. - next/link for the "open the full page" jump. check-client-navigation missed the raw anchor because its regex needs <a and href on one line. Guards, tsc and 4,039 tests green; dictionary keys pushed first.
· kamo-internal
Getting Started card on the Settings landing page
Adds an essential-group card linking to /settings/wizard, matching the makeCard idiom used by its siblings. Dictionary keys already pushed ****************
· kamo-internal
A SEND TO AI switch on the report form
Right-aligned on the "What are you reporting?" row, off every time the window opens — it starts a session on a machine holding cluster credentials, and a switch that remembered its last position would eventually send something nobody meant to send. Submitting with it on hands the report straight over: In Progress, and read by the operator who sent it. Drawn only for a holder of MANAGE_SYSTEM_BUGS, read from the server-resolved PlatformRightsContext — checkRight is the TENANT rights spine and cannot answer for a platform right at all, so asking it would have hidden the switch from every platform administrator. That is chrome either way: SecurityService's endpoint sits under a class-level @RequirePlatformRight and refuses anybody else with a 403. The request carries a bug id and NOTHING ELSE. The form has no prompt box, no model picker and no screenshot picker, so the server chooses all of them; assembling a payload here would have put those choices in the browser, where the member holding the right could edit them. The hand-off runs after the uploads, not beside them — the dispatch reads whatever is attached at the moment it runs, and a session started early would be working from half a report. A dispatch that fails still applies its status, so the report is In Progress by the time the button is pressed again and a reporter may only edit one that is still New: the window remembers what it last wrote and skips the redundant re-save, or the retry would report that a saved report could not be saved and never re-attempt the thing that actually failed.
· SecurityService
Send To AI straight off the report form
A holder of MANAGE_SYSTEM_BUGS gets a SEND TO AI switch on the Report Bug window, and submitting with it on hands the report to a session without the triage dialog in between. The new endpoint takes NO BODY, and that is the point. The form has no prompt box, no model picker and no screenshot picker, so the instruction, the model, the effort, the screenshots and the status are every one of them chosen here — a caller past the gate cannot express anything the dialog could not, which makes the switch strictly narrower than the surface it stands in for. It then goes through dispatch() unchanged, so the throttle, the audit row written before the network call, and the screenshot ownership check are the same ones, not a second copy to keep in step. Guarded by the class-level **************** the dialog already sits under. The console hides the switch from everybody else, but that is chrome: a report submitted with the flag by a member without the right is refused with a 403 here and reaches no session. "Already viewed" is a per-member fact and cannot be written for everyone — PlatformRightsResolver answers "does this member hold it", never "who holds it". So the read is recorded for the operator who asked for the dispatch, before the network call rather than after it (it can take two minutes and it can fail), and IN_PROGRESS is what takes the report off every other manager's NEW badge.
· kamo-internal
Getting Started Configuration Wizard at /settings/wizard
Turns the nine moved wizard steps into an editor over the organization the member is already signed into. Each step now seeds its value from live org data (useOrgContext, GET /api/security/org/{id}, /api/security/features, or useAppliedModel as appropriate) and saves to the endpoint that already owns that setting on "Save & continue"; "Skip this step" records progress without writing. - orgSettingsSave.ts: per-step payload types (StepSavePayloads) and a generic saveStep(key, orgId, value) dispatch, typed without `never`/`any`. - orgDetails.ts: shared fetch for the org fields the thin theming Organization class does not carry (contact, labels, description, etc). - wizardSteps.tsx / StepRail.tsx / page.tsx: the step table, the always- clickable rail, and the shell (BrandScanProvider, inert during saves). - IdentityStep drops **************** controls — those are Settings -> Domains/Behavior & Labeling territory now. - BackgroundsStep tracks existing + new slots together, since /backgrounds/commit replaces the whole ordering and committing only new files would have deleted an organization's existing slideshow. - complianceWizard.ts: widened getComplianceStepUi's parameter to the five fields it actually reads, so the Compliance step can call it with useAppliedModel's settings instead of a create-time BranchTypeData. - api.ts: exported buildCustomLabels for reuse by the labels saver.
· kamo-internal
The /copilot hero and both scan codes carry the chameleon mark too
were the last two sites, and neither could just take the new src unchanged, because the artwork is a transparent 127x102 silhouette where the raster was an opaque square. CopilotHero. `objectFit: cover` would crop the snout off both sides of the square tile, so the mark is `contain`ed inside an inset wrapper — `fill` resolves against the padding box, so the inset cannot live on the plate itself. The plate also gains a translucent fill: it used to BE the icon, and an unfilled one leaves the border and the drop shadow outlining nothing. QrFrame, both codes. Two fixes: - The mark is white, and a QR code is printed on white. As shipped it was an invisible logo sitting in a hole punched out of the code. `brightness(0)` on the emitted <image> drives it to black and leaves the knockouts transparent. It has to be CSS — imageSettings renders an <image href>, and an external SVG referenced that way is an isolated document that inherits no colour. - 48x39, not 48x48. qrcode.react hardcodes preserveAspectRatio="none" on that element, so the box IS the aspect ratio and a square one squashed the mark. The narrower box also shrinks the excavated hole to ~2.9% of the code, under the 3.5% it was before and far inside what level M can lose and still scan. Verified by rendering QRCodeSVG with these exact props and rasterising it: the mark reads clearly at the centre and the excavation clears no finder pattern. Both codes, not just Install — they are one shared frame on one shared white plate, and QrFrame's own docs say to resist styling either call site individually.
· kamo-internal
Swap the mobile-app mark for the chameleon SVG in the rail and on home
raster. Both now draw the KamoMobileApp artwork as vector. Checked in as SVG because that is what the mark is: a single white silhouette with transparent knockouts. Passed through svgo at 2-decimal precision first, which is lossless for this trace's geometry but takes the file from 675 KB to 106 KB (~23 KB over the wire once compressed) — the export carried six decimals on every one of ~5k trace segments. Two things do NOT carry over from the raster: - The box. The viewBox is 127x102, so a square width/height squashes it. Each of the three call sites gets a 127:102 box sized to keep the glyph's optical height where the padded PNG's was. - next/image optimization. `unoptimized` on all three: /_next/image cannot resample vector, so without it the request is a round trip that hands back the same bytes. CopilotHero and the QR centre still use icon.png at their own sizes; the raster stays.
· kamo-internal
Create My Free Org widget
Ships the same CreateOrgForm the /network/create page renders, in its widget layout, as a home-launchpad widget (org.create) gated on CREATE_FREE_ORG_WIDGET. Registered last: it is a standing invitation, not a time-critical pane. No gate — creating an organization is not an app, and the five rights are the whole of the decision. Also mirrors CREATE_FREE_ORG_WIDGET and its four policy children into the hand-maintained TS rights hierarchy (205 -> 210 edges), which widgetPolicy.test.ts's five-right-shape assertions require for every registered widget.
· KlusterServices
Open Send To AI in a VS Code session tab, not a Konsole window
A dispatch now starts a BACKGROUND session and hands VS Code its session id, instead of running the CLI in a terminal window: Passing the prompt to the URI instead is a trap: the extension calls setInputText and never sends, and synthesising a Return is impossible while the KDE locker holds its X grab. A session that is ALREADY RUNNING has nothing left to submit, which is what makes the handoff work with nobody at the machine. Four things measured rather than reasoned about: - cwd must be realpath(workspaceFolders[0]), NOT the common parent this used before. The panel host roots itself at the first folder and lists only the sessions it finds there, so a session started anywhere else is invisible to the window it is meant to open in — the handoff opens an empty tab. The other repositories stay reachable through --add-dir instead. - --add-dir is VARIADIC and was eating the prompt, so sessions came up with an empty composer. The directory list now goes first, closed by the flags after it. - A launch can print no handle, or one marked "(idle — send a prompt to start)". Both mean the message never arrived, so both are stopped and retried rather than opened in front of the operator as an empty tab. - The panel cannot own a background agent, and a finished one stays resident, so a watcher stops it once the work is done. Reopening the tab then resumes the session normally, whole conversation intact. Confirmation is now the transcript addressed by session id, which removes the JSON-escaped-needle search that reported delivered dispatches as failures.
· kamo-internal
/network/create is one page; the twelve steps move to /settings/wizard
CreateOrgForm.tsx is one component with two layouts (page/widget), built from Task 7/8's CreateOrgFields model, field groups and OrgBrandPreview. page.tsx is now just the full-width shell around it. The old wizard's nine step files plus Stepper/stepIcons move to app/settings/wizard, where Task 12 rewires them into the Getting Started Configuration Wizard; BranchTypeStep, MemberInfoStep, ReviewStep, StepNavigation, CancelModal and the sessionStorage-backed useWizardForm hook are retired outright, since a one-page create has no steps to navigate between and nothing left to save a draft of. Every relative import in the moved files that pointed at ../ui/*, ../brandscan/*, ../../types, ../../validation, ../../appSelections, ../../complianceWizard or ../../hooks/* is rewritten to the absolute @/app/network/create/... form, since components/ui and components/brandscan stay put (the surviving field components still use them) and only resolve one directory up from the steps' old location. hooks/useWizardForm.ts is trimmed rather than deleted: __tests__/submitWizard.test.ts imports freshWizardData from it and must stay green and unedited, so the file keeps that one export (and the INITIAL_DATA it needs) and drops everything else that no longer has a caller. Two correctness fixes found in review before wiring the field components up: - CreateOrgForm's onChange is now referentially stable across renders (requestScan read through a ref instead of a useCallback dependency) — OrgTypePicker and IdentityFields each list onChange in a useEffect dependency array, and requestScan's own identity changes on every completed brand scan, which would have re-fired both effects on every scan rather than only on real input changes. - DropZone takes a real disabled prop (tabIndex=-1, guarded handlers, native disabled on its file input and fallback button) instead of relying only on an ancestor's pointer-events-none, which does nothing for keyboard activation; wired through BrandFields' logo slots so a locked form actually locks out keyboard users too. /network/create?fresh=1 becomes plain /network/create in FirstOrgCta.tsx and network/page.tsx: the one-page form starts from initialCreateOrgFields() on every mount rather than restoring a sessionStorage draft, so the param has nothing left to do.
· kamo-internal
Reusable field groups and the live brand preview
Extracts the inputs from BranchTypeStep, BasicsStep, LogoStep, ColorsStep, AppsStep and AgeRestrictionsStep into five controlled field components (OrgTypePicker, IdentityFields, BrandFields, AppSwitches, ComplianceFields) that both /network/create-as-one-page and the launchpad widget can render, leaving the step framing (header, skip button, navigation) behind. Adds OrgBrandPreview, a live brand miniature driven by previewPalette(), which pattern-checks any creator-authored colour before it reaches a style attribute and falls back to the platform CSS variables. IdentityFields and OrgTypePicker were adjusted mid-task for the **************** addition landing in parallel: OrgTypePicker now captures the parent org's subdomain-branch context alongside the branch type, and IdentityFields consults webAliasPossible() instead of re-deriving it, offering the web-alias path only when it is genuinely available. Task 8 of the **************** plan.
· kamo-internal
Getting Started Configuration Wizard button with an attention border
Adds the launchpad's entry point into the setup wizard: an owner-only, rights-gated pill that reads "Getting Started Configuration Wizard (x/9 steps complete)", placed first in the pill row ahead of My Widgets. Renders null until the org-owner + right + loaded-summary + not-finished gate resolves, so it never flashes for an owner whose organization already finished setup. The border carries a continuous conic-gradient sheen plus a breathing box-shadow in the organization's own --color-primary, and stops animating (while staying visibly two-tone) under prefers-reduced-motion.
· kamo-internal
Getting Started wizard step model and client
Adds the client-side step model (app/lib/orgSetupWizard.ts), a thin API client for GET/PUT/DELETE /api/security/org-setup-wizard (app/lib/orgSetupWizardApi.ts), and the useOrgSetupWizard hook that the wizard page and the launchpad button will both build on. Pure logic lives in app/lib rather than beside the page because app/settings/** and app/hooks/** are not in vitest's include allowlist. isWizardFinished is decided solely by the server's completedAt latch, not by counting completed steps, so the launchpad button stays gone for an organization that already finished even after the step list grows. normalizeSummary is total over unknown input and drops step keys the client does not recognize. useOrgSetupWizard keeps `loaded` separate from `summary` so the launchpad button renders nothing until the real answer is known.
· SecurityService
Add progress service and endpoints behind the Getting Started wizard
Org-scoped GET/PUT/DELETE under /api/security/org-setup-wizard track which of the nine Getting Started steps an organization has completed. A read never creates a row, and completedAt is a stored latch set once when the last outstanding step lands - it survives both un-completing a step and the step list growing later, so a finished organization's launchpad button never reappears.
· kamo-internal
The maximized pane zooms into the conversation you picked
Switching conversations in the maximized view was an 8px nudge and a 180ms fade — close enough to nothing that the pane looked like it had simply repainted. It now reads as arriving somewhere: the incoming conversation comes up from 6% under size with a short rise and lands in the pane, opaque early so it is readable while it is still moving, on a curve that spends most of its travel up front and then settles. The shape is dictated by the z-stack rather than chosen. Clicking a navigator row focuses the new window, so the incoming is painted OVER the outgoing, and that decides both halves: - The incoming may grow INTO the pane but never past it. The maximized shell is a fixed element the size of the viewport and nothing here pins the document's overflow, so an arrival that overshoots 1 paints outside the viewport. - The outgoing holds full size and only fades. While the incoming is still under-scale it does not cover the pane and the outgoing is the only thing behind it, so shrinking that too would open a rim of backdrop around a MAXIMIZED window — a flicker, not depth. It is free to fade because the incoming is opaque over it long before it is gone. That coverage rule is arithmetic, not taste, so it is a test: the outgoing fade must outlast the incoming's. Two more pin the constraints that are invisible until they break — the arrival never scales past 1, and it finishes inside the swap phase, which retires the role and would otherwise retarget it mid-flight. Reduced motion keeps the plain crossfade. Incidentally fixes a smaller thing: the arrival used to be 200ms inside a 180ms phase, so it was handed the settle target 20ms before it finished.
· kamo-internal
Three-column widgets and per-widget minimum sizes
Widens WidgetWidth to 1 | 2 | 3 | 'full' and lets a WidgetDefinition declare minWidth/minHeight. widgetMinSize(definition) converts that into a PaneSize floor (or null when nothing is declared), giving both axes a floor even when only one is declared — the un-declared axis floors at its own default size. Threads that floor from the registry through every consumer that sizes or resizes a pane: useHomePanes' sizeOf/setSize, PaneChrome's drag AND keyboard resize paths (all three clamp calls), SortableCard, the home page's sortable-grid widget render site, and the settings-page layout preview. The pinned-widget band in page.tsx is untouched on purpose — it always renders at widgetDefaultSize and never reads stored preferences, so a declared default already satisfies its own floor. No widget in the registry declares width 3 or a minimum yet; that is later work. This only widens the vocabulary and wires the machinery.
· kamo-internal
Touch up a traced logo by hand, and download the converted file
Four things on the raster-to-vector converter and the simple logo slot. **The scroll bar on the raster options is three times wider.** 4px was a hard target to land on, and the rules were webkit-only, so Firefox had never had a styled bar at all — scrollbarWidth/scrollbarColor now carry the same intent there. **A source that already has a transparent background is left alone.** The eyedropper used to happily return a colour for a transparent pixel: a canvas stores premultiplied alpha, so every one of them reads back as (0, 0, 0), and picking one told the pipeline the background was black. It then went and deleted the dark parts of the logo. Clicking transparency now says so instead of picking. The matching server-side fix is in vectorservice. **A wand and a brush, each able to add or remove.** Automatic removal gets two things wrong often enough to need arguing with: a background that leaks into an enclosed part of the design, and a piece of the design that shares the background's colour and goes with it. The wand takes a whole contiguous region in one click; the brush takes whatever is dragged over. Every operation resolves against the ORIGINAL crop, never the current cut-out. That is what makes "+" possible at all — once removal has zeroed a region's alpha there is no region left to find, but the original still has the colour. It also makes the ops replayable, so nudging a setting afterwards re-runs removal without discarding the corrections. Edits paint locally for instant feedback; only the settled result crosses the wire, and only the alpha mask, as an 8-bit grayscale PNG of a few hundred bytes. **A download button beside the simple logo**, so the member can keep the SVG the converter produced — often the only vector copy of their logo anyone has, and otherwise reachable only from a bucket they cannot browse. The extension comes from sniffing the bytes, not the stored name: that slot writes every upload to a fixed logo.svg key regardless of type, so theme.kamocrm.com serves PNG bytes from an .svg name for any org that set a logo before this converter existed. Fetched into a blob rather than linked, because a cross-origin `<a download>` is ignored and would open the file instead of saving it.
· kamo-internal
One-page field set and the defaults that replace the dropped steps
CreateOrgFields is what the new single-page /network/create form actually asks for: a branch type, name, alias, the app switches, and a handful of optional fields. toWizardFormData fills in everything the twelve-step wizard used to collect but this page never shows, using exactly what skipping each of those steps used to send (bindPhone/bindEmail true, blank contact/labels, watermark FULL, brand sources COMPANY) so an org created here is indistinguishable from one made the old way by someone who filled in only what was required. withBranchType also keeps parentAliasId pointed at an option the selected model actually offers (ROOT first when allowed, else the model's first custom alias) — the twelve-step wizard did this in a BasicsStep effect the creator never saw fire, and without an equivalent here createOrgReadiness blocks forever on a field this page never asks about for any branch type whose only parent option is ROOT.
· kamo-shared-library
Create My Free Org widget rights and Getting Started wizard progress
Adds RoleRightType 288-292 (CREATE_FREE_ORG_WIDGET and its four ADDED/OPTIONAL/ REQUIRED/MOVABLE policy children), hung off CREATE_CHILD_ORG (131). Adds OrgSetupWizardStep, the nine-step enum backing the Getting Started Configuration Wizard, and **************** mapping the org_setup_wizard_progress table from Task 2. Also updates RoleRightHierarchyTest's pinned taxonomy shape (278/205/35/38 -> 283/210/36/37): CREATE_CHILD_ORG was a standalone root and gains its first child here, moving it into the rootsWithChildren bucket, matching this file's own established convention of recording each widget shipment in that test.
· kamo-internal
Per-pane minimum size, applied on read as well as on drag
Threads an optional `min?: PaneSize | null` through clampPaneSize, the two resize-delta helpers, and the two preference read/write functions in the pure launchpad pane model. Backwards compatible: every existing caller omits the argument and keeps today's behaviour, since clampPaneSize falls back to the grid-wide **************** floor when no per-pane minimum is supplied. The floor is enforced on resolvePaneSize's STORED value, not only in the drag handlers, so a member's preference recorded before a pane had a minimum (or before it was raised) gets pulled back up on read.
· SecurityService
Put a real time budget on resend-verification
The endpoint had no limit of any kind. The *** the register screen demanded before enabling its resend button was never read here — the request body is three fields of JSON and nothing on the server looked for a payload — so the control gated a button, not the request, and a script posting straight at this URL was already unlimited. VerificationResendRateLimiter replaces it with a budget the server owns: a 60s cooldown taken as a single SET NX so racing replicas cannot both win, 5/hour and 15/day per address, and 20/hour per public IP. Checked BEFORE the org and user lookup and keyed on the SUBMITTED address, so a probe costs budget whether or not the account exists and the 429 does not become the account-existence oracle that the endpoint's careful "if this account exists" answer exists to avoid. The IP window is skipped for a non-public address: undetermined must not collapse into one shared key for every registrant. Redis-backed because this service runs replicas, and fails open on a Redis outage. A rejection answers 429 with Retry-After and retryAfterSeconds, so the screen can count down against this clock rather than guess at its own.
· InitializerService
Create org_setup_wizard_progress for the Getting Started wizard
· kamo-internal
The org's year established is optional, and unset renders as absent
Nothing on the server ever required it — the column is nullable, the create parser maps an empty value to null, and patchOrganization applies only the keys a PUT body mentions, so "yearEst": null is a deliberate clear. The only gate was this wizard's validateBasics, which blocked the whole Basics step until a year was picked. Plenty of real customers have no answer (a new LLC, a division spun out mid-year, anyone who simply does not know), so the gate just taught them to invent one. Drop it; the field keeps its asterisk-free label and gains the same "Optional." FieldHint every other optional field on the step already has. Fixes the reason nobody noticed the field was already nullable end to end: the org DTO parsed yearEst with a bare **************** and coercion runs BEFORE optionality, so Number(null) === 0 and every org without a year arrived as founded in the year zero. It stayed invisible because display sites guard on truthiness and 0 is falsy — it would have surfaced as "Est. 0" at the first site that checked `!= null` instead. Preprocess null and "" back to undefined, normalize the same way in Organization.fromJSON, and widen the network/theme DTOs to admit the null that actually arrives. The two display sites that used `{org.yearEst && <Fact/>}` become ternaries: a falsy-and render is what would have painted a bare 0 into the drawer. The settings form now routes the save through parseYearEst, so a blank box clears the value on purpose rather than by way of JSON.stringify turning NaN into null.
· kamo-internal
Show a member only the tabs that are theirs to see
Three member-settings surfaces were open to people they cannot mean anything for. Each gate is on the MEMBER TYPE or the org's apps, never on checkRight — checkRight returns true for every right under active god mode, and the page's canViewOtherMemberAccount ORs in rights that have nothing to do with the subject **************** among them). getMemberType() defaults to 'MEMBER', so every one of them fails closed on a session whose type has not resolved yet. Plans & Billing: team members only. The plan, the subscriptions, the invoices, the payment methods and the seat all belong to the business; a plain member holds none of them, so the tab was showing them the company's money. Gated on the VIEWER, so an owner opening someone else's page keeps the administrative path. The /settings card is gated the same way — a card that opens a tab which will not render is worse than no card, exactly as Commissions beside it. Ringtones & Alerts: gated on the same condition as NavTop's phone icon, which is now one function (hasPhoneAccess) rather than two copies of the same &&. The subtab is nothing but the configuration of the phone that icon opens — which ringtone, which device, how loud — so a member with no phone was being handed controls that cannot change anything they will ever hear. Audio, Video and Event Sounds are untouched: those are the meeting devices and the app's own notification sounds, which every member has. Legal & Compliance: team members, and an org with both the Docs and HR apps. Rather than inline the new conditions, they fold into legalBadgeAppliesTo, which NavTop's signature button and the count provider behind its badge already share. That button is a link straight to this tab, so a fourth copy of the condition would let it outlive its own destination and drop the member on their profile with no explanation; the provider goes quiet on the same condition instead of holding a STOMP subscription and a 60-second poll for a count that is 0 by construction.

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