Mabadiliko
Nini kipya.
Kila kuboresha jukwaa, kama ni meli.
· KBService
Lock the shared sweeps, and run two pods
Every @Scheduled sweep here whose effect is shared now takes a named distributed lease through SingletonTaskRunner before it does anything, so it runs once across the fleet instead of once per pod. That was the only thing keeping this service at one replica — there is no ShedLock anywhere on this platform, so the replica count was what made the sweeps correct. @Transactional and friends stay on the OUTER method where present: Spring applies them with a proxy, and the inner method is reached through a this:: reference, which never goes through one. Moving them inward would leave the sweep silently non-transactional. replicas 1 -> 2, with a PodDisruptionBudget in KlusterServices so a node drain waits.
· ESigService
Lock the shared sweeps, and run two pods
Every @Scheduled sweep here whose effect is shared now takes a named distributed lease through SingletonTaskRunner before it does anything, so it runs once across the fleet instead of once per pod. That was the only thing keeping this service at one replica — there is no ShedLock anywhere on this platform, so the replica count was what made the sweeps correct. @Transactional and friends stay on the OUTER method where present: Spring applies them with a proxy, and the inner method is reached through a this:: reference, which never goes through one. Moving them inward would leave the sweep silently non-transactional. replicas 1 -> 2, with a PodDisruptionBudget in KlusterServices so a node drain waits.
· AIService
Lock the shared sweeps, and run two pods
Every @Scheduled sweep here whose effect is shared now takes a named distributed lease through SingletonTaskRunner before it does anything, so it runs once across the fleet instead of once per pod. That was the only thing keeping this service at one replica — there is no ShedLock anywhere on this platform, so the replica count was what made the sweeps correct. @Transactional and friends stay on the OUTER method where present: Spring applies them with a proxy, and the inner method is reached through a this:: reference, which never goes through one. Moving them inward would leave the sweep silently non-transactional. replicas 1 -> 2, with a PodDisruptionBudget in KlusterServices so a node drain waits.
· kamo-internal
coder sessions beside the running shells, and a scrollbar that reaches both ends
## Terminals is now two views of one machine Sub-tabs rather than two top-level tabs, because they are not two destinations: a terminal is a shell that may be running a session, a session is a conversation that may have a shell. The switch is a segmented control, not a second MUI Tabs strip — two tab bars stacked read as chrome, a segmented rail reads as "one of these two", which is what it is. Each half carries its own create, so a single `+` up in the navigator header (which could only ever mean one of them) is now shown only while collapsed, where neither row fits. TerminalsWorkbench is shared by the Interaction Center and the maximized navigator, exactly as TerminalsPane already was, so the two cannot drift. It takes the terminal list as a prop rather than polling again — both surfaces already hold it. ## Two verbs, and which one appears IS the state A session already running in a terminal says **Open** and raises that window. One that is not says **Resume** and opens a terminal that picks it back up. Never both, and never a disabled row. Resuming a session that is already attached gives one conversation two heads: both CLIs write to the same transcript and the second opens by replaying what the first is in the middle of. The rule is one function, `terminalForSession`, so nothing can answer it twice and differently. the machine leaves a receipt the moment it prepares one — and says so rather than offering to resume something that has not been written yet. Sessions are grouped by day. Four hundred of them in one flat column is a wall; the same four hundred under Today / Yesterday / Earlier this week / Before that is a list you can find yesterday afternoon in. The list keeps the surrounding rules: the same row geometry and 3px rail as the terminal list, and the same typographic split — what a person or the model wrote is set in the interface's sans, what the machine reports is set in the emulator's mono. Nothing here can ask for a permission mode. Every session starts with permissions bypassed and that is decided on the dev machine; the request names which session and nothing else. A test pins that the client cannot grow such a parameter. ## The terminal's own scrollbar xterm draws the browser's grey bar on its viewport, which can only offer "a bit up" and "a bit down". In a terminal the two ends are not two more positions — they are the two places you actually want, the start of what you ran and the live prompt — so each gets a button: jump-to-start above the up arrow, jump-to-end below the down arrow, with press-and-hold repeat on the arrows, click-to-page on the track and a draggable thumb. xterm's own bar is hidden, not restyled; the viewport still scrolls, so wheel, trackpad and touch are unchanged. The jump-to-end button lights when output has arrived below the fold. A terminal that has stopped following looks exactly like one that has finished, and that is the only thing the bar knows that the reader cannot see. ## Verification The full gate: 14 source guards and 5322 tests, plus tsc. The new UI was also rendered in a browser from a local harness and read rather than assumed — which caught a transparent verb chip showing the truncated title through the word "Resume", and an empty state that flashed over a machine with four hundred sessions because `loading` is still false on the render before the effect runs. axe-core reports zero WCAG AA violations across the new components. The workbench carries its own MotionConfig: the Interaction Center wraps its panel in one, the navigator does not, and framer-motion outside one animates straight through prefers-reduced-motion without a word. Dictionary keys landed first, as CI requires.
· SecurityService
List a member's coder sessions, and open one in a terminal
Two endpoints beside the terminal ones, and they follow the same rule those do: the Linux account is resolved from the CALLER'S OWN SESSION and no account name is ever taken from the request. A member can therefore only see or reopen their own sessions — there is no id they could supply that would reach somebody else's, because the account is not part of what they supply. unreachable dev machine comes back 200 with `unreachable: true` and an empty list, as the terminal listing does: "the machine is down" is something the tab can say, where an error leaves it able to say nothing. new session or resumes an existing one. Nothing runs when the POST returns. The machine writes the session down and hands back a receipt; the terminal window the console opens next presents its ordinary ticket, and the receipt is what turns that terminal into this session — the same hand-off Send-to-AI uses, through the same TerminalDispatchRegistry. The window id is named HERE because both halves need it: the console to open the window, the registry to key the receipt, and tmux to survive the reconnects it exists to allow. This endpoint cannot express a command, a model, an effort or a permission mode. Every one of those is the machine's to decide. What crosses the wire is which session, and nothing else — so `bypassPermissions`, which every one of these sessions runs with, is not a parameter here and cannot become one. TerminalSummary grows `coderSessionId`: the session a shell is running, read off the tmux user option the agent sets. It is what lets the console say "already open" instead of resuming a conversation that is live in another there is nowhere else this could come from. Opening one is audited exactly as opening a shell is — same event type, same HIGH severity — because it is one. Verified with `mvn test`; Java CI runs none, so a red repo would still deploy.
· KlusterServices
Measure whether a deploy actually costs anyone a request
Every fix in this programme is a claim about what happens during a rollout. Until now those claims were untested: MediaService shipped with strategy Recreate at one replica for its entire life, so every deploy deleted the only pod serving chat and attachments and started the replacement afterwards — a ~65 second hole, several times a day — and nothing measured it. The first anyone knew was a member reporting a failed document upload, and even that read as a flaky feature rather than as chat being down on a schedule. A blackbox exporter now loads the eight public hosts every 10 seconds. Ten, not the 30s default: the failure this exists to catch is short by design, and at 30s a clean graph would only mean the hole fell between two scrapes. Two alerts matter, and the second is the more useful: PublicHostDown a host has not answered for a minute. An outage, deploy or not. PublicHostFlapping below 100% success over ten minutes. This is the one that catches a rollout dropping a handful of requests — it recovers long before a 1m 'for' elapses, which is precisely why that failure gets reported as 'it randomly logged me out' and never as an incident. Plus a certificate alert (the cold-Traefik self-signed failure had no detector at all), a stuck rollout alert, and one for a Deployment sitting below its desired replica count — because replicas: 2 only helps while the second pod is actually up, and one that is not looks healthy. TLS verification is deliberately on: a certificate this platform serves wrongly must fail the probe, since that is exactly what a cold Traefik used to do to every org at once. monitoring/ is not a CI apply target by long-standing convention here, so both files carry their own apply line.
· KlusterServices
coder sessions on the dev machine, listed and resumable
is here, because the machine is the only thing that knows any of it. ## The session index home, exactly as their terminals are whatever tmux is running. No table, for the same reason: the disk already knows, it is what the CLI itself resumes from, and Memoized on (mtime, size), and it has to be — the machine this was built against holds 444 sessions and 1.1 GB of transcript, one of them 205 MB. Only the first and last 128 KB of each file are read: the working directory, the branch and the opening prompt are in the first records, the generated title and the summary are appended as the session goes on, and the middle is tool calls. Measured against the real store: 2.8s cold, 17ms warm. ## Two things a transcript cannot say A transcript does not record that the console started it, so a session started without it the row a member just created is missing from the list they are looking at, which reads as the button having done nothing. And a transcript does not record where it is being driven from, so a shell that is running one now carries the session id as a tmux user option (@kamo_coder), beside the title that already lives there. That is the whole duplicate rule: resuming a session that is already attached gives one conversation two heads, with both CLIs writing to one transcript and the second replaying what the first is in the middle of. The listing therefore grows a field, so `list-sessions -F` now asks for seven — the title stays LAST and the split stays bounded, because the separator is printable and a member can type it. ## The caller still never supplies a command The command line is composed here: the CLI's path resolved under the account's own home, the session id minted or found on this disk, and `--permission-mode bypassPermissions`, which is not a parameter and cannot be one. Prepared records now carry a `kind`, and one that predates the field is a Send-to-AI hand-off, which is the only kind there was. The working directory is the one thing a request can influence and is bounded by realpath to the account's own home. That is not a privilege boundary — the terminal already runs as that account — it keeps the set of directories a REQUEST can name smaller than the set a person can reach. A record that names an account is also only redeemable by that account's terminal. 40 new tests, 199 in the suite, all passing. The index is verified against the real 444-session store rather than fixtures alone. Ships by install-agent.sh, not by CI: pushing this deploys nothing to the VM.
· KlusterServices
Budget every drain, and fail a deploy that regresses rollout safety
PodDisruptionBudgets for the 25 Deployments that now genuinely run two replicas. minAvailable: 1 lets a drain evict one pod and wait for its replacement instead of taking a whole node's worth down at once. The kamo namespace had ZERO of these while cluster-services, kamo-universe, ratestack, longhorn-system and kubevirt all had them — the namespace holding production was the one without. Only Deployments actually at two replicas appear. A PDB on a single-replica Deployment is worse than none: minAvailable: 1 against replicas: 1 permits zero disruptions, so it does not protect the workload, it blocks node maintenance forever. README.md says so, and names the deliberate singletons that must never be listed. scripts/audit-deploy-safety.sh fails the deploy if a Deployment in kamo has drifted back to a shape that drops requests during a rollout: no readiness probe, no preStop hook, no minReadySeconds, Recreate without an opt-out, or two replicas with nothing spreading them across nodes. It audits the CLUSTER rather than the manifests. Those settings live in 48 separate repositories with 48 workflows; a per-repo guard would catch a regression earlier, but it is 48 copies to keep in step and any repo that never adopts it is silently exempt. Every one of those manifests ends up here. The trade is that it is a smoke alarm rather than a lock — it reports after the fact, so it prints the whole table and names what drifted rather than just failing. Exemptions are annotations on the Deployment itself, and a reason is mandatory: an exemption without one is a failure. That is what lets the next person tell a deliberate singleton (kamodaemon binds exclusive NATS durables) from something nobody got round to.
· RAGService
Share the indexing consumers, and run two pods
RAGService binds two JetStream durables of its own — rag-note-consumer and rag-kb-consumer — and a durable push consumer with no deliver group admits exactly ONE subscriber. A second pod would have been refused [SUB-90012], leaving the indexing pinned to whichever pod bound first with nothing taking over when that pod is the one that dies. Both now join a deliver group, so NATS hands each event to exactly one pod. That is what indexing needs: once, not once per pod, and not once per pod that happened to win a race. replicas 1 -> 2. This service runs no @Scheduled work and binds no exclusive NATS durable, so a second pod duplicates nothing — it is stateless request serving, and the only thing keeping it at one replica was that nothing had raised the number. At one replica maxSurge hides the exposure during a clean deploy and nowhere else: an OOM kill, a failed liveness probe or a node drain took the capability out entirely for the 59-79 seconds a Spring context takes to boot. A PodDisruptionBudget in KlusterServices now makes a drain wait.
· VectorService
Run two pods
replicas 1 -> 2. This service runs no @Scheduled work and binds no exclusive NATS durable, so a second pod duplicates nothing — it is stateless request serving, and the only thing keeping it at one replica was that nothing had raised the number. At one replica maxSurge hides the exposure during a clean deploy and nowhere else: an OOM kill, a failed liveness probe or a node drain took the capability out entirely for the 59-79 seconds a Spring context takes to boot. A PodDisruptionBudget in KlusterServices now makes a drain wait.
· ConversionService
Run two pods
replicas 1 -> 2. This service runs no @Scheduled work and binds no exclusive NATS durable, so a second pod duplicates nothing — it is stateless request serving, and the only thing keeping it at one replica was that nothing had raised the number. At one replica maxSurge hides the exposure during a clean deploy and nowhere else: an OOM kill, a failed liveness probe or a node drain took the capability out entirely for the 59-79 seconds a Spring context takes to boot. A PodDisruptionBudget in KlusterServices now makes a drain wait.
· APIService
Run two pods
replicas 1 -> 2. This service runs no @Scheduled work and binds no exclusive NATS durable, so a second pod duplicates nothing — it is stateless request serving, and the only thing keeping it at one replica was that nothing had raised the number. At one replica maxSurge hides the exposure during a clean deploy and nowhere else: an OOM kill, a failed liveness probe or a node drain took the capability out entirely for the 59-79 seconds a Spring context takes to boot. A PodDisruptionBudget in KlusterServices now makes a drain wait.
· kamo-internal
Answer a readiness probe, and run two pods
This deployment had no readiness probe, so a pod counted as Ready the instant its container process started. With maxUnavailable 0 Kubernetes reads that as "the new pod is serving" and retires the old one — while Next is still initialising and has not bound its port. Requests landed on a port nothing was listening on, which is where the intermittent 502s on deploy came from. /api/health answers only for this pod and deliberately touches no backend: a readiness probe decides whether this pod leaves the Service, and wiring it to a backend turns a backend blip into a rolling restart of every pod here. The drain and the 660s grace period were already here; the missing probe is what stopped them counting. Kubernetes was retiring the draining pod before the replacement could serve. replicas 1 -> 2. Server-side state lives in Redis, not in the pod, and there is no scheduled work to duplicate, so a second replica changes nothing except that losing one pod stops being an outage. At one replica an OOM kill, a failed liveness probe or a node drain took the whole thing down for as long as it takes to boot. topologySpreadConstraints (added earlier, inert until now) keep the two on different nodes where the cluster can manage it, and a PodDisruptionBudget in KlusterServices makes a drain wait.
· kamo-marketing
Drain on SIGTERM, and run two pods
Next handles SIGTERM with a bare process.exit(143), so every deploy severed whatever this pod had in flight — a form post, a server action, a streamed RSC payload, an upload. Invisible when a request lasted milliseconds; not invisible on a cluster that deploys on every push. scripts/standalone-entry.cjs wraps the standalone server: on SIGTERM it stops accepting, drops idle keep-alives at once so a pod with nothing in flight still exits in about a second, and lets running requests finish under a cap that sits below terminationGracePeriodSeconds. Ported from kamo-internal, which has run it in production for months. The wrapper starts server-nonce.js rather than server.js, through NEXT_SERVER_ENTRY — the CSP nonce server stays exactly where it was in the chain. replicas 1 -> 2. Server-side state lives in Redis, not in the pod, and there is no scheduled work to duplicate, so a second replica changes nothing except that losing one pod stops being an outage. At one replica an OOM kill, a failed liveness probe or a node drain took the whole thing down for as long as it takes to boot. topologySpreadConstraints (added earlier, inert until now) keep the two on different nodes where the cluster can manage it, and a PodDisruptionBudget in KlusterServices makes a drain wait.
· kamo-capcha
Run two pods
replicas 1 -> 2. Server-side state lives in Redis, not in the pod, and there is no scheduled work to duplicate, so a second replica changes nothing except that losing one pod stops being an outage. At one replica an OOM kill, a failed liveness probe or a node drain took the whole thing down for as long as it takes to boot. topologySpreadConstraints (added earlier, inert until now) keep the two on different nodes where the cluster can manage it, and a PodDisruptionBudget in KlusterServices makes a drain wait.
· kamo-register
Drain on SIGTERM, answer a readiness probe, and run two pods
Next handles SIGTERM with a bare process.exit(143), so every deploy severed whatever this pod had in flight — a form post, a server action, a streamed RSC payload, an upload. Invisible when a request lasted milliseconds; not invisible on a cluster that deploys on every push. scripts/standalone-entry.cjs wraps the standalone server: on SIGTERM it stops accepting, drops idle keep-alives at once so a pod with nothing in flight still exits in about a second, and lets running requests finish under a cap that sits below terminationGracePeriodSeconds. Ported from kamo-internal, which has run it in production for months. This deployment had no readiness probe, so a pod counted as Ready the instant its container process started. With maxUnavailable 0 Kubernetes reads that as "the new pod is serving" and retires the old one — while Next is still initialising and has not bound its port. Requests landed on a port nothing was listening on, which is where the intermittent 502s on deploy came from. /api/health answers only for this pod and deliberately touches no backend: a readiness probe decides whether this pod leaves the Service, and wiring it to a backend turns a backend blip into a rolling restart of every pod here. The Dockerfile HEALTHCHECK has pointed at /api/health since it was written; the route did not exist, so that check has been failing for as long as it has been there. replicas 1 -> 2. Server-side state lives in Redis, not in the pod, and there is no scheduled work to duplicate, so a second replica changes nothing except that losing one pod stops being an outage. At one replica an OOM kill, a failed liveness probe or a node drain took the whole thing down for as long as it takes to boot. topologySpreadConstraints (added earlier, inert until now) keep the two on different nodes where the cluster can manage it, and a PodDisruptionBudget in KlusterServices makes a drain wait.
· kamo-login
Drain on SIGTERM, answer a readiness probe, and run two pods
Next handles SIGTERM with a bare process.exit(143), so every deploy severed whatever this pod had in flight — a form post, a server action, a streamed RSC payload, an upload. Invisible when a request lasted milliseconds; not invisible on a cluster that deploys on every push. scripts/standalone-entry.cjs wraps the standalone server: on SIGTERM it stops accepting, drops idle keep-alives at once so a pod with nothing in flight still exits in about a second, and lets running requests finish under a cap that sits below terminationGracePeriodSeconds. Ported from kamo-internal, which has run it in production for months. This deployment had no readiness probe, so a pod counted as Ready the instant its container process started. With maxUnavailable 0 Kubernetes reads that as "the new pod is serving" and retires the old one — while Next is still initialising and has not bound its port. Requests landed on a port nothing was listening on, which is where the intermittent 502s on deploy came from. /api/health answers only for this pod and deliberately touches no backend: a readiness probe decides whether this pod leaves the Service, and wiring it to a backend turns a backend blip into a rolling restart of every pod here. replicas 1 -> 2. Server-side state lives in Redis, not in the pod, and there is no scheduled work to duplicate, so a second replica changes nothing except that losing one pod stops being an outage. At one replica an OOM kill, a failed liveness probe or a node drain took the whole thing down for as long as it takes to boot. topologySpreadConstraints (added earlier, inert until now) keep the two on different nodes where the cluster can manage it, and a PodDisruptionBudget in KlusterServices makes a drain wait.
· kamo-nowww
Drain on SIGTERM, and answer a readiness probe
Next handles SIGTERM with a bare process.exit(143), so every deploy severed whatever this pod had in flight — a form post, a server action, a streamed RSC payload, an upload. Invisible when a request lasted milliseconds; not invisible on a cluster that deploys on every push. scripts/standalone-entry.cjs wraps the standalone server: on SIGTERM it stops accepting, drops idle keep-alives at once so a pod with nothing in flight still exits in about a second, and lets running requests finish under a cap that sits below terminationGracePeriodSeconds. Ported from kamo-internal, which has run it in production for months. This deployment had no readiness probe, so a pod counted as Ready the instant its container process started. With maxUnavailable 0 Kubernetes reads that as "the new pod is serving" and retires the old one — while Next is still initialising and has not bound its port. Requests landed on a port nothing was listening on, which is where the intermittent 502s on deploy came from. /api/health answers only for this pod and deliberately touches no backend: a readiness probe decides whether this pod leaves the Service, and wiring it to a backend turns a backend blip into a rolling restart of every pod here. Already at two replicas; this is what makes the second one actually cover the first during a rollout rather than both being replaced blind.
· kamo-shared-library
A distributed lock, so a scheduled sweep can survive a second replica
There is no ShedLock anywhere on this platform and there never has been. All 97 @Scheduled sweeps across 18 services are correct only because each service runs exactly one pod. Scale any of them and the sweep runs twice a tick — two meeting reminders in a member's inbox, two chat-email notices, two passes over a billing period. Those single-replica deployments were not merely un-scaled; the replica count was load bearing, and the code says so in three places already. **************** lease, body) runs a body on exactly one pod. AN EXPLICIT CALL, NOT AN ANNOTATION. @SingletonTask beside @Scheduled is the obvious design and it is a trap here: it would be applied by an AOP proxy, and a proxy is bypassed entirely when a method is reached from inside the same bean — the Spring self-invocation problem this codebase has been caught by five separate times. A sweep that silently lost its lock and ran on every pod is that same failure, and nothing would reveal it until a customer counted their emails. Taking the lock by calling it cannot be bypassed by how the method was reached, and it greps. REDIS, NOT THE DATABASE. A lock table needs DDL, and DDL on this cluster makes every service answer 500 'schema version mismatch' for ~15 minutes while Hikari connections age out. Paying a platform-wide outage to install the machinery that prevents platform-wide outages is a bad trade when Redis is already deployed. FAILS CLOSED. If Redis is unreachable the task is skipped, not run: for work that sends email or moves money, running twice is worse than not running, and the next tick retries. Release is a compare-and-delete script so a body that overran its lease cannot delete a lock another pod has since taken, and overrunning is logged at ERROR because nothing else would reveal it. Where a service has no Redis the fallback runs the body always — exactly what the code does today, so adopting this never changes behaviour on its own — and warns at startup. The manifest guard in CI refuses replicas > 1 for a service in that state. 7 tests; full suite 2710 green.
· kamo-register
Paint every logo on the sign-up site in the org's overlay colour
The org chooses "Register Site Logo Overlay" in kamo-internal beside the home page and loading screen overlays. One setting for the whole site: every step of the registration flow, the organization and team screens, the completion screen and the closed-signup screen. So ThemeLogo takes no prop for it and reads the colour itself, the way it already reads the company-vs-software auth logo identity - config.json is this site's only source of org branding, and a prop would be one more thing every call site has to remember. The colour arrives already resolved: kamo-internal stores a TOKEN so an org that rebrands keeps a matching overlay, and securityservice resolves it against that org's palette when it writes config.json. Nothing here needs the thirteen-token vocabulary. A value CSS cannot parse is dropped rather than passed through - an invalid background-color paints the masked box transparent, which reads as a logo that vanished. A mask, not a filter: a filter can only push a logo's existing colours around and cannot reach an arbitrary target. The <img> stays mounted and merely invisible so it keeps holding its exact box for the mask to fill, leaving every caller's layout unchanged. Its own column and its own config.json key, separate from the sign-in site's: the two are branded independently, and an org that white-labels one need not white-label the other.
· kamo-login
Paint every logo on the sign-in site in the org's overlay colour
The org chooses "Login Site Logo Overlay" in kamo-internal beside the home page and loading screen overlays. One setting for the whole site: the sign-in form, the workspace picker, password recovery and reset, and the sign-out screen are the same logo on the same background, and nobody wants to set the shade five times. So ThemeLogo takes no prop for it and reads the colour itself, the way it already reads the company-vs-software auth logo identity - config.json is this site's only source of org branding, and a prop would be one more thing every call site has to remember. The colour arrives already resolved: kamo-internal stores a TOKEN so an org that rebrands keeps a matching overlay, and securityservice resolves it against that org's palette when it writes config.json. Nothing here needs the thirteen-token vocabulary. A value CSS cannot parse is dropped rather than passed through - an invalid background-color paints the masked box transparent, which reads as a logo that vanished. A mask, not a filter: a filter can only push a logo's existing colours around and cannot reach an arbitrary target. The <img> stays mounted and merely invisible so it still fires onLoad (the only source of naturalSize) and onError (which walks the logo-full extension fallbacks), and so it still holds its exact box for the mask to fill. Every caller's layout is unchanged. The small animated mark in the "Powered by KamoCRM" pill is the org's own logo too, so it follows the setting - it just renders through next/image for that pill's eager load, so it carries the mask itself rather than becoming a ThemeLogo. It would otherwise have been the one logo on the site the setting missed.
· kamo-internal
Choose a logo overlay for the sign-in and sign-up sites
A fourth and fifth surface beside the home page, loading screen and KamoMeets overlays, sharing their thirteen-token vocabulary and their live swatches. Both are always offered, unlike the KamoMeets one: every org has a sign-in and a sign-up site whatever apps it runs, and an org with no domain of its own still reaches them on login./register.kamocrm.com painted in its own branding. They are also the only two overlays here that do not apply the moment they are saved. kamo-login and kamo-register have no OrgContext to read, so securityservice resolves the token and bakes the colour into the theme config.json those sites fetch; the change lands when provisioning rewrites that file, which the same save already triggers. Five near-identical dropdowns would have been five places to drift, so the editor block is now one renderOverlaySelect() the surfaces share - a sixth surface is one call rather than another twenty lines. The read-only rows already shared theirs. The two fields ride Organization.fromJSON like their siblings, with a round-trip test: the mapper runs twice on the way to the browser, and a field it cannot read back from its own output is a setting that silently reverts on its next load.
· MediaService
Make MediaService correct on more than one pod, and run two
MediaService has only ever run a single replica, so a deploy of it was a total chat outage and an OOM was the same. It could not run two, for reasons that were all in the code: 1. THE STOMP BROKER IS IN-HEAP. enableSimpleBroker means convertAndSend reaches only the WebSockets connected to the pod making the call. Thirteen relay controllers already handled this correctly by subscribing to NATS with a plain dispatcher (no queue group, so every pod receives every message) and re-publishing locally. Thirteen other call sites did not — typing indicators, presence, unread badges, reply grants, WebRTC offer/answer/ICE, incoming-chat notifications — and each would have delivered to roughly half its intended audience. StompFanout generalises the pattern those relays already use: publish {destination, payload} on one core NATS subject, every pod relays it into its own broker. Core NATS, not JetStream, because these are live-moment events with no value replayed and no stream covering the subject. Because core NATS also delivers to the publisher, send() does NOT write locally as well — that would deliver twice here. With NATS down it falls back to a local send, which is what the platform did before. 2. THE PER-CONVERSATION CONSUMER WAS AN EXCLUSIVE DURABLE. ChatSessionSubscriptionManager named its JetStream durable after the chat session GUID, and a durable push consumer admits exactly one subscriber. The second pod's bind was refused [SUB-90012] and every member whose socket landed there received nothing — no messages, no read receipts, no member-added events, no error. Now an ephemeral consumer: no name to collide over, one per pod, reaped by the server. The retry this removes invented a fresh durable name on collision. It worked, and it leaked a permanent server-side consumer per collision that nothing ever deleted. 3. FIVE MORE FIXED-NAME DURABLES, needing opposite treatment depending on the job. The conversion and VOIP STOMP relays went through JetStream rather than a core dispatcher, so they had the same exclusivity bug — now ephemeral, since every pod must receive. The chat-email notifier, social inbound consumer and marketing conversion consumer must run exactly ONCE, so they keep their durable and join a deliver group, which also means a survivor picks up the work when a pod dies. Sticky sessions on the media route are required, not optional: these STOMP clients use SockJS, whose xhr-streaming/xhr-polling fallback is several HTTP requests standing in for one connection against session state that lives in one pod's memory. Round-robin breaks it. See ingressroute.yaml. replicas 1 -> 2. 367 tests pass.
· SecurityService
Bake the sign-in and sign-up logo overlays into config.json
kamo-login and kamo-register run on their own hosts with no OrgContext, so this file is the only route their branding takes - the same one KamoMeet already uses. The token is resolved against the org's palette HERE and the finished hex is written, which keeps the thirteen-token vocabulary out of two more codebases that would each need their own copy of it. Baking a resolved colour is not the mistake the stored token avoids: this file is regenerated on every branding save, colour changes included, so the value cannot go stale. Absent means no overlay, so an org that has chosen none gains no key and its logos render untouched - which is every org until one is chosen. Tests cover the three surfaces staying apart: they share one builder, and an org overlaying its sign-in site must not have its KamoMeet logo painted as a side effect.
· kamo-shared-library
A logo overlay for the sign-in and sign-up sites
Two more columns in the vocabulary home_logo_overlay already established: a token, not a hex, so an org that rebrands keeps an overlay matching its new colours. Both nullable with no backfill - null is NONE, which is what every org looks like today. Separate columns rather than one shared setting, for the same reason the home page and loading screen have their own: the two sites are branded independently, and an org that white-labels one need not white-label the other. patchOrganization is an ALLOW-LIST, so the names go there too. A column this service happily deserializes but that method does not name is accepted with a 200 and silently never stored - no error, no log line, and the only symptom is a setting that reverts on its next load.
· kamo-login
Answer the link in an address-change letter
/verify-email-change spends the token in a change-of-address letter and says what happened. It lives here rather than in the workspace because the letter goes to a PERSONAL mailbox — usually read on a phone that has never signed into the workspace, and often not signed into anything. login.<domain> is the one host that always resolves for an organization and never needs a session, so it is the only place the link is certain to work for the person who most needs it. Nothing on the page asks for anything. It arrives, spends the token, and reports — a form here would be a form the reader cannot fill in from the mailbox they opened. The spend is guarded against React's development double-invoke, because the token is single-use and a second call would spend it and then report the success as "this link is no longer valid". The relay beside /api/recover and shaped the same way: SecurityService is ClusterIP-only, so it goes over cluster DNS, which makes Host the service name and puts the browser's hostname in X-Forwarded-Host — the only header the backend reads it from.
· kamo-internal
Change your own address, and a platform tab for every account
Two surfaces, joined by one distinction the platform has never explained to anybody: a user account is the PERSON, and a member is one organization's relationship with that person. My Profile gains a Personal email address card, in the Access room beside the password because that is what it is — one of the three identifiers sign-in accepts, and the address a password reset is mailed to. Filing it under contact details would be filing a key under stationery. It commits itself rather than going through the room's save bar: a change is a request, a letter and a confirmation, and a Save button that wrote a sign-in identifier with no proof the mailbox exists would be the entire problem in one control. The account belongs to the organization named by security_provider_id, and only that one may change it. The card is therefore visible everywhere and editable in one place — hiding it in the others would leave a member hunting for a setting that appears to have been deleted. Where it cannot be edited it says whose it is, and offers both ways back: switching from the top-left menu without leaving the app, and signing in directly at that organization's own sign-in address (or, when it has none yet, at the shared one, named as such). Both come from OrgEntryDomainResolver, so the host is never one that fails to resolve. An explainer sits under the card on EVERY organization, not only inside the refusal. "User account" versus "member account" is a distinction nobody outside the platform has any reason to know, and it becomes load-bearing at exactly the moment somebody is refused something — which is the worst possible time to meet an idea for the first time. /account gains a fourth tab, Users, gated on the new MANAGE_USERS platform right. The right already carries the "current organization must be the platform organization" precondition — PlatformRightsResolver returns nothing at all to a session inside a tenant — so the tab does not check the organization separately; a second authority there could only ever disagree with the server's. Search and paging are the server's too: the grid's own filter searches the rows it happens to hold, which on a platform-wide table means reporting "no results" for an account that exists. The drawer behind a row is banded by consequence rather than by topic. Identity edits into a draft and saves once; account state commits per switch and reports how many sessions went with it; recovery is set apart and confirmed. God accounts and the System User render read-only, because the server refuses them and composing an edit that was never going to land is worse than being told first. Also fixes the security-provider banner that has been on this page all along. It built its link from `org.domain` plus THIS organization's member id — a hostname that is not the workspace host, at a member id that does not exist over there. It now points at the sign-in screen the server resolves.
· kamo-shared-library
Add a queue-group subscribe, so work is shared rather than pinned to one pod
Companion to subscribeEphemeral. Two different jobs, two different consumers: subscribeEphemeral every pod must see every message (anything ending in a STOMP broadcast) subscribeQueue the work must happen exactly ONCE (sending an email, calling a provider) Neither is what a plain durable does at more than one replica. With no deliver group a durable push consumer admits a single subscriber, so the second pod is refused [SUB-90012] and the work is pinned to whichever pod bound first — with nothing taking over if that is the pod that dies. Adding a deliver group turns the same durable into a queue the pods share, keeping redelivery and maxDeliver exactly as they were. reconcileDeliverGroup mirrors the existing reconcileDurableFilter, and is not optional: every consumer this change converts already exists on the stream in the old shape, and jnats will not join a plain durable as a queue. Without the reconcile the first deploy after this would leave those subscriptions permanently dead instead of shared — the same silent, self-perpetuating failure the filter reconcile was written for. The delivery loop is now one method behind all three entry points.
· kamo-shared-library
Add an ephemeral subscribe, so more than one pod can serve a conversation
A durable push consumer admits exactly ONE subscriber. The second pod to bind the same name is refused with [SUB-90012] Consumer is already bound to a subscription, and nothing retries. ChatSessionSubscriptionManager names its durable after the chat session GUID, so with two MediaService pods whichever one bound a conversation first would silently own it and every member whose WebSocket landed on the other pod would receive nothing — no messages, no read receipts, no member-added events, no error. That single property is why MediaService has been pinned to one replica, and why a deploy of it is a total chat outage rather than a rollover. subscribeEphemeral() creates an unnamed consumer instead. There is no cross-pod identity to collide over, so every pod receives every message and fans out to its own STOMP clients — which is what the in-heap simple broker requires. It keeps the filterSubject the durable path learned to set (an empty filter consumes the whole stream), and adds an inactiveThreshold so a pod that is SIGKILLed without unsubscribing does not leave its consumer on the stream forever. No behaviour change for existing callers: subscribe() is untouched, and the delivery loop both paths use is now one method so they cannot drift. Ephemeral consumers do not replay messages published while nothing was subscribed. Neither did the durable ones — both use DeliverPolicy.New — so this is not a regression.
· kamo-internal
"What happened?" fills the window, so maximizing enlarges it
Every other field on the report form is sized by its content. The description is the one somebody is actually writing in, so it now takes whatever height is left — and maximizing the tool window is therefore worth doing. Three parts, and all three are needed: The scroll area gains an inner flex column at `minHeight: 100%`, not `height: 100%`. At a fixed full height a form taller than the window would compress its own fields, because flex children shrink by default. As a minimum it is exactly full height when there is room to spare — which is the room the description takes — and grows past it when there is not, at which point the outer box scrolls and nothing is squashed. The field becomes a flex item with a `minHeight` floor. `minRows` is gone with it: that drives TextareaAutosize, which measures the content and writes an inline height, and with `flex-basis: 0` there would otherwise be nothing stopping the field collapsing to zero. The textarea is released with `height: auto !important`, NOT `height: 100%`. The outlined wrapper stretches its children, so the textarea would fill it unaided — except a flex item with a definite cross-size does not stretch, and autosize gives it one inline. Setting height back to auto restores the stretch instead of fighting it. 100% was tried first and silently does nothing at all: the percentage has no definite containing block to resolve against, so it falls back to auto and the field renders at its intrinsic two rows, with no error anywhere to notice. Measured, not assumed. A puppeteer harness built from this component's own emotion output gives 240px of textarea in a 720px window, 444px at 940px, and a floored field with the form scrolling at 430px — with the title field still 40px in all three. The committed guard asserts the rule rather than the height, since jsdom does no layout, and it was confirmed to fail when the rule is reverted to 100%.

Ulimwengu wako unasubiri.
Kuanza bure katika dakika. Ongeza bidhaa wakati wowote wewe ni tayari - hakuna uhamiaji, hakuna logins mpya.
Bure milele - hakuna kadi ya mkopo inahitajika