மாற்று பதிவேடு
என்ன புதிது.
அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.
· kamo-internal
Starting a game from a chat asks once, then plays — it never asks who
Clicking chess in a conversation opened the new-game pane and asked whether you wanted to play the computer or a person, then made you pick the person. The person was on screen. This menu only exists in a conversation with exactly one other member, and that member IS your opponent — asking was asking about something already visible. It confirms instead, because the click puts a message in a colleague's chat: small, but not something to do on a mis-click while browsing a menu. Resuming a game you already have skips the confirmation — reopening your own board costs nobody anything. The confirmation says what actually happens, since it is not what "invite" usually implies: the board opens for you straight away and you move first as white, while they get an invitation to join whenever they like. The board also stops claiming the opponent is thinking when they have not opened it at all — `awaitingOpponentJoin` says which, and the invited player's window reports itself as having looked so the nudge stops.
· kamo-internal
The opening pane, rebuilt around a game instead of an icon
The pane you get when chess has no opponent yet was the SaaS-card kit: an icon in a rounded square, two tab-cards, two segmented rows, a button. It looked like a settings dialog for a feature whose whole point is that it is a game. The hero is now a real game. Anderssen–Kieseritzky, London 1851 — the Immortal Game, in which white gives up both rooks and the queen and mates with three minor pieces — plays itself a move at a time behind the choices, on the member's own board scheme, so the pane is also a preview of the board they are about to play on. Reduced motion rests it on the final mating position. so the browser still owns no chess logic. All 43 moves resolved as legal through that generator, which is a small independent check on it — a hole in it could not have produced this game. The palette is the object's own. A tournament roll-up board is dark green and buff and a chess clock has brass trim, so the chrome is board lacquer, aged ivory and brass, and the board keeps whatever scheme the member chose. Strength is three pips rather than a number, because a number would be a rating claim this engine cannot honestly make. Three things a browser decided, not a preference: - The backdrop had to be a WHOLE board anchored to the bottom. Fitting it letterboxed it into a strip; covering the pane cropped a tall window so hard that four files filled the screen and the game read as an abstract pattern. - Header and CTA are fixed and only the choices scroll. One scroller around the column put the primary action below the fold at every size this window can be. - The backdrop draws no rank and file labels. Behind the words they are not navigation, they are debris. It opens on Computer rather than on nothing: the empty state was mostly emptiness, and that is also the honest default, since playing the computer needs nothing from anybody else.
· MediaService
The inviter plays immediately, and the invitee is told when it is their turn
An invitation used to produce a board the inviter could look at and not touch, waiting on somebody who might be away from their desk. Now the game is running from the moment it is created: the inviter is white, so by the rules of chess it is already their move, and they play. The invited player gets the same chat card as before. When they open it, their window stamps DATE_OPPONENT_JOINED — not an acceptance, since nothing was waiting on them, but the fact that they have seen the board. That fact drives the nudge. If the inviter moves while the opponent has still never opened it, a second card goes into the conversation, because what the first one said is no longer true: it said somebody would like a game, and the accurate statement is now that it is their turn. At most once per game — the only thing that triggers it is an opponent who has never looked, and looking is what ends it. Declining stays available until they open the board. After that, walking away from a game you have started playing is resigning, which belongs in the record.
· kamo-shared-library
An invited game starts running, and records whether it has been seen
Inviting somebody created a PENDING game nobody could touch, so the person who had just asked for a game sat in front of a board they were not allowed to use until somebody who might be at lunch clicked something. Nothing needed that: white moves first by the rules, and the inviter IS white. A game between two people is ACTIVE from the moment it is made. "Has the other player opened it yet" is DATE_OPPONENT_JOINED — a separate question from whether the game is running, which is what it always was, and the one the your-turn nudge keys on. PENDING is kept and deprecated rather than removed: the column persists the name.
· MediaService
A whole game of chess, server-authoritative, over NATS
The rules live in ONE place and it is this one. ChessEngine implements the whole ruleset — move generation, legality under pin and check, castling, en passant, promotion, checkmate, stalemate, threefold repetition, the fifty-move rule and insufficient material — and the browser owns none of it. Every state the server publishes carries `legalMoves` with it, so the board lights up with no round trip and still cannot disagree about what is legal, having never been asked to have an opinion. Proved by perft rather than by inspection: leaf-node counts to depth 3-4 from the four canonical positions. That was not ceremony. The first run failed at depth 3 on a guard keyed to `Math.abs(delta)`, which silently rejected every knight move of the shape "one rank, two files" — a generator right about all 20 opening moves and wrong three plies down. Nothing short of perft finds that. ChessAi is alpha-beta over the same generator, with quiescence (without it a fixed-depth search stops mid-exchange and believes a temporary material count), MVV-LVA ordering, and piece-square tables. Two bugs worth recording: the search was fail-HARD, so a refuted root move returned the alpha bound, which negated into a huge positive score and made the worst move win; and a quiet middlegame at depth 6 took 22 seconds, which is not a hard opponent but a board that looks broken. It is fail-soft now, and every level has a time budget that iterative deepening makes safe — each completed depth leaves a whole usable move behind. Realtime is `chess.member.{memberId}`, per member and NOT per game. That is the only shape StompDestinationAuthz can check: it is a static guard on the interceptor path with no database, so a game-keyed topic could not be authorised at all. Guarded on SEND as well as SUBSCRIBE — the simple broker routes by destination without caring which command carried the frame, so a guard on SUBSCRIBE alone would still let anyone on the socket paint a fabricated board onto a colleague's screen. One live game per pair is a partial unique index, not a check in the service: a check loses to two invitations sent in the same second, which is exactly when it happens. The service reports the race rather than failing it, and returns the game they both wanted.
· kamo-shared-library
Games becomes a parent app, with Kamo Universe and Chess under it
GAMES was a category with exactly one thing behind it — the nav icon launched Kamo Universe directly, so the app and the thing inside it named the same click. Chess is the second thing, and one icon cannot open two. Both become children of GAMES in the ServiceType tree, which is what lets an organization grant one game without the other. ACCESS_KAMOWORLD moves to KAMO_UNIVERSE and keeps its id AND its name: ROLE_RIGHTS persists the id, so changing it would revoke the right from everyone holding it, and **************** persists the NAME as a string, so a rename would silently reassign every security model that grants it. PLAY_CHESS (318) is new. Both ship COMPLETED + enabled deliberately. AppCatalogService derives availability from the stage and the auto-provision loop skips an unavailable app, so a lower stage on KAMO_UNIVERSE would leave every organization playing it today with no ORG_FEATURES row for the app that now owns their right — a live capability withdrawn by a stage flag. CHESS_GAMES holds a game as one row: FEN, because that is the format the rules are actually defined over, plus the SAN move list. There is no clock, on purpose — chess with one is a different game and cannot be paused by closing a laptop. CHESS_GAME_MOVES is the per-ply log, carrying the resulting position so a rewind is one read rather than a replay. MEDIA_OBJS_GAME makes a game invitation a real message row beside _transit and _call, so it survives a reload instead of living only on a WebSocket frame.
· kamo-internal
Filter the bug and improvement reports by who filed them
Reuses /leads' Agent filter — the same field, the same picker, the same gesture — pointed at a different directory: everybody who has filed at least one report, across every organization, with how many they filed. Drawn only when the server says canFilterByReporter, which is a TeamMember of the platform organization and nobody else. Not derived here, and not the same question as isPlatformOrg: that one is about the ORGANIZATION and is true for a plain Member there too. The board is global and a customer is protected only by SystemBugIdentityPolicy withholding WHO filed a row, so a control that lists reporters by name goes to exactly the population that rule already discloses everything to. A second copy of a disclosure rule in the browser is how a name reaches a surface the server withheld it from. The reporter travels on the status counts as well as the listing, because the number beside a status promises what picking it would show. It also joins the empty-state test: chosen while the status is widened to "all" it is the only filter in force, and without it the page would announce an empty platform to somebody who had picked a quiet colleague. AgentSelector gains a `fetcher` it forwards to its picker. It accepted no such prop before, and the failure mode was the quiet one: a filter with its own directory would have shown the viewer's own colleagues instead, which looks exactly like a working filter.
· MediaService
A reporter filter, for platform team members only
The board is global — every organization sees every report — and what protects a customer is SystemBugIdentityPolicy withholding WHO filed a row. A control that lists reporters by name is that same disclosure arriving through a different door, so it is offered to exactly the population the policy already discloses everything to: term 1, a TeamMember of the platform organization. Reused rather than respelled, and a plain Member of that org is refused with it — "works at the platform org" and "may read customer identities" are different populations, which is the distinction the policy exists to draw. GET /reporters is refused outright to everybody else, because an empty list is a fact about the board and this is a fact about the caller. ?reporterId= on the listing is IGNORED rather than refused, following includeVoided — a stale filter in a bookmark is not an attack. Ignoring is what matters here, not politeness: every row on this board is already visible to everybody, and a listing that honoured the parameter would answer "which reports did member 1170933975273603078 file?" for anyone who asked, which is precisely how an alias:2 on a card gets resolved back to a person. statusCounts now takes the caller rather than their id — it needs their TYPE for the same gate — and carries the reporter through, so the number beside a status keeps promising what picking it would show. The list response gains canFilterByReporter so the console draws the control from the server's answer. A second copy of a disclosure rule in the browser is how a name reaches a surface the server withheld it from.
· kamo-shared-library
Let the listing be narrowed to one reporter
Adds an optional reporterId predicate to findVisible (both halves) and to countGroupedByStatus, so the numbers beside the status menu keep promising what picking a status would actually show. It filters on b.reporter.id and never on b.reporterOrg — the second would be the tenancy predicate this query is required not to have, wearing a different name. Adds the directory behind the filter's picker: everybody who has filed at least one report the viewer can see, with how many. Two queries rather than one, both applying the listing's publish and voided rules verbatim — a name the filter offers that the list cannot produce a row for reads as a broken filter. Deliberately a query and not a maintained table. A denormalised "who has reported something" table has to be kept in step with every create, void and delete, and its failure mode is silent: a reporter missing from the filter looks exactly like a reporter with nothing to their name. The table it groups over grows by one row per bug somebody sat down and wrote — 87 of them across the estate as this shipped. If it ever stops being free the answer is an index on reporter_id, not a second source of truth.
· kamo-internal
The browser half of the fifty easter eggs
THE CLAIM ROUTE DID NOT EXIST. The Java endpoint has accepted CLIENT claims since the catalogue's first ones shipped, but nothing in Next forwarded to it and nothing in the browser called it — so all six existing CLIENT achievements, TOP_CORNER included, were declared, rendered as locked cards and unearnable. That route is the first file here, and TOP_CORNER now claims from the goal it was always about. ONE OBSERVER PER CONCERN, NOT ONE CLAIM PER PAGE. Nine of the eggs are about where a member has been, six about the dock, three about who else is online. Each set is a single component that renders nothing, owns no element and reads no state anybody else can see — so the layout engine, which is load-bearing for the standing rule that a tool window never loses state, gains no achievement code at all. The remaining twenty-one are one import and one line at the place the thing actually happens. FIRE AND FORGET, DELIBERATELY. Nothing waits on a claim, branches on one, or shows an error for one. A member who has just found a secret is in the middle of doing something else. The local ledger is an optimisation against re-POSTing from scroll and route handlers, never a source of truth: clearing it costs one redundant round trip. Four eggs are narrower than they were specified, and each says so in its own doc rather than quietly reading as the thing it is not. Full-text search over document CONTENT does not exist in the manager, presence does not carry a route or a department, and chat heads bounce off the ceiling rather than leaving through it. ON_THE_SCENT is the biggest of these: it was meant to pay a member for TELLING a colleague, which needs an organization-wide "somebody found something" broadcast that does not exist and should not be invented quietly for a ten-point badge.
· kamo-internal
An hourglass sprite for the Loitering track
The catalogue guard gives every TRACK a sprite no other track and no single action wears, so the ninth track needed its own 16x16. Drawn three times before it read right, which is what crop-screenshotting at 3x is for. The near-black outline is invisible against the cabinet plate, so a sprite whose shape lives in its outline has no shape at all — the first pass was a serif "I" and the second a goblet. What reads is the COLOURED mass: two mirrored hollow bulbs, a waist, caps, and the sand piled in the bottom. Empty on top and full underneath, because a full hourglass is a timer nobody has started and this achievement is about time already spent.
· SecurityService
Probes for the eleven derived easter eggs
Eleven of the fifty secrets are questions about durable data rather than about whether a member has SEEN something, so they are DERIVED and they backfill: a member who reacted to a colleague's message at the same second in March holds High Five the first time this runs. Every query was run against the live schema before it was written down, and two of them already answer TRUE for existing members. The two that count the hunt itself generate their IN-list from the catalogue rather than hard-coding it. A hand-typed list is a second place the set of secrets is defined, and the failure it produces is the worst available here: adding a fifty-first secret would leave the completion badge awardable without it, so a member would hold "found every secret" while the cabinet showed them one short. TAG_TEAM binds assigned_from_id — the member who HANDED the lead over, which is the half of the row the achievement belongs to — so the probe test's whitelist of member-binding columns gains it.
· kamo-shared-library
Fifty secret easter eggs, spread across the product
The catalogue was good at rewarding the parts of the product a member already uses and bad at getting them into the parts they do not. A prompt cannot fix that — a prompt for a feature you have never heard of reads as a chore. A rumour can, so these are hidden: the server withholds the name and description until one is earned, and the only way to find one is to go looking. THE LINE THEY ALL SIT ON. useSecretGestures says a secret must be impossible to reach by accident, which is right, but a secret that has to be TOLD to you drives no exploration at all. So the rule here is narrower: impossible to trip while doing your job, easy to trip while poking. Opening a mode of a tool nobody opens, reaching the bottom of a long document, hitting the ceiling with a chat head. CATEGORY IS THE ONLY HINT ANY OF THEM GIVE. Spread across all five rows rather than heaped into CURIOSITY, so a silhouette in TEAMWORK says it takes a colleague and one in DEDICATION says it takes a clock. That is a hint about where to look and never about what to do. Fifty anonymous plates in one row would read as a single feature rather than fifty things to find. Eleven are DERIVED and backfill themselves for every member who did the thing before today; the other thirty-nine are CLIENT, because "have you seen this yet" leaves no row anywhere. All thirty-nine are worth ten or twenty points, which is the documented price of a forgeable claim. THE_COMPLETIONIST's threshold is computed from the catalogue rather than written down, so a fifty-first secret moves the finish line instead of leaving somebody holding a completion badge for a set they have not completed.
· SecurityService
Award Loitering from the session that is still open
The measure is the age of the *** the member is holding, so the session had to start carrying the instant it was minted — nothing anywhere recorded it, because a session is a Redis record that leaves no row behind when it lapses. The stamp survives updateSession, so re-baking rights mid-session does not reset a member's clock. Two seams, the shape syncGrowthMissions established. The session-extend endpoint is the one that matters: it fires BECAUSE the member is still here, so the popup lands during the loitering instead of congratulating them tomorrow on a stretch that has already ended. /self and /self/pending cover the dropped extend and the member idling on one page. Under half an hour the sync returns on arithmetic alone and never reaches the database, which matters when the caller is the endpoint that keeps people signed in — and nothing it does can fail that endpoint, or a badge would be signing members out. An impersonated session earns nothing. Every other achievement records a deed, and a deed done while acting as somebody is still theirs; this one records only that a tab stayed open, and an admin who leaves one up overnight must not hand a member a level for a session they have never seen.
· kamo-shared-library
Loitering, nine levels of not leaving
A DERIVED track over the age of the session the member is holding right now, in minutes: 30, 45, 60, 90, 160, 360, 720, 1440, 2880. The first entry in DEDICATION, which until now was the one empty row of the cabinet. The count DROPS to nothing at every sign-in, which no other track does, and nothing had to be added for it: record() keeps progress at Math.max and never lowers a level, so the card settles on the member's longest session ever and a fresh sign-in cannot take a level back. COMMON and worth ninety points across all nine levels, because rarity drives the celebration on every level-up and a screen-shake for having sat still for half an hour would insult the member who spent it closing something.
· kamo-internal
Contact Effects moves inside Audio, Video & Controls
It was a top-level tab on the member page, sitting beside Audio & Video and answering the same question — what this shell does under your own hands. It is now the fifth sub-tab of that one, which already had a sub-tab bar for Audio, Video, Ringtones & Alerts and Event Sounds, so this is one more entry rather than any new machinery. The tab it joins is renamed to say so. The Quick Tips rail travels with it. A tab's help text is part of what the tab is, and left alone the rail would have gone on describing microphones while somebody edited what happens under their pointer. It goes through HelpTipsOverrideContext, the same route the four provider-setup screens take, and restores with `undefined` rather than `null` so SettingsView falls back to whatever the tab itself declared. Nothing changes about who sees it: both tabs were already gated on isSelf, and the service resolves member scope to the caller's own id from the session regardless of the path parameter, so an administrator on somebody else's profile still gets the read-only editor. The sub-tab label reuses **************** already translated in all 22 files, rather than minting a key to say "Contact Effects" a second time. Wand2 rather than Sparkles for the icon — Sparkles is spoken for four times over inside the audio panel. ?tab=contact-effects no longer resolves and falls back to My Profile. Nothing in the repo links to it (the platform configuration page's contact-effects tab is a different screen), so this is bookmarks only, and a redirect would have meant lifting sub-tab selection into the URL, which none of the other four do. Verified in a browser: the bar carries five tabs with no clipping or overflow from 760px to 1500px of content width, measured identically with four tabs and with five.
· kamo-marketing
Labor Day — a working skyline clocking off
The nineteenth scene, and the first Monday in September with the Saturday and Sunday before it. It is the one window on this calendar that cannot collide with anything: the day walks September 1-7, so the window runs August 30 to September 7 at the extremes, four clear days short of Patriot Day in the worst year and eight weeks clear of Independence Day behind it. Ordinary priority, default boost, nothing in PERMITTED_ECLIPSES. The scene is about the end of a shift rather than about a parade. A skyline along the bottom edge in the last light of summer, a tower crane, and a few hundred lit windows that GO OUT WHILE YOU READ — one at a time, on an irregular clock, with the occasional one coming back on because somebody always forgets something. Four things carry it: · holiday.progress sets both the floor the lights settle at (42% still working on the Saturday, 13% by Monday night) and how fast they get there. A reader who comes back on the Monday sees a different city, which is the argument for a scene that reads the calendar rather than merely knowing the date. · The pointer turns the lights out, with a ragged edge rather than a circle. It is the only interaction in the scene, and the relight trickle runs regardless, so sweeping the whole band finds the city refilling behind you instead of leaving a dead grid. · The crane's jib foreshortens as L·cos(theta) and flips through zero with the counter-jib coming the other way, because a tower crane's jib is level and swings in plan. Rotating the bar in the picture plane is the usual mistake and reads as a windmill. The hook chases the trolley on a dt-based damped follow, so the load lags into a swing. · The haze over the city is one blit whose alpha tracks the lit fraction, so the emptying reads as dusk falling and not as dots switching off. Cost, per frame: one half-scale sheet blit (sky, sun, far skyline), one glow blit, one path for the masses, one stroke for the rim, at most sixteen batched fills for the windows through a pre-built alpha table, about thirty path ops for the crane, one small blit per mote. No gradient built in onFrame, no shadowBlur, every soft edge a pre-rasterised sprite. The mote and glow sprites depend only on dpr and sit behind a guard, so the resize storm an iOS URL bar produces cannot rebuild them; the sky sheet must be rebuilt for a genuine height change, and the skyline carries its lit state across so the city does not relight mid-scroll. Two numbers were chosen by looking at it over the homepage rather than by argument. The masses are at 0.3 alpha, not the 0.5 they started at: on a dark page a dark silhouette is invisible everywhere except where it lies across content, so every point of alpha it carries is spent entirely on dimming the words behind it. And the band is capped against the WIDTH as well as the height, because a fraction of the height alone gives a 390px phone the same 220px of city a desktop gets — a quarter of that screen, sitting across the primary call to action. Reduced motion gets the same city with the clock stopped: lights where the seeded layout put them, the crane parked with its load hanging straight, the beacon caught lit, and no dust, no shift and no swing. Scene counts in the surrounding prose go from eighteen to nineteen, and the guard's dressed-days figure from a stale 88 to the 87 it now prints.
· kamo-internal
The celebration engine — an arcade machine installed inside the CRM
/achievements was already a 1987 cabinet, and nothing ever HAPPENED on it. This is the layer that happens: a banner that drops from the top shouting ACHIEVEMENT UNLOCKED!!!, then INCREDIBLE!!!, then LEGENDARY!!!; chunky pixel confetti with tiny trophies and thumbs-up hidden in it; arcade score numbers flying off whatever was pressed; a combo counter that escalates to ABSOLUTELY UNNECESSARY!; fake criticals dealing x9999 damage for five experience points; a Windows 95 dialog announcing that Windows has detected an unusually productive employee; milestones restated as boss fights with health bars; and, at the top of the rank ladder, a seven-second curtain that tells a member there is nowhere left to go except back to work. THE HUMOUR IS THE CONTRAST, so the professional half is untouched. The page's layout, spacing, information architecture and reading order are exactly what they were. Everything ridiculous is drawn on ONE portalled overlay that takes NO POINTER EVENTS — a member can read, tab through and click their achievements in the middle of a fireworks display, which was verified in a browser by hit-testing the centre of the screen mid-show and finding the tile heading underneath. ONE ENGINE, not twenty animations. `ArcadeFxProvider` mounts once in AuthedChrome, wrapping both the unlock popup and the page, and owns the show queue, the combo window, the randomness budget and the off switch. Two engines was the obvious shape and the wrong one: a member who earns something while looking at their own achievements would be congratulated twice by two systems that cannot see each other. Shows are serialised and the queue is capped at three, so hammering plates cannot buy sixteen seconds of banner. EVERY NUMBER IS REAL. A boss's remaining health is the distance to the member's next tier, computed from the thresholds the card already carries. A streak flame is levels actually cleared. MYTHIC — the sixth rung the brief asks for — is DERIVED as a LEGENDARY track finished to its last level rather than invented as a rarity the server does not have. The score pops are the one deliberate fiction and the real award is fired last, so the honest number is the one still on screen. REDUCED MOTION IS AN OFF SWITCH, NOT A DIMMER, following ConfettiBurst's precedent: no banners, no particles, no marquee announcements, no fake alerts, and the plates stop being buttons. Verified in a browser with the media feature emulated — the stage is absent, celebrate() is inert, and the XP bar shows its true value immediately. Two defects found by looking at it in a real browser rather than at the code: * The XP meter's "have I already filled to this value" ref made StrictMode's second effect pass return early, so the bar sat empty at zero forever. The dependency array was always the guard. * A canvas silently IGNORES `fillStyle = 'var(--ach-cyan)'`, and three callers legitimately name a cabinet variable, so every one of their particles was painted black. Resolved against :root now. New copy is a local English lexicon, not dictionary keys, and that is a decision: "SPREADSHEET WARLORD" is a cabinet shouting rather than product copy, a 1987 machine shouted English in every arcade on earth, and machine translation destroys the register the whole joke rests on. The two strings that ARE product copy — the replay button's label and the line telling members the plates are pressable — went through kamo-translation-dictionary first. Verified: 6,640 tests and all fifteen guards pass, tsc clean, `next build` clean, and every show screenshotted in a real browser across its own beats.
· kamo-internal
Resolve contact effects through four layers, with a tab at the platform and the member
Contact effects were a single per-member setting on one organization page. They now resolve exactly the way event sounds do — shipped catalogue, then the platform, then the organization, then the member — with a pane for each layer and Inherit above No effect on every one below the top. Platform Administration → Configuration → Contact Effects (new tab) Settings → Essential → Behavior & Labeling (the organization's layer) Settings → Member → Contact Effects (new tab, on your own profile) EVERY EVENT RESOLVES INDEPENDENTLY. A member who picks confetti for one contact keeps their organization's choice for the other five and the platform's for whatever the organization never touched. The alternative — copying the resolved set into a layer on save — would freeze that member at whatever shipped the day they first opened the pane, and nothing an administrator changed afterwards would ever reach them. `null` and absent both mean inherit, and neither is interchangeable with a value: `none` is a decision to paint nothing and it survives a lower layer saying otherwise. That is the whole difference between "I don't mind" and "not for me", and it is asserted. Inherit says what it would give you — "Inherit — Chime", "Following your organization: Chime. Change it there and this follows." Offering it as a bare word asks somebody to choose blind. The platform pane has no Inherit at all: there is no layer beneath it but the catalogue, and its reset control is what returns to that. One editor serves all three panes; they differ only in which layer they write and whether Inherit is offered. Three near-identical editors is how the three drift apart. The platform defaults are now reticle → nova for the left button, aperture → burst for the middle, and pool → droplet for touch. Chosen as a SET rather than as six favourites: a press pulls energy in and a release lets it out, so a click still reads as one gesture, and none of the three pairs is round, which keeps a shell that is mostly rectangles from looking like it is blowing bubbles. Backed by SecurityService's new /api/security/contact-effects (a31a3fb..88c9566) over its own CONTACT_EFFECT_PROFILES table, gated for the platform layer by the new MANAGE_PLATFORM_CONTACT_EFFECTS right. The member scope resolves to the caller's own id from the session, so the member tab is read-only on somebody else's profile and the service would refuse the write regardless. Two defects found by rendering the panes rather than reasoning about them. The platform pane displayed "Inherit — Reticle" for events it had not set, while offering no Inherit option — a value absent from the options list is one nobody can return to; it shows the catalogue value as the standing choice now. And every pane previewed the FULLY resolved effect, so an administrator setting the organization default saw whatever their own member override happened to paint under their own The layer store is a module-level cache plus a window event rather than a React context: the canvas is mounted as the last child of <body>, outside every provider, because it must also cover /validate and /logout. Every failure path lands on the shipped defaults, silently — this loads on pages with no session, so a 401 is the normal case. Not migrated: the member's old `contactEffects` ui-preferences key, written only by the previous single-layer pane during the day it existed. It is orphaned rather than read, and everyone starts from the layered defaults.
· SecurityService
Serve the three layers of the contact-effect stack
GET /api/security/contact-effects returns the platform, org and member layers in one round trip; PUT and DELETE on /{scope} write and reset one layer. The service never merges them — the browser has to merge again the moment a member changes something anyway. Platform writes need the new MANAGE_PLATFORM_CONTACT_EFFECTS platform right. Org writes need CONFIGURE_SYSTEM and land on the caller's OWN org from the session, never a path parameter. Member writes are for the caller alone: there is no path to write somebody else's, because an administrator Reads are ungated beyond authentication, deliberately. Every signed-in member needs the layers above them to know what to paint, and a 403 there would mean a shell with no feedback under the pointer rather than a visible failure. A near-copy of EventSoundController rather than a shared generic. The two differ only in their table and their right today, but they are separately-governed surfaces — sounds already grew an uploaded asset library and a per-device output route. The duplication is forty lines; the coupling would have been permanent.
· kamo-shared-library
The storage and the platform right for a three-layer contact-effect stack
CONTACT_EFFECT_PROFILES holds the platform, org and member layers of the animation painted where a pointer touches the screen — the same shape, and for the same reasons, as EVENT_SOUND_PROFILES: one scope, the id that scope is about, and an opaque settings blob. A separate table rather than a KIND column on the sounds one. Sharing would mean widening a unique constraint that live rows already depend on, to save a migration that costs nothing, and the two documents have nothing in common but their layering. MANAGE_PLATFORM_CONTACT_EFFECTS gates the platform layer, exactly as MANAGE_PLATFORM_SOUNDS gates its own. Safe to append: right_type is @Enumerated(STRING) on a varchar(64), and the live platform_administrator_right table carries no CHECK constraint to be frozen at the old value set. The table is created by hand (ddl-auto is none), with no CHECK constraints of its own.
· kamo-internal
Eighteen sprites for the single-action achievements
Thirty-five new achievements arrived in the catalogue and every one of them was rendering the padlock fallback, because all eight existing sprites belong to the multi-level tracks. Eighteen sprites for thirty-five achievements, grouped by FAMILY rather than one each. That is a decision rather than a shortcut: a track is a trophy somebody chases for months and has to be recognisable at a glance, but the single actions are a "have you tried this yet" checklist where the family — notes, the desk, chat, meetings, leads, contributions — is the useful grouping. Eighteen drawn carefully beats thirty-five drawn quickly. AchievementCatalogTest still enforces the half that matters: no single action may wear a track's sprite, and no two tracks may share one. Two of them were typed with a Cyrillic 'о' instead of an ASCII 'o' — identical on screen, and it would have rendered as a transparent pixel in the middle of a sprite with nothing to explain it. A 16x16-and-legal-characters check caught both before they were pasted in; pixelArt.test.ts is the standing guard.
· SecurityService
Award and backfill the single-action achievements
Twenty-six of the thirty-five are questions the database can already answer, so they are awarded by asking it. SingleActionProbes holds one EXISTS per achievement — what "has this member done it" MEANS, in one reviewable file rather than scattered as derived queries across twenty repositories in the shared library, where the next reader has no idea any of them is load-bearing. THE READ PATH IS THE BACKFILL. These were true before the catalogue existed, so the first achievements read after this ships awards every member everything they had already done. Nothing to run, no migration. Probes are skipped once the member holds the achievement — it is never revoked, so held is a permanent answer and re-asking can only cost — and each runs in its own try/catch, so a renamed column costs one badge rather than an achievements page that awards nothing. EVERY PROBE WAS EXECUTED AGAINST THE LIVE SCHEMA BEFORE SHIPPING, which is the only reason four of them work. As written and reviewed they all looked correct: TAKE_A_SEAT required meeting_participants.joined_at, which is NULL on all 283 live rows because nothing writes it. Permanently unearnable. WORTH_KNOWING compared contacts.owner_id to a member id. It is a USER id. MAKE_IT_YOURS keyed user_backgrounds on member_id, which the upload path leaves null; the row is keyed by user. STAKE_YOUR_CLAIM used primary_agent_member_id, which is null on most SEE_IT_THROUGH reservations. The requester is the one who gets the lock. Each was a badge that would have sat locked forever with nothing in any log to say why. A census across every member now credits 336 member-achievements on first read, from 83 members with a photo down to 1 who has closed a locked opportunity. Five probes answer zero and that is correct — nobody has confirmed 2FA, filled a picture frame, had an idea seconded, taken a free-for-all lead, or uploaded a wallpaper yet. The other nine are awarded through two guarded paths. /internal/achievements/event is server-to-server behind X-Internal-Auth and refuses anything not EVENT; /achievements/self/claim resolves the member from the SESSION and refuses anything not CLIENT. Neither can reach a counted track however good its credentials, which is what the trigger kind is for.
· kamo-shared-library
Thirty-five single-action achievements, and a trigger kind
The "have you tried this yet" half of the catalogue: one tier at a threshold of one, cheap on purpose, existing to make the product's own surface discoverable. AchievementTrigger is the load-bearing part. These thirty-five do not all reach the platform the same way, and leaving that implicit would mean one endpoint able to award anything: BASELINE granted for existing. Exactly one — JOIN_THE_TEAM. It has its own kind so the probe map's startup guard needs no special case, and a guard with an exception in it is a guard the next person edits around. DERIVED recomputed from rows the platform already stores. Twenty-six of the thirty-five, and the strongest kind: true exactly when the data says so, nothing to fire at the right moment, and every member who did the thing before the code existed is credited the first time they look. EVENT a service reports it. Three, and only where there is genuinely nothing to count: a status flipped and flipped back, a sign-in from a client a session does not record, a game whose world is in another database. CLIENT the browser claims it. Six actions no server can see — a floated note, a maximised window, a screen shared over WebRTC, a chat head thrown at a goal. THESE ARE FORGEABLE BY THE MEMBER THEY BELONG TO. That is accepted deliberately and bounded deliberately: the member comes from the session so nobody can claim for a colleague, the service refuses any other kind through that path, and every one is worth a shrug. AchievementCatalogTest fails the build if one exceeds thirty points. Every track stays DERIVED and a test enforces it — a level counted forward from events is wrong the first time an event is lost, with nothing to compare against. TOP_CORNER is the catalogue's only secret. Everything else here is a prompt, and a prompt withheld is not a prompt; this one is an easter egg, and an easter egg listed with instructions is a chore. Three catalogue guards caught real mistakes while this was being written: a level title reused across two achievements (twice), and a client-attested achievement priced at ninety points.
· kamo-internal
Skins — the one bar, wearing a different machine's clothes
The scrollbar already let a surface tint itself: five custom properties on the scroller, inherited, read by the overlay. For every screen in the app that is the whole of what "matching" means. It is not enough for the achievements cabinet, where the bar has to stop being a tasteful pill and become part of the furniture. So: a SKIN, resolved from `--kamo-scroll-skin` exactly as the palette is resolved — same mechanism, same inheritance, no new plumbing — carrying everything about the look that is not a colour: rounding, bevel, track and thumb texture, outline, and the class any motion hangs off. IT IS DATA, NOT COMPONENTS. A new skin is an entry in a table, not a branch inside the view, so the app still has genuinely one scrollbar: every skin gets the same five controls, the same keyboard handling, the same ARIA and the same drag maths. A test pins that the arcade skin still draws all four end buttons and a track, because a skin must never cost a member a control — those buttons are why this component replaced seven hand-written bars. `default` is unchanged and is what a caller that never mentions a skin gets, which is the risk worth guarding: it is what ~40 scrollers render with, and nothing else in the suite would notice it drifting. An unrecognised name resolves to it rather than throwing — this comes from a CSS custom property, so a stylesheet typo reaches it, and that should cost a screen its decoration, never its scrollbar. One real bug found on the way, and only because the skin exposed it: every colour here was written with the `background` SHORTHAND, including the two imperative hover handlers. The shorthand resets `background-image`, which is where a skin's texture lives — so the texture was correct at rest and erased the moment a pointer touched the thumb. All of it is longhand now, and a test asserts the shorthand never comes back. (The flat look in my first capture was jsdom's old CSS parser dropping the gradient entirely, not this bug. Chrome parses it fine either way. Worth saying because the two look identical in a dump and only one is real.)
· kamo-internal
24 more contact effects, grouped by character rather than by geometry
The catalogue sorted itself by what an effect DID — converging, expanding, lively. Nobody opens a settings pane wanting "something that expands"; they want something quiet, or something ridiculous, or something that looks like an instrument. The groups are now Quiet, Classic, Graceful, Eccentric, Brute force and Technical, running from restraint to force, and twenty-four new effects fill them out. Forty-eight in all. Graceful petal five rounds swing open like a flower ribbon a wave runs around an unfurling ring lace a scalloped edge of twelve half-rounds plume a feather sweeps out from the point firefly motes drift and twinkle out of step veil two soft rounds grow and lift away Eccentric wobble a circle that never quite manages it boing a ring on a spring, overshooting pinwheel five curved blades thrown out spinning scribble a scrawl that draws itself bubbles rounds of odd sizes rise and sway starpop a five-pointed star arrives spinning Brute hammer driven onto the point and stopped dead crack six fracture lines split the surface crater a heavy rim with debris thrown clear quake three heavy rings leave with a shudder shards eight thick pieces tumble outward rivet punched in, one hard ring, done Technical radar a sweep goes round with its afterglow caliper two jaws close on a marked scale grid a lattice snaps down onto the point waveform an oscilloscope trace runs outward chevrons two ranks of arrows close in nodes points come online and wire together No effect stays first in every list, the six defaults are unchanged, and every existing id survives the regrouping, so nobody's stored choice is disturbed. The palette and the marks move to paint.ts so effects.ts is nothing but the effects. Two new primitives earn their place: polyline, and shapedRing for anything defined by a radius function — between them they buy the wobbling ring, the scallops, the star, the trace and the scrawl. That widened what a mark can be, and it broke the reach assertion in a way worth recording. The recording context bounded a path by its first and last point, which is exact for a segment and blind for everything else: a star's furthest pixel is a tip in the MIDDLE of the path, and a wobbling ring's is a lobe. It now records every vertex. The invariants held for all 47 painting effects on the first run afterwards, at three seeds across ten points of each life. Two effects were rebuilt after rendering them. The bubbles all left the same pixel at the same rate and climbed as a single knot; they now start from their own pixel at their own rate. The plume's barbs were too short on too tight an arc and it read as a comb rather than a feather. Verified in a browser: 48 options, No effect first, seven groups all emitting role="group" with presentational headings, and type-to-filter reaching the whole catalogue.
· kamo-internal
Make the contact effect a choice — 24 of them, one dropdown per event
The six animations shipped last change were hard-wired to the six contact events. They are now a catalogue any of which binds to any event, and the member picks, from a new pane on **************** beside the org's sounds. Twenty-three effects that paint, plus No effect, in six groups by what they DO rather than by what they were designed for: Quiet halo, pinpoint, crosshair, reticle Drawn inward converge, aperture, pinch, magnet Pushed outward bloom, burst, ripple, pool, echo, chime, shockwave Lively nova, confetti, orbit, spiral, comet, droplet Technical hexlock, scan No effect is the first option in every dropdown and is never the default: silence has to be one click away, but shipping it as the default means nobody discovers the feature exists. The defaults stay the six from last change, which are a set rather than six picks — a press pulls energy in and a release lets it out, so an ordinary click still reads as one gesture. Three modules, each knowing only what it must. effects.ts is the catalogue: a draw function plus how long it lives and how far it paints, and nothing about mice or members. binding.ts maps the six events to effect ids and reads a stored preference back. impulse.ts is one effect at one point at one instant. That split is what keeps the whole drawing layer testable in node against a recording context — 49 tests, including that no effect paints outside its declared reach (asserted at ten points across each life, at three seeds), that each fades to nothing, and that no two are the same animation wearing two names. Stored per MEMBER in the generic ui-preferences store, so it follows a person between devices and changes nothing for anyone else — no schema, no service change. Every failure path lands on the defaults, because the canvas also runs on /validate and /logout where there is no session to ask. An id that no longer exists falls back per FIELD, so retiring one effect cannot cost a member the other five choices they made. Each row previews live, using the real engine at the org's own colours. At rest the pad draws the effect's own reach as a dashed boundary: every effect is over in under a second, so a pad that only paints while playing spends nearly all its life empty, and six empty boxes in a settings pane read as something that failed to load — which is exactly how it looked when first rendered. Three defects found by driving it in a browser rather than by reasoning about it: - MUI's Select clones role="option" onto EVERY child it is given, ListSubheader included, so the six group headings were announced to screen readers as selectable options. Autocomplete with an explicit renderGroup emits role="presentation" headers wrapping a real role="group" — and brings type-to-filter, which twenty-four options genuinely want. - A sticky group heading sat on top of the first option in its own group and hid its name. - toBinding read stored[event] straight, which walks the prototype chain. Verified end to end in Chrome with real trusted input: the default binding paints converge at 22px, rebinding to shockwave paints at 79px, an unknown id falls back to the default, and No effect allocates no canvas and paints no pixel at all.
· SecurityService
Award the six tracking-link achievements, and backfill them
**************** reads all six of a member's marketing counters in one row and settles all six tracks against them. Wired into /self and /self/pending beside the baseline and the Growth Hub recount. NO EVENT SEAM HERE, AND THAT IS DELIBERATE RATHER THAN UNFINISHED. Three of the six are driven by beaconed page views, so an award call per event would put a cross-service request on the marketing ingest hot path. Hooking only the three low-frequency conversions instead would make half the feature instant and half of it not, and a member who learns that some of their trophies arrive late stops trusting the rest. One recount, on every achievements read, treats all six alike. One query for six numbers, read at one instant: six sums would be six round trips on a page load, and would let ingest move between them — recording a member as having more organizations than signups, which is not a state the funnel can be in. The column-to-achievement mapping is one readable list, because getting it wrong files a member's page views as their paid accounts and nothing downstream could detect that. THE BACKFILL. `award` SETS the running total rather than counting forward from now, so the recount credits every level a member has already earned the first time it runs for them — the counters have been accumulating for months and the launch has to find people already holding levels. The read path does that for anyone who opens a page; **************** does it for everybody else, nightly, and its first run IS the backfill. There is no script to run. The sweep exists because of the LEADERBOARD: it publishes every colleague's points to everybody in the organization, so a member who has driven ten thousand visitors but has not opened the page would sit at zero on everyone else's board — a wrong statement about somebody else that they cannot fix by looking. Nightly rather than one-shot for the same reason: a one-shot has to be correct on the single night it runs and leaves the board drifting the next day. It is bounded by members who own a tracking code, skips a member whose standing has not moved without writing anything, runs at 04:40 (after MediaService's 03:15 counter reconcile, so it never awards a level off figures about to be corrected downwards), takes a SingletonTaskRunner lease so two pods cannot both publish an unlock frame for the same level, and logs at INFO even when nothing moved — "nothing moved" and "the sweep did not run" look identical from outside and only one of them is fine.
· kamo-shared-library
Six tracking-link achievements, ten levels each
The marketing funnel, read six ways, one track per counter already carried on MARKETING_TRACKING_CODES: DRAW_A_CROWD uniqueVisitors 1 .. 50,000 BRING_THEM_BACK visits 2 .. 100,000 HOLD_THEIR_ATTENTION pageRequests 4 .. 200,000 SIGN_THEM_UP uniqueSignups 1 .. 250 PLANT_A_FLAG orgsCreated 1 .. 250 MAKE_IT_PAY paidAccounts 1 .. 250 Summed across EVERY code a member owns, not per code. A member who runs one link and a member who runs eight are doing the same job, and scoring per code would pay the careful one less for labelling their channels separately. RARITY IS WHERE THE FUNNEL IS EXPRESSED. The three traffic tracks share UNCOMMON because they are three views of the same traffic — the thresholds sit at roughly 1:2:4 because that is the shape traffic has, so one afternoon levels all three, and three rarities would celebrate that afternoon at three different volumes. The three conversion tracks then run 1..250 with IDENTICAL thresholds and escalating rarity — RARE, EPIC, LEGENDARY — because the same numbers are far harder one step further down the funnel, and that is worth saying once in the rarity rather than by inventing three sets of numbers to remember. MAKE_IT_PAY is the catalogue's only LEGENDARY and should stay rare company. `totalsForMember` answers all six in ONE row. It runs on every achievements read, so six sums would be six round trips on a page load — and the six have to be read at one instant, or ingest moving between two of them records a member as having more organizations than signups, which is not a state the funnel can be in. Inactive codes are included deliberately: retiring a link stops it counting new traffic, and must not also take away the levels its traffic already earned. `distinctCodeOwners` is the backfill sweep's worklist — members who own a code rather than every member on the platform, which would be most of the cost for none of the result. Also here: **************** which compiles every hand-written @Query on that repository through Hibernate's own parser with no database. A malformed @Query is invisible to the compiler and to every mocked-repository test; Spring Data validates it while building the bean, so the first thing that notices is the pod, and the way it notices is a crash loop. This project has no @DataJpaTest harness, so those queries had no coverage of any kind until now. The catalogue tests grew three guards worth having: no two achievements may wear the same icon, no two levels anywhere may share a title, and no level title may collide with a MemberRank name — the HUD shows a rank and a level side by side.
· kamo-internal
A storage meter beside My Widgets, for the owner who is filling up
An owner had no way to know how much of their plan's storage they had spent without opening a settings tab they had no reason to open. The bar that told them lived on /settings/account, behind a tab, on a page about costs — which is to say it told the people who had already gone looking. A liquid capsule in the launchpad's pill row, immediately left of My Widgets. It fills from empty on mount, the digits count with it, and a sheen travels the fill so it reads as a level rather than a rectangle. Hairline ticks sit at 70% and 95% so the bands are legible BEFORE you are in one. WHAT IT COSTS THE PAGE Nothing. The figures ride the entitlements payload the launchpad already fetches — one request per tab, shared by eleven consumers, cached fifteen minutes in Redis. The flyout's breakdown and sparkline are fetched only when it opens, and from /storage/history, which reads the snapshot table; /storage/summary sums nineteen live domains and has no business behind a hover. One folded history payload draws both, so the last point of the line and the total of the bars are the same measurement and cannot disagree. GREEN IS WIDE ON PURPOSE StorageUsagePanel already carries the reason — "a bar that is amber at 60% teaches people to ignore it" — and a meter every owner sees every day has far more to lose from crying wolf. Above 95% the pill turns red, grows a pulsing aura, marches hazard stripes and shudders once every six seconds. Six, not continuous: a thing that moves all the time is read as decoration and tuned out within a minute, and this has to still work on the day it matters. 95% is not a taste judgement either — it is StorageQuota.AUTO_EXPAND_AT, the point the platform itself considers the org out of room, so the meter going red and the system acting are one threshold rather than two that can drift. THE THREE WAYS THERE IS NOTHING HONEST TO DRAW, all of which render as 100% on a naive used/capacity: no payload (not an owner); no snapshot yet, where usage is zero because nobody has looked; and a capacity of zero on a plan that is not unlimited, which is far more likely a catalog row missing its STORAGE feature than a real allowance of none. A launchpad siren caused by a catalog gap is the worst version of this bug, because nothing the owner does will clear it. Each gets its own state. Band colours are fixed rather than mixed from --color-primary. "You are about to run out" is a traffic light, not a brand statement, and an org whose primary is red would wear a permanent alarm. Under prefers-reduced-motion every animation stops and the red pill keeps a static ring and static stripes — the cue survives, the motion does not. Every selector in that block carries [data-band], which is the whole rule rather than a flourish: a media query does not raise specificity, so the bare .kmeter reset lost to .kmeter[data-band='CRITICAL'] and the siren went on shuddering at exactly the people who asked it not to. Caught by measuring computed animation-name in a real browser, not by reading it.

உங்கள் உலகம் காத்திருக்கிறது.
நிமிடங்களில் இலவசமாகத் தொடங்கவும். நீங்கள் தயாராக இருக்கும் போது பொருட்களை சேர்க்கவும் - எந்த மாற்றமும் இல்லை, புதிய உள்நுழைவுகள் இல்லை.
எப்பொழுதும் இலவசம் — கடன் அட்டை தேவையில்லை