மாற்று பதிவேடு
என்ன புதிது.
அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.
· kamo-internal
Give the Linux options a Tux who hops
The icon beside "Linux Desktop" and "Dev Machine" was an 18.6 KB static penguin. It is now a 1.4 KB vector one who bounces, built from flat ellipse geometry so he still reads as a penguin at the 16 px the buttons render him at. The animation is CSS keyframes inside the SVG itself. Browsers run those for a file loaded through <img>, so this costs no JS, no re-render and no second request, and it honours prefers-reduced-motion on its own. He hops twice and then rests, because a penguin bouncing perpetually beside a nav label is agitating rather than charming. Vector rather than a GIF because four flat colours and no gradients is the case vectors win outright: 555 bytes gzipped against 19.7 KB for the same 26 frames as a 64 px GIF, and exact at every size instead of soft on a retina panel. Animated WebP was worse than the GIF here — a palette is what flat colour wants, and WebP's predictors have nothing to work with. AdministratorCard had its own copy of the <img> rather than calling the shared wrapper, so the artwork had two homes and this swap would have needed two edits. It now calls LinuxDesktopNavIcon.
· DocsService
Announce a classic save to the member who made it
A WOPI save on a CLASSIC version now publishes docs.classic-saved.{memberId} so the Kamo PDF editor, if it is open on the form-fillable version, can redraw the pages the fields sit on instead of showing the ones they were placed over. Core NATS and unstreamed, matching the training badge: this is a live nudge, not a durable log. A member whose socket is down misses it and sees correct bytes the next time they open the document, which is the behaviour without any of this. Ids go on the wire as strings. They are int64s past 2^53, and a bare number is rounded by JSON.parse — the editor would then compare against an id that does not exist and ignore every frame it was sent. Nothing in the announce path can fail the save: the bytes are already stored, and a 500 here would tell the member their edit was lost when it was not.
· MediaService
Relay "the classic version was just saved" to the editor
DocsService publishes docs.classic-saved.{memberId} after a WOPI save lands on a CLASSIC version; this puts it on /topic/docs/saves/{memberId} over the STOMP connection the app already holds, so a Kamo PDF editor open on the form-fillable version redraws instead of sitting on the pages the fields were placed over. Built as the twin of the training relay deliberately — same core NATS, same one-token subject rule, same per-member destination — because a relay with its own failure modes would be a second set to learn for no gain. The topic is guarded per member on SEND as well as SUBSCRIBE. Unguarded it would publish a live map of which colleague is editing which document, and a forged frame would make someone else's editor discard unsaved field placements and reload.
· DocsService
Save As branches a new original, not a new copy of one
PutRelativeFile was unimplemented, so "Save As" in the LibreOffice editor did nothing. It now does what the member means by it: the bytes become a brand-new ORIGINAL, as if they had uploaded the file, and that original immediately gets its own CLASSIC version — which is the id handed back to the editor. Saving as therefore lands them exactly where uploading and opening would have: an untouched original, and a classic copy they are editing. Returning the new original's WOPI source instead would put the editor straight back to writing over an upload, which is the thing this model exists to stop. The suggested-target header is not trusted to be a filename. Docs sends plain UTF-8 rather than the modified UTF-7 the spec asks for, and very often sends only an extension meaning "same name, new format" — taken at face value that produces documents literally named ".docx". getWopiBaseUrl is shared rather than copied: a drifted second copy would hand the editor a callback host its token was never minted for.
· InitializerService
Move existing designs onto FORM_FILLABLE rows
Before versions existed, putting form fields on a document attached an esign_templates row straight to the original, and the Modified tab was "originals that happen to have a design". A design now belongs to a derived row and Originals holds only what was uploaded — so without this, every document a member had already put fields on stops appearing under Modified. Derives the missing rows, then repoints the templates at them. Both are idempotent on their own terms: the insert is guarded by NOT EXISTS, and once a template's img_id names the derived row it no longer matches an original's id. The derived row shares the original's dat_id rather than copying bytes, for the same reason the live derive does — nothing writes an ImgDat in place. Applied to prod ahead of this push: 4 documents derived, 4 templates repointed, 0 left pending, each sharing its original's bytes.
· kamo-internal
Seed 13% of a member's panes with a colour, and offer glass everywhere
TWO THINGS. A SHARE OF THE LAUNCHPAD ARRIVES COLOURED. On a member's first load — once ever — each pane gets a 13% chance of a random palette colour. Enough that most people find one or two coloured panes and the page has some life in it; few enough that it never reads as a decision somebody made for them. Every pane it paints is theirs to change or clear from that moment on. Never twice, and the flag says so rather than the emptiness of `colors`: a member whose roll painted nothing, or who cleared what it painted, has still been seeded, and re-seeding them on the next load would repaint a launchpad they had deliberately made plain. It fires only once the pane list is COMPLETE — widgets resolved, notes fetched, nav options gated — because a pane that had not appeared yet would miss its only chance, and it writes immediately rather than through the debounce, cancelling anything the debounce was holding: that pending write carries a pre-seed snapshot and would undo the one write that has no second chance. GLASS IS A COLOUR NOW. The product's own translucent panel is a swatch, so a floating note can be set back to the material every other element on the page wears — the one surface a member could not return to the original look. It replaces the menu's "no colour" swatch, which never meant no colour: on a note it meant the default yellow, which is a swatch that lies about what it does. An UNSET colour keeps meaning exactly what it always meant, so nothing written before the palette existed is quietly repainted. Its bg and edge are real hex on purpose. Fifteen places take the resolved colour's edge for a scrollbar, a rule, a caret or a chip, and the dock's hex head feeds bg through a hex darkener; a translucent value would fall out of all of them as a fallback colour. Only the paper itself is special-cased. And it stays out of STICKY_PALETTE, so a new note can never rotate into it — one that came out translucent by chance would read as one that failed to load.
· kamo-internal
One Modify menu, and Modified as its own records
"Open in Docs" and "Edit PDF" are gone. Each row has Modify, which offers "Edit In Form Fillable Mode" always and "Edit in Classic Mode" only where the LibreOffice editor can open the file. That last flag comes from the server per document rather than from the filename here: a form-fillable row reports the extension of whatever it is currently based on, so deriving it in the browser gets it wrong the moment a classic version exists. The Modified tab is now a query rather than a view. It used to be **************** — the same originals shown twice — and a modified version is a row of its own, which the originals query now excludes. Each row carries a Classic or Form Fillable badge, in a column shown only on that tab. Opening either editor derives the version it needs if there is not one yet, so the Modified list is invalidated on the way out whether or not the member is looking at it.
· DocsService
Derive modified versions instead of editing originals
Opening a document in the LibreOffice editor no longer hands over the original. It derives that document's CLASSIC version — once, reused every time after, and made unrepeatable by a partial unique index — and edits that instead, which is what keeps the Originals tab holding the bytes the member uploaded. POST /api/docs/form-fillable/{id} does the same for the row the Kamo PDF editor opens. Deriving costs nothing until it has to: a new version points at the original's existing ImgDat, and saveImgContent writes a fresh one on the first save, so the copy owns its bytes from the moment it differs and never before. A classic save rebases the form-fillable row onto the new bytes. The fields are addressed by page position and are deliberately NOT moved with them: an edit that reflows pages can misplace them, which is the trade taken over refusing to let a member edit a document they have already put fields on. A failed rebase is logged and swallowed — a stale form-fillable row is still openable, and losing the just-saved classic edit to it would be worse. The session-map-to-KToken mapping now has one implementation rather than one per controller: it decides what clearance a request carries, and two copies are two chances for one to stop reading securityLevel and hand out PUBLIC.
· InitializerService
Add imgs.modified_kind and imgs.original_img_id
Both stay nullable — null kind means the row is an original, so there is nothing to backfill and no NOT NULL step, unlike the usual column-add shape. The partial UNIQUE index is the "at most one version of each kind per original" rule put where it cannot be raced: two tabs both missing the same lookup and both deriving would otherwise leave a member with two classic copies and no way to know which one their edits reached. Applied to Yugabyte ahead of this push, per the ordering that took binders down: the shared-lib entity maps both columns the moment it ships.
· kamo-shared-library
Split a document into its original and its modified versions
imgs.modified_kind + imgs.original_img_id. Null kind means the row IS an original — the bytes a member uploaded — which is the correct reading for every row that predates this and is why neither column needs a backfill. A derived row resolves its bytes through an ImgDat rather than through original_img_id, so the reference is nullable and uncascaded on purpose: trashing an original removes its metadata, not its content, and its versions go on working.
· kamo-internal
Resizable, recolourable panes — and pinned notes join the grid
FOUR THINGS, one grid. CUSTOM SIZING. A widget can now declare `custom`, and all five do. The registry's width and height stay the DEFAULT — custom adds a handle, it does not take the design's opinion away — and what the member drags it to is stored against their member record, so a widget they made taller is taller wherever they sign in. The handle is keyboard resizable too: it is the only way to change a pane's size, and leaving it pointer-only would strand anybody who cannot use a mouse with whatever their organization chose. HALF ROWS. The grid's track is now half a unit, which is what lets an app pane be shorter than a widget: two app tiles stack in the height one widget occupies. Two half rows plus the gap between them is still 236px — the height a launchpad card has always been — so nothing else on the page moved. App panes resize by row only; one app card per column is the launchpad's rhythm, and a two-column-wide launcher tile is a hole in the grid with an icon in it. Docs, Calculator and Support start two rows tall because their card IS a list; everything else starts at half a row and drops to a compact form that shows the icon and the name without a blurb clipped mid-sentence. BACKGROUND COLOUR. The notes palette, the notes menu, on every pane. Painted BEHIND the pane's own glass rather than on it — a launcher card is translucent by design, so tinting the card would wash the colour out to nothing. A widget takes the control INTO its own header, because it already has controls in that corner and an overlay would land on top of its page arrows; an app card and a note get the corner overlay, and the pin chip steps aside to make room. PINNED NOTES ARE PANES. They were a band above the grid, which meant a member could arrange every pane on the page except those. Now they are dragged, resized and coloured like anything else, and still one click from being picked up as a floating sticky. A note that IS floating keeps its slot, greyed and inert, rather than vanishing: removing it was tidy and wrong — the pane the member had positioned disappeared, everything after it jumped a row, and closing the sticky shuffled the whole launchpad back. Its colour control writes to the NOTE, not to the pane store, so one note is not yellow in two places and blue in a third.
· kamo-internal
Say Everyone when an event invites the organization
· kamo-shared-library
Count an event's guests without loading them
· kamo-internal
Invite departments, job types and the whole organization
· kamo-internal
Offer departments and job types, and pick several at once
· kamo-internal
Three more widgets — unread mail, leads needing action, today's timecard
The widget system paid for itself: each of these is a registry entry and a component, inheriting the grid, the drag, the ordering, the per-member show/hide, the settings screen and the five-right policy without any of those places learning its name. UNREAD MAIL takes its count from the websocket EmailService already pushes and its envelopes from the metadata index, because a pane that is on screen all day must not spend a member's IMAP connection budget on a summary. The number is therefore never stale and the list occasionally is, which is the right way round. LEADS NEEDING ACTION ranks by consequence — calls already missed, then calls about to be, then the leads whose missing comment is blocking this member from accepting more. It reads through the EXISTING grid endpoint rather than a new one, deliberately: a second path would have to re-implement the permission floor and the §164.312(b) PHI access record, and one that got either subtly wrong would be an audit hole that looks exactly like a working feature. TODAY'S TIMECARD shows where the member stands, what they have actually worked (meals and breaks taken out, which is why it is derived from the punches rather than from elapsed time), every punch on the record, and the punch button. It still ASKS before it writes, through the same confirmation dialog the nav control uses — a punch is a wage record only an HR administrator with a written reason can correct. That widget also needed the system to grow one thing: an availability HOOK. Whether a member punches a clock is a persisted employment fact rather than a right, so it has to be fetched, and offering a clock to somebody who does not punch one would be worse than no widget at all. A null answer while it is unknown is load-bearing — the launchpad waits on it rather than rendering a pane and taking it away again. /messages now opens a message from ?folder=&uid=. The mail widget hands out that link and, like the calendar before it, every item in it previously landed on the folder and left the member to find the thing again themselves.
· SecurityService
Seed the mail and lead widgets, and add a universal seed for the timecard one
The mail and lead widgets follow their data rights exactly as the calendar ones do — ACCESS_MAIL and VIEW_LEADS — so a holder gains no data they could not already reach and the widgets do not ship invisible. The timecard widget has no data right to inherit from, because a member's own punches are gated by their persisted enrollment rather than by a right. Seeding it from MANAGE_TIMECARDS would have handed it to the HR administrators, who mostly do not punch a clock, and to nobody who does. UNIVERSALLY_SEEDED_RIGHTS is that gap, with a deliberately high bar written into its javadoc: a right belongs there ONLY when the capability behind it is gated by something other than a right, so granting it to everybody grants nobody anything they could not already do. It is not a shortcut for 'we would like most people to have this'. REQUIRED is excluded from every one of these, as everywhere: it is the one policy that takes a member's choice away, and that is an organization's decision to make.
· EmailService
Serve the home-page unread digest from the index, never from IMAP
A widget on the home page is on screen for everyone all day, whether or not anybody is reading mail — and a member's IMAP connections are a shared budget of ten per user and IP for the whole pod. So the unread digest reads email_metadata.search_index and does NOT fall through to the mail server the way the mail client rightly does; one widget that spent a connection per home-page load would starve the client it summarises. The consequence is that the LIST is as fresh as the last sync, and the widget is built around that: the count beside it comes from the live unread snapshot the websocket already pushes, so the number people act on is never stale even when the envelopes are a few minutes behind. It also reports whether the inbox has ever been indexed, because 'no unread mail' and 'nobody has opened this mailbox' are the same picture and very different facts. Envelopes only. body_text is in the index and is deliberately not sent: a home page is the most over-the-shoulder surface in the product.
· kamo-shared-library
Three more home widgets, and the one that cannot have a parent
Unread mail hangs off ACCESS_MAIL and leads-needing-action off VIEW_LEADS, both by the same rule as the calendar widgets: a widget is gated by the right that governs the data it shows, so granting the widget can never be a way round that gate. TIMECARD_TODAY_WIDGET is the exception, and it is a deliberate one. A member's own punches are not modelled as a right anywhere in this platform — enrollment is a persisted employment fact precisely because a right would leak ON through a grant-all role, god mode and the child-org template, and would vanish from a terminated member exactly when HR must produce their final timecard. The gate on that data is therefore stronger than a right, not missing, and the widget honours it before it renders. Rather than force a wrong parent or quietly delete the invariant, the test now takes a NAMED allowlist with the reason attached. Totals move to 218/155/33/30 and 63 roots.
· kamo-internal
Search departments and job types alongside people
· kamo-internal
Model an event's guest audience on the client
· EmailService
Publish and accept an event's guest audience
· EmailService
Materialize an event's audience into its guest rows
· EmailService
Resolve an event's guest audience into people
· kamo-shared-library
Map an event's guest audience and each attendee's source
· InitializerService
Add the DDL for an event's guest audience
· kamo-internal
Put live widgets on the launchpad, starting with two calendar strips
A widget is a live pane that shares the grid, the drag and the ordering with the app cards rather than sitting beside them. Adding the next one is a single registry entry plus a component: it inherits the grid, the per-member show/hide, the settings screen and the five-right policy without any of those places learning its name. The launchpad's masonry had to go. Masonry positions items in independent columns, so nothing in it can ever be two columns wide, and a widget system whose widest size is 'all of them' cannot live there. A real grid gives back a UNIT — one column of width, one row of height — that means the same thing to a card and to a widget, which is what lets a member drag one past the other and still get a page that lines up. No dense auto-flow: dense backfills gaps by pulling later items into earlier holes, and the order here is the member's. Widgets pinned in place by their organization render in their own band ABOVE the sortable grid. Leaving them in and merely refusing to pick them up would still let a movable pane be dropped in front of one — the same outcome the policy exists to prevent, reached from the other side. Show/hide is stored as DEVIATIONS, not as a list of what is on. Storing 'these are on' would freeze a member's page at the moment they last touched it: a widget added to the platform next month, or newly granted to them, would never appear. Storing only where they differ from their organization means the defaults keep flowing through, and Required overrides a stored 'off' so a policy change reaches exactly the members it was aimed at. The two calendar widgets show ONE EVENT PER UNIT OF WIDTH — four side by side on a wide launchpad, one on a phone — with arrows walking through the rest in pages of that same size. Slots are measured from the element rather than read off a breakpoint, so a widget in a narrowed grid or at an odd zoom is told the truth. Events in progress carry a live progress bar, and every 'is this happening now' decision offsets by the server clock: a device running four minutes fast must not show a meeting under way before anybody is in it. Changes arrive over one shared STOMP connection for however many widgets are on the page, with a five-minute poll and a refetch on tab focus underneath it. **************** is where a removed widget is found again — the launchpad leaves no trace of one by design. Self only: a member's arrangement is stored against their own session. Every switch that refuses to move says why, because three of the four policies are the organization's and not the member's. /calendar now opens an event from ?eventId=. Both widgets and the notification cards hand out that link, and until now it landed on the current month with no indication which event was meant.
· SecurityService
Seed the calendar widgets to everyone who can already read the calendar
Both widgets show a member exactly what /calendar already shows them, so a calendar reader gains no data they could not reach — which is what makes this a safe seed rather than a grant. Without it the widgets would ship switched off for every organization on the platform and land invisible. ADDED, OPTIONAL and MOVABLE seed. REQUIRED deliberately does not: it takes away a member's ability to clear their own home page, and no deploy is entitled to make that choice for an organization — an administrator turns it on when they mean it. NEW_RIGHT_SEEDS moves to Map.ofEntries; Map.of tops out at ten pairs and this is thirteen.
· EmailService
Serve and announce the home-page calendar widgets
CalendarWidgetService answers what a widget actually needs and the calendar page hasEarlier/hasLater so an arrow greys out instead of offering an empty page. The two scopes are disjoint on purpose — the organisation's shared calendar for one widget, the member's own for the other — so a company meeting appears once rather than on both panes side by side. Neither the calendar ids nor the member id come from the request: the caller passes the session's org and member and the service resolves the rest, so a request naming anything else has nowhere to say so. CalendarChangePublisher pushes every write to /topic/calendar/org/<orgId> or **************** always AFTER the commit. Publishing inline would send the nudge while the write is uncommitted, and a widget that answers a nudge by re-reading would race the transaction and usually win — redrawing with the state it already had and never hearing about the change again. Both destinations are guarded in WebSocketConfig, whose per-org guard is now a prefix list like the per-member one.
· kamo-shared-library
Give every home widget a five-right policy, and page the calendar for it
Ten rights arrive as two five-right blocks — one per widget — hanging off VIEW_CALENDAR, because a widget must be gated by the right that governs the data it shows or granting the widget is a way round that gate. Each block is the widget itself plus the four policies every widget carries: Added By Default, Optional, Required and Movable. The suffixes are a contract, not a naming style: the widget resolver derives all four from the parent's name, so a widget shipping three of them would silently treat the fourth as off for everybody with nothing to grant. RoleRightHierarchyTest now pins that shape for every widget there will ever be. The pinned taxonomy totals move to 203/141/32/30 and 62 roots, which also picks up OVERRIDE_CHAT_POLICY — added in 85b6607 without updating the counts, so the test was failing at HEAD. three or four events and walk either way from now, and serving that from findVisibleToMemberInRange would read a member's whole calendar on every home-page load and every websocket nudge to throw nearly all of it away. The opening page is the one query keyed on endTime, so an event already under way stays on the widget instead of falling off it the moment it begins.

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