உள்ளடக்கத்திற்குச் செல்லவும்
KAMOUniverse

மாற்று பதிவேடு

என்ன புதிது.

அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.

  1. · kamo-internal

    Write a scheduled-support description as rich text

    The description on **************** was a three-row textarea. It is the /kb/new writing surface now, minus pictures and the media library — those upload into KB media, which is the right home for an article and the wrong one for a settings field. The column holds two shapes from here on: a Lexical document on anything saved since, and bare prose on every row older than that, since nothing rewrites them. app/lib/richTextField.ts is the bridge. It detects a document by parsing rather than by a leading brace — prose like '{beta} customers only' is legal, and sniffing would send it through parseEditorState, which throws, opens the editor empty and loses the author's text on the next save. Legacy prose is wrapped into a document so it opens as editable text instead. An emptied editor still serializes to one blank paragraph, so what gets stored is decided by the plain text the editor hands back, not by the JSON — a cleared description reads as absent everywhere downstream. Every reader is fed the words rather than the document: the booking wizard's picker card keeps its two-line teaser, and the confirm step gains the rendered description, which is the one place the formatting an author wrote is actually read. RichTextView renders it through the sanitising lexicalToHtml, and shows a legacy value as escaped text — whatever a paste once carried into a plain-text field is exactly what would otherwise run. KbArticleEditor gains placeholder and allowMedia; the note preview typography in globals.css is now shared with a .kamo-richtext reading surface, which differs only in that its links work. Requires the MediaService that knows descriptionPlain — deployed and verified before this landed, since its ObjectMapper rejects unknown properties.

  2. · kamo-login

    Add the workspace picker

    A sign-in that names no organization now comes back with a list instead of a session. This is the client half: /api/login passes that payload through (before the no-OTK check, which would otherwise report a perfectly good sign-in as a failure), /api/session/select relays the choice and plants the session exactly as /api/login does, and a third step in the card asks which workspace. Built around what members actually do, which is open the same workspace as last time: the remembered one is preselected and named on the button, so this reads as a confirmation rather than a survey, while every alternative stays one keystroke away. Arrow keys move, Enter opens, and search appears past eight workspaces. The alias under each name is not decoration. Organizations here genuinely share names -- there are four called "Acme Corp" -- so it is the only thing that tells two rows apart. One flourish: a single rail that springs between rows via shared layout, so keyboard selection is legible without a focus ring on every element. Honoured against prefers-reduced-motion, and the step only mounts after the password, so the serialized opacity:0 that makes Framer initial states block first paint never applies here. Still unreachable in production: SecurityService gates the picker behind **************** which is off. This lands the UI first so the flag has something to switch on.

  3. · MediaService

    Carry the type description in both of its shapes

    The description is a Lexical document now, so the service stores the plain-text mirror the client sends alongside it and hands readers whichever shape they can actually use. WebinarTypeDTO gains descriptionPlain for a surface that wants the words rather than the document. The public booking DTO and the scheduled-webinar DTO get plain text only — neither an anonymous page nor a reminder has a Lexical runtime, and printing raw editor state at a reader is worse than nothing. The mirror is written unconditionally beside the document: a request carrying no mirror is carrying prose, so clearing it puts the row back into the legacy shape readers already fall back to, where a stale mirror beside a fresh description would be words that no longer match. Requires the webinar_type.description_plain column, applied by KamoInitializer and verified present in Yugabyte before this landed.

  4. · InitializerService

    Rich-text description columns on webinar_type

    Mirrors the shared-lib entity change. Two things the table needs before MediaService maps them. The description column has to be unbounded: it holds a Lexical editor state now, and a varchar ceiling would not truncate, it would reject the whole save with a value-too-long error inside a @Transactional update that then rolls back and leaves nothing to explain it. Check-then-alter rather than a bare ALTER, because the entity has declared TEXT since the table was created — so on every database this platform runs the probe short-circuits and no DDL is issued at all, which matters on YugabyteDB where an unconditional ALTER COLUMN ... TYPE would log a failure on every startup. description_plain is new: the plain-text mirror, nullable with no default, because null is the honest value for every row that predates the editor and is what tells a reader to fall back to description. Verified against the live cluster: description was already text, and both columns are present after the run.

  5. · kamo-shared-library

    Store the type description as rich text

    The description on **************** is a Lexical editor state now rather than a line of prose, so the column has to be an unbounded TEXT bound as a long varchar — a bare @Lob on a String maps to a PostgreSQL large object (oid) on Hibernate 6, which this column is not. DESCRIPTION_PLAIN is the plain-text mirror the editor writes alongside the document, for the same reason TrainingSlide keeps bodyText next to bodyJson: only a Lexical runtime can read a Lexical document, so every consumer wanting the words would otherwise have to ship a parser for an editor's internal format. RichTextValues reads either shape, because rows written before the editor landed still hold bare prose and nothing rewrites them. PublicWebinarTypeDTO keeps serving plain text: an anonymous booking page has no Lexical runtime, and a public field silently changing from prose to JSON would print raw editor state at a visitor. CreateWebinarTypeRequest gains ignoreUnknown — MediaService builds its own ObjectMapper from a bare new ObjectMapper(), so FAIL_ON_UNKNOWN_PROPERTIES is at Jackson's default of true and every future field here is a deploy-order trap.

  6. · SecurityService

    Let a member sign in without a host naming their org

    Adds the second half of the split: a sign-in that names no organization authenticates the user, then asks which workspace. POST /api/security/session/select turns one choice into a session, and SignInCompletionService holds every gate between a correct password and a usable session so login and selection cannot drift apart. Reached in two cases. A host that names no organization always defers -- before now that was a sign-in that could not succeed at all, so nothing is taken away. The platform host is the case the change exists for: login.kamocrm.com resolves to KamoCRM Inc., so without this it goes on scoping every sign-in to that one org and a member of a domainless org still cannot get in. That second case ships behind **************** default FALSE. Turning it on makes a member of several orgs receive a workspace list instead of a session, which a client that cannot render one reads as a failed login -- 17 of 53 users have more than one membership. It goes on with the release that teaches kamo-login the picker. Until then this release changes nothing for anyone signing in today, and a custom domain keeps the original path either way. Disclosure rules are preserved deliberately. A correct password that can enter nothing is refused in the same words as a wrong one, or the form becomes an oracle for valid credentials. The workspace list is never an authority: selection re-proves membership, so an edited, stale or invented list gets a refusal. The selectToken is not a session -- single-use, five minutes, a user id and nothing else. **************** correctly flagged the new endpoint as pre-session; it is baselined with the reasoning, since an endpoint whose purpose is to mint a session has none to resolve. Org ids cross every new wire as digit strings. 1200096283020229788 is a real id here and has no exact double representation. 923 tests pass.

  7. · InitializerService

    Remember the last workspace a member signed in to

    users.last_org_id backs the workspace picker. An organization is no longer named by the hostname, so a member who belongs to several chooses one after signing in; this column stops that being a fresh decision every morning. One statement, unlike UserIsFakeMigration's four. That column ends up NOT NULL with a DEFAULT and Yugabyte rejects ADD COLUMN ... NOT NULL on a populated table. This one is deliberately nullable with no default and no foreign key: NULL is the meaningful value for "has not chosen yet", which is every account predating the picker, and a foreign key would let a deleted org block a sign-in. A stale id is harmless -- the picker only uses it to preselect a row it is already listing. Already applied to the live database, since this service has no CI and pushing it ships nothing; this is what gives a fresh environment the column.

  8. · kamo-internal

    A file may be dropped anywhere on a chat window, not only on the input strip

    The drop target was the composer surface alone — a few dozen pixels at the bottom of a window that is otherwise all messages. A screenshot let go over the conversation did nothing at all, and worse: the browser's default for an unclaimed file drop is to NAVIGATE the tab to the file, which takes every open tool window and every unsent draft with it. `useComposerIngest` gains `bindDropTarget`, which binds the drag gestures to an element the composer does not own, and `ComposerSurface` resolves that element from the DOM — the shell's own `data-tool-window`, or `data-chatbox-root` for a chat that draws its own frame. Native listeners in the CAPTURE phase, so nothing inside the window can take the gesture first or stop it short before the window has seen it; only drags carrying files are claimed, so the AI chat's folder tree keeps its own drag-and-drop untouched. A file drag is claimed even while attaching is locked — refusing the file must not cost the member the page. One ingest per composer still, so the window and the surface can never stage the same drop twice. Member chat, support tickets, social conversations, AI chat and SMS all get this from the one surface they share; a composer with no window around it keeps the old behaviour.

  9. · kamo-internal

    Drop the greeting block and put the masthead on one row

    The launchpad's masthead carried a kicker, a "good afternoon" heading and a status line of apps/title/trial/date beside the logo, and the member's own controls floated over it absolutely — so the header reserved a 150px gutter and ~130px of height to say the time of day. The logo and the controls now share one flex row, which is the height of the logo alone, and the grid starts that much higher.

  10. · kamo-internal

    Move Binders and Uploads to the right of the strip

    The strip now separates what a document manager IS from where it takes you. Left: Originals, Modified, Common — three views of the same documents. Right: the four icon shortcuts, then Binders and Uploads as destinations, keeping their labels and their paint. Out there they are no longer children of <Tabs>, so they set activeTab themselves and mark their own selected state — there is no indicator outside the group to do it. A painted tab shows selection with a white underline: on brown and blue that is the one cue that reads at a glance without disturbing the fill, and it reserves the 3px whether selected or not so the label does not shift when it is clicked. paintedTab also carries its own typography and sizing now. Inside <Tabs> those came from the parent's `& .MuiTab-root`; a standalone Tab inherits none of it and would have rendered at MUI's uppercase defaults.

  11. · kamo-marketing

    Set the headline in quotation marks

    Quotes around the banner line, as asked — but rendered from CSS (`open-quote`/`close-quote` against `quotes: auto`) rather than typed into the string, for two reasons. The pipeline has already translated `previewBanner.headline` into 19 locales since e67f59b landed. `translate.ts` re-queues a key only when it is missing or still equal to English, so adding quotes to the English value would leave all 19 translations exactly as they are — quoted English, unquoted everything else — and no run short of `--force` would ever notice. That is the trap the rename in e67f59b was written to avoid, and doing it again for punctuation would mean a third key name and a second full retranslation of a line that has not changed a word. And the glyph is not shared. German sets „…“, French « … », Japanese 「…」. One pair typed into en.json is one convention that the other 21 locales carry wrongly or drop; `quotes: auto` resolves against the `lang` on <html>, which [locale]/layout.tsx already sets per locale, so each gets its own. English resolves to the curly “…” that display copy wants rather than the typewriter pair. `quotes: auto` is the initial value and is written out anyway — `quotes` is inherited, so an ancestor rule added later could otherwise change what this renders without touching this block. Nothing else in globals.css sets it today. No dictionary change in this one, so no cross-repo push ordering to get wrong.

  12. · kamo-internal

    Expression — colour, Spirit Animal and logo as one decision

    Two corner controls become one. That split was an accident of the order the features shipped rather than a distinction anybody holds: a colour, a pattern and the organisation's mark are three LAYERS of one question — what does this pane look like — and they combine. Choosing them from two popups meant you could never see the combination you were making while making it, so the sheet opens on a live preview stacking all three exactly as the pane stacks them. THIRTY-FOUR SPIRIT ANIMALS, as SVG <pattern> defs rather than CSS gradients. Gradients do stripes and dots and very little else, and half this list is rosettes, polygonal patches, overlapping scales and a peacock's eye-feather. SVG draws real shapes, stays crisp at any pane size, takes the member's ink directly, and is markup rather than an asset — no request for the CSP to have an opinion about and no bytes to transmit. Shapes are declarative data turned into elements, so nothing reaches the DOM as an interpolated string, and every ink is hex-checked on the way in for the same reason the frame designer's colours are. Colour and pattern are stored separately and combine, because they are separate choices: changing your mind about the colour must not lose the animal. The pattern's default ink is the paper's own edge colour — its deeper companion — so "Sage + Leopard" means leopard in sage rather than leopard in some unrelated hue, which is what a member picking those two plainly meant. An explicit ink from the org ramp or their own palette overrides it. The pattern paints UNDER the pane's content and its glass, unlike the org mark which had to move in front to survive the blur. A texture belongs under the same frosted surface the colour is under; in front it would print zebra stripes across somebody's text. Applies everywhere the old controls did — widgets, app cards, pinned notes, the pinned-widget band — since all four already shared the pane chrome. PaneColorButton and PaneBrandControl are deleted rather than left beside their replacement: two ways to set one thing is how they drift.

  13. · kamo-internal

    Count on the Binders tab, and land on Selected Docs

    The tab reads "Binders (n)" so the count the Selected Docs binder holds is visible from the strip — including (0), which is the case worth seeing, since it says the binder is empty before the member goes looking for it. Entering the tab now opens Selected Docs. The member arrives here from choosing documents, so that binder is what they came for; making them find it in the list was a step with no decision in it. Opening another binder during a visit is not undone by the next sync — the "already landed" flag is a ref reset by unmounting, so it re-lands per visit rather than fighting the member mid-session. A new i18n key rather than a retitle of tabs.binders: the legal package dialog uses that one as a plain label and has no count to put in it.

  14. · kamo-internal

    The picture frame IS the pane, not something inside one

    Every other widget is content in the launchpad's frosted glass card, with a header, a title and a corner control. A picture frame is not: rendered that way it is a frame sitting inside a second, invisible frame, and the moulding the member designed becomes a detail inside somebody else's rectangle. So the registry gains `ownsSurface`, and declaring it takes away exactly the three things that fight such a widget rather than serve it — the glass surface, the background COLOUR swatch (a frame already has a mat colour and a moulding the member chose) and the background LOGO (a watermark printed across somebody's photograph). Both places that paint pane decoration honour it: the sortable grid and the pinned-widget band. What the header carried is still there as controls that fade in over the moulding — the frame's name on a legible plate bottom-left, the way into its editor and the remove control top-right. Dark and translucent rather than the launchpad's usual light chrome, because these sit on a photograph and a pale button vanishes the moment a bright sky lands in that corner. They stop pointer events from reaching the pane underneath, or reaching for the editor would start a drag. A flag on the registry rather than a check for this widget's id, so the second widget that paints its own surface needs no changes here.

  15. · kamo-internal

    Show which layers of a modified document were worked on

    A document has one modified record, so the Modified tab has one row per document. The column names the layers touched rather than which of two kinds a row was: Document once its bytes differ from the original's, Form Fields once fields have been laid over it, and "Not edited yet" for a record that was derived by opening an editor and then nothing was done. That last state is real and worth saying: opening an editor derives the record, so claiming a layer there would tell the member they edited something they only looked at.

  16. · kamo-marketing

    Trade the NOW LIVE badge for the headline it was hiding

    The top banner led with a six-character pill — bordered, uppercase, pulsing — and put the actual message after it. The pill now carries the message itself: "Running Your Business without Kamo is a Mistake." None of the badge chrome survives that. A 1px box around a full sentence reads as a button, `text-transform: uppercase` would shout a line written in sentence case, and 1.2px tracking is a badge measure rather than a reading one. The `::after` white wash and its `badge-pulse` keyframe go with them, along with the dead `.preview-banner-live` entry in the reduced-motion list — the animation was on the pseudo-element, so pinning the element never did anything. What is left is bold and white. #fff over #cc1111, the lightest stop of the gradient behind it, measures 5.25:1; the rest of the banner sits at 0.92 alpha, so the weight and that last 8% of white separate the headline from the changelog line beside it. The wrap breakpoint moves 640px -> 768px. That number was chosen when this slot held six characters; the single nowrap line now measures ~625px with the changelog copy after it, so at 640px the banner would spend the last ~15px of its single-line range overflowing its own fixed 38px box. 768 also lines the wrap up with the reduced-motion breakpoint below, so the banner stops shimmering and starts wrapping at one width instead of two 128px apart. The copy key is RENAMED, not re-valued (dictionary side, same change set). `translate.ts` queues a key only when it is missing or when its value still equals the English source — it has no notion of an English string that changed. Editing `nowLive` in place would have left all 21 locales translating "Now LIVE!" until someone thought to run `--force`. A new key is missing everywhere, so the next sync picks it up, and `writeLocaleFile` iterates en's keys, which prunes the old one from every locale. Until that runs, the English fallback merge in i18n/request.ts renders the English sentence rather than a raw key.

  17. · InitializerService

    Merge the two modified records into one with two layers

    Where a document has both kinds, the CLASSIC row survives — it is the one that may hold edited bytes — the design is repointed onto it, and the FORM_FILLABLE row is deleted. Losing either side would be losing member work: the bytes are edits made in LibreOffice, the design is field placement done by hand. The unique index becomes one row per original rather than one per (original, kind), and is only built after the merge leaves no document holding two. modified_kind is deliberately left in the table. Dropping it would break any DocsService pod still running the previous image mid-rollout, which names the column in every Img SELECT; it can go once the fleet has turned over. The earlier runner no longer creates the old per-kind unique index, so the two migrations do not spend every run fighting over it. Applied to prod ahead of this push: 8 modified rows, 4 carrying designs, 2 with diverged bytes, none doubled, nothing lost.

  18. · kamo-internal

    Picture Frame — a member's own photos and videos, in a frame they build

    The first INSTANCED widget. A member hangs as many frames as they like, each with its own contents, its own moulding and its own place in the grid, all from one registry entry. THE MITRE. A frame is ONE moulding profile repeated on four sides and cut at 45° where they meet, which is how a real frame is made and the reason this model has no "top border" or "left border" — there is nothing to keep in step because there is only one thing. A member designs the cross-section, the four sides fall out of it, and every corner is a 45° line BY CONSTRUCTION rather than by a style anyone can get wrong. Each side is a trapezoid clipped out of a full-size box and painted with a gradient running from the outer edge inward; the sloped ends are the mitre, and four of them tile the border with no overlap and no seam. `border-*` colours would have been free and were the wrong tool: they cannot carry a multi-band profile or a texture, and a member who edited one side would have three more to fix. THE PICTURE. Aspect ratio always kept, frame always filled, overflow cropped. Letterboxing inside a moulding looks like a fault in the frame rather than a property of the photo. Instances travel as `widget:picture-frame#<uuid>` through the layout, the pane store and the show/hide preference, so a frame keeps its own size, colour, logo and position and hiding one leaves its siblings alone. The id vocabulary moved into widgetIds — pure and covered, because those strings are persisted in every member's stored arrangement and their shape is load-bearing. An instance defaults to SHOWN: the member made it, and making it is the decision to have it. The editor at **************** puts the frame at hanging size on the left and everything that changes it on the right. The preview is the SAME component the launchpad renders, not a facsimile — a moulding that looks refined at 90px is a slab at 400, and two renderers would drift with the preview being the one that drifted. Saves are debounced and optimistic: designing a frame is a hundred small changes in a minute, so a save button would mean losing the lot to a mis-click. Every colour in a stored design is pattern-checked before it reaches a gradient. These blobs are member-authored data being interpolated into a style attribute, and the check is what keeps a saved frame from being a way to write arbitrary CSS. Media reorders with arrows rather than by dragging: this grid sits inside a page reachable from a launchpad the member already drags panes around on, and a second differently-behaved drag surface is how people move the wrong thing. Arrows also work on a phone and with a keyboard. Uploads go one at a time — firing twenty concurrent multipart requests at one pod is how a member with a holiday folder takes the service down for everyone else on it.

  19. · SecurityService

    Frame and media endpoints, and seed the new rights

    Self only, and enforced in the QUERY. Every read and write resolves the member from the session and every repository call takes that id, including the single-frame lookup — a frame id is a UUID that travels to the browser and back, so a findById would hand somebody else's wall to whoever guessed one. There is deliberately no "manage another member's frames" endpoint: an administrator's control over this feature is the right, not the contents. Media goes through ImageService rather than being stored here, which buys content-addressed dedup (the same photo in three frames is one stored file), org storage accounting, and playback through the existing imaging proxy — which already speaks HTTP Range, so a video already seeks. Hashes are computed in the controller because the pipeline validates the caller's against its own and the caller here IS the server; that is what every other server-side uploader in the platform does. A slideshow's ORDER lives in the frame's settings blob, not as a column on each image: an imaging row is shared by every association referencing its bytes, so a sort column on it would belong to whichever frame wrote last. Deleting a frame trashes its media. Leaving the rows behind would keep billing the organization for a wall nobody can open, which is the orphan class this platform has been bitten by before. Ceilings: 12 frames a member, 120 items a frame, 100 MB a file. The last is well under the platform's other upload limits on purpose — this is wall decoration that autoplays behind a launchpad, and a 2 GB video would be fetched by every tab the member opens. The rights seed from MANAGE_OWN_BACKGROUND_IMAGES, which already says this member may decorate their own screen with their own uploads — the safest seed of the lot, since nothing in a frame is anybody else's data. ADDED deliberately does NOT seed, unlike every other widget: the others open on something the member already has and are useful the moment they appear, while an empty picture frame nobody asked for is clutter. Handlers name their session helper currentMember so the unauthenticated endpoint ratchet can see the guard — its scan reads one method body and does not follow calls.

  20. · InitializerService

    Create the picture_frames table

    One row per Picture Frame widget a member has arranged on their launchpad. A table rather than a preference blob because the photos and videos in a frame are imaging rows carrying its id in assoc_object_id, and an association needs something durable to point at. A frame id living only inside a member's JSON preferences would be a foreign key to nothing: nothing could enforce that the frame still exists, and clearing a member's preferences would silently orphan every byte they had uploaded. settings is TEXT holding JSON on purpose. It is the frame's appearance — read whole to draw one pane, written whole on save, never queried into — so as columns every new bevel or grain the designer grows would be another DDL change landing here before the service that maps it, for a value no query will ever filter on. @Order(0) per the schema convention: once the shared-lib entity exists, any runner that loads a PictureFrame through JPA emits these columns in its SELECT, so a late-ordered migration would never be reached — an earlier runner would die on "relation picture_frames does not exist" and take the run down first. NOTE FOR DEPLOY: SecurityService runs hibernate ddl-auto: none, so this table does not appear on its own. This service must be RUN before the picture frame feature works.

  21. · kamo-shared-library

    The schema, rights and storage domain behind frame widgets

    A Picture Frame is a member's own photos and videos on their own launchpad, and a member may hang several. This is everything the platform needs to hold one. PictureFrame is a row rather than a key in the member's preference blob because the media are imaging rows associated BY frame id, and an association needs something durable to point at. Member-scoped rather than user-scoped: the bytes are stored under an organization and counted against it, so a frame that followed a person between orgs would put one org's storage on another org's bill. Its appearance is one JSON column — read whole to draw a pane, written whole on save, never queried into — so a designer that grows a bevel next month does not need a DDL change ahead of it. ImageAssocType.PICTURE_FRAME scopes to the FRAME, not the member: several frames are several slideshows, and scoping to the member would make every one of them show everything. It buys content-addressed dedup for free, so a member who puts the same photo in three frames is billed for one. StorageDomain.PICTURE_FRAMES gets its own line rather than being folded into a general bucket. This is the one domain an organization's own people fill a few megabytes at a time with nobody administering it, and an org watching its bill needs to see that separately from the documents its business runs on. The coverage test would have failed the build otherwise, which is the point of it. Rights follow the shape every other widget uses — PICTURE_FRAME_WIDGET plus the four policy children — hung off MANAGE_OWN_BACKGROUND_IMAGES, the right that already governs the personal media a member puts on their own screen. Not a root: a widget whose grant is unreachable from the group that owns its data is a right nobody finds. Ids 229-233; 228 was taken. Taxonomy 219/156/32/31 -> 224/161/32/31.

  22. · kamo-internal

    Put the four member shortcuts on one row

    are one class of control, so they now share one row: the launchpad's top-right corner reads as a single strip rather than a two-line stack. styling; it uses the shared pillLinkSx like its neighbours, and collapses its label at xs as they do, so four shortcuts still fit a phone. The row wraps rather than overflowing when a member holds every one of them.

  23. · kamo-internal

    Position and colour for a pane's logo, and always let a member open the editor

    THE EDITOR WAS UNREACHABLE FOR ANYONE THE ROLL HAD NOT DECIDED FOR. `brandOf` answered null until a mark existed and the control was gated on the answer, so a member whose seeding had not run — or had not persisted — had no button at all and no way to give a pane a logo. An undecided pane is an unbranded one, not a missing one: `brandOf` now always answers, and the editor is always there. The pinned-widget band had the same symptom for a different reason. It renders outside SortableCard, which is what reveals a pane's chrome on hover, so its controls sat at opacity 0 permanently. It carries the rule itself now. POSITION. Nine anchors in the popup, drawn as a grid whose cells each hold a dot in their own corner, so the picker reads as the pane it stands for. The rotation had to move onto the logo itself: rotating the sizing box swung an edge-anchored mark around the pane's centre and out of the corner the member had just put it in. COLOUR. The mark can be painted a solid silhouette, through ThemeLogo's existing overlayColor — a CSS mask, so it reaches an arbitrary colour rather than pushing the artwork's own colours around. Two sources: the organisation's brand ramp from **************** deduped because an org that set only a primary has its light and lightest defaulted to the same value; and the member's own palette, using each colour's deep companion rather than its pastel paper, since a pale silhouette on a pale pane is a mark nobody can see. SIZE JOINS THE ROLL, between 0.55 and 1.1 of the pane. Every mark at exactly 0.85 was the tell that a page had been generated rather than composed.

  24. · kamo-internal

    Paint the master notes in the organisation's colours

    "Our Master Note" is the organisation's PRIMARY and "My Master Note" its SECONDARY, each as a pastel — an 18% tint on white. The pair reads as a set at a glance, and neither can be mistaken for one of the twelve post-it colours a member picks for their own notes. Derived from --color-primary / --color-secondary rather than a hex, so a white-label org gets its own brand. The edge is floored against slate and the paper deliberately is not: an org may set a near-white primary, and an 18% tint of that is just a paler sheet, but an edge mixed from it alone would be an invisible rule, pin and caret. Same reasoning as the page scrollbar tokens. One consequence, and it is the point: a master note has no palette any more. The colour is what says whose note it is, which is not a member's choice — so the swatch is gone from the pane, the card, the sticky and the editor, and KBService stops seeding one. `${color.edge}33` had to go with it. Appending hex digits to a color-mix() expression yields a declaration the browser drops silently, leaving borders with no colour at all; withStickyAlpha() takes both forms and every notes surface goes through it now. useMasterNotes also unions with the floating overlay's cache, the way selectPinnedPanes does — otherwise a master note edited in its sticky showed its mount-time text on the launchpad until a reload.

  25. · kamo-internal

    Put the two master notes on every launchpad

    "My Master Note" and "Our Master Note" are panes in the same sortable grid as everything else — dragged, resized and ordered like any other pane — and neither can be removed. The organisation's carries a turning conic-gradient ring painted from --color-primary, the only moving edge on the page, so the one pane that did not come from the member reads as such from across the room. Stilled to a static ring under prefers-reduced-motion. Both are pinned, so they arrive in the pinned list too; selectPinnedPanes drops them there, or each would be painted twice and the duplicate would be the copy without the badge and the edge. Fetched from /api/notes/master, which is NOT gated on the Notes app: the pinned note panes are the Notes app spilling onto the home page, and these are furniture of the home page itself. Every control the server would refuse is taken away rather than disabled: no unpin, no delete and no rename on either master note, and no colour, no toolbar and no typing surface on the organisation's for a member without the right. A read-only member gets a reader — the same rendering the pane uses — instead of an editor whose saves would be rejected after they had done the work. notesApi.saveNote routes by scope, so the sticky, the editor, the wall and the launchpad's colour picker stop needing to know that the organisation's note has no member and so cannot be reached by the per-id endpoint.

  26. · KBService

    Serve, provision and protect the two master notes

    GET /api/notes/master answers both and provisions whichever is missing. PUT /api/notes/master/org writes "Our Master Note", addressed by organisation rather than by note id, so there is nothing to guess and no other organisation's note to reach. Editing it takes MANAGE_ORG_MASTER_NOTE or ownership of the organisation — the owner floor is the anti-lockout guarantee, since the right is deliberately not seeded onto existing roles. GET /master deliberately takes NO notes right. VIEW_NOTES gates the Notes *app*; these are fixtures of the home launchpad, and "every member always has one" has to mean every member. The ordinary listings keep their VIEW_NOTES gate and now also return the organisation's note, so it is readable and searchable on the wall. The rules are enforced here rather than in the client: a master note cannot be deleted (409 — a fact about the note, not about who is asking), cannot be unpinned, and cannot be renamed. Title and pin are ignored rather than rejected, because the editor sends every field on every save and refusing the request would fail an edit to the BODY over a title the member never touched. Content keys follow scope: keyForOrg(orgId) for the organisation's note, so every member of the organisation can read it. Deriving it from whoever wrote it would make a shared note readable by one person. Provisioning is its own bean and its own transaction, and does every read before its first write: since the move to Yugabyte a SELECT after a write in one transaction can come back 40001 and abort it. A private method on NotesService would have been inert as @Transactional and run in the caller's transaction.

  27. · InitializerService

    Add the master-scope columns to notes

    MASTER_SCOPE ('MEMBER' / 'ORG' / NULL) and ORGANIZATION_ID, plus the two indexes the entity declares and a partial unique index per scope so one live master note per member and per organisation is enforced by the database rather than only by the find-then-create in NotesService. No backfill: every existing note is an ordinary note and NULL reads as exactly that. The master notes themselves are provisioned lazily on the first read that wants them, which covers memberships and organisations created after this runs without a second migration. @Order(0), like every column-add runner: the moment the shared library maps these columns, any runner that loads a Note through JPA emits them in its SELECT, so a migration ordered late is never reached. Applied to Yugabyte 2026-08-22: all seven statements applied, both columns and all four indexes verified present.

  28. · kamo-shared-library

    Model the master notes and the right that guards the org's

    A master note is the one note a member — or an organisation — always has. "My Master Note" is provisioned per membership; "Our Master Note" is one per organisation and appears on every member's launchpad. Both are ordinary `notes` rows carrying a MASTER_SCOPE, not a new table, so the Lexical editor, autosave, versions, colours, the floating sticky and the RAG publish all keep working with nothing to keep in step. An ORG row carries a NULL MEMBER_ID on purpose: every member-scoped finder matches `n.member.id = :memberId`, so the organisation's note can never be returned by one member's private listing, update or delete. Reads that should see it go through the new `…VisibleToMember` finders, which widen to it explicitly; writes keep the member-scoped pair and so cannot reach it at all. MANAGE_ORG_MASTER_NOTE (228) is a standalone root with a null serviceType, following MANAGE_ORG_BRANDING: the role, job-title and department editors only render a service's section when the org has that app enabled, and the org master note is on every launchpad whether or not Notes is switched on. Not parented to VIEW_NOTES for the same reason — KBService serves the note without a notes right.

  29. · kamo-internal

    Brand a third of a member's panes with the org logo, and let them tune it

    A third of a member's panes wear the same faint org mark /leads and /calendar carry, at pane scale. Which panes, and how, is rolled once and kept. WHAT THE ROLL DECIDES. A branded pane's mark depends on the shape of the pane it is going into, because a wordmark and an icon fit different rectangles. A square pane is the stated coin toss between the two. A wide one leans to the wordmark at three in four — a wordmark IS wide, and a square icon in a four-column strip is a dot in a field. A tall one always takes the wordmark and turns it ninety degrees clockwise, which fits a tall pane better than either mark lying flat. And every branded pane gets a few degrees of tilt whatever else was rolled: a mark laid perfectly square on a rectangular pane reads as a layout mistake, and a few degrees off true reads as a stamp, which is what it is. The tilt goes either way, because a page of marks all leaning left reads as a rendering bug. PER PANE, NOT ONCE EVER — the difference from the colour seeding, and the reason a pane that is NOT branded has that recorded rather than left blank. Absence means "this pane is new, roll it", which is what lets a widget switched on next month, a note pinned next week or an app newly granted take the same pass on its first appearance while nothing already on the page moves. It is also the backfill: a launchpad arranged before any of this existed carries no decisions, so every one of its panes is new by that definition and is rolled on the next load. No migration, and no member the feature fails to reach. THE ORG'S OPT-OUT COMES FIRST. An organization whose bgWatermark is NONE has turned background logos off; a new surface is not an exemption from a preference they already expressed, so those panes render no mark whatever was rolled. THE EDITOR. A control beside the colour one opens a popover for the mark itself: full, simple or none, plus rotation, size, hue and contrast. Everything in it is live — the value goes straight into the preference the pane renders from, so a member dragging the hue slider watches the pane behind the popover change as they drag. Stored against their member record like every other pane preference. The filter is hue-rotate and contrast rather than the page watermark's grayscale, because grayscale strips the hue and would have left the member a slider that moved nothing. Reset-to-defaults on the widgets page deliberately does NOT clear brands: re-rolling a logo would hand somebody a different launchpad every time they pressed it, which is not a reset.

  30. · kamo-internal

    Edit in Classic Mode, and redraw when it saves

    The form-fillable editor can now branch out to LibreOffice. The server derives the classic version on the way if the document has not got one, which is the point of offering it from in here — a member who has only ever added form fields does not have a classic copy to open yet. When that other tab saves, the editor reloads. The pages its fields sit on have just changed underneath it, and it would otherwise keep drawing the ones they were placed over. It says so rather than redrawing silently: the difference between "the document updated" and "my fields jumped for no reason", and after a reflow they may genuinely need moving. The editor that DID the save is deliberately not reloaded — it already holds the bytes it wrote, and reloading it would discard anything typed since. Ids are compared as strings end to end. Two different int64 member ids compare EQUAL as numbers, which would mean a colleague's save reloading your editor; the test asserts that equality directly so the reason is not just a comment.

உங்கள் உலகம் காத்திருக்கிறது.

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

எப்பொழுதும் இலவசம் — கடன் அட்டை தேவையில்லை