மாற்று பதிவேடு
என்ன புதிது.
அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.
· EmailService
Who caused a notification travels with it, so the bell can draw them as a member
- NotificationRequest gains actorMemberId/actorName; the old constructor stays for every producer that predates them. - raise() keeps the pair only when both halves are there, the name fits its column, and the title or body really spells it: a name the text does not contain has nowhere to be drawn, and one past 255 would otherwise fail the insert and take the whole notification with it. - toPayload() sends actorMemberId as TEXT beside actorName - the list, browse and the socket frame. - /api/internal/notifications reads actorMemberId (text or number) and actorName. - /api/notifications/self (the browser's unread-chat card) keeps the actor only when that member is in the caller's own organisation. The id is the browser's word; it may name the sender it saw, but not have somebody from elsewhere (an Exec2Exec counterpart, a stale id) drawn as a colleague. Needs kamo-shared-library 727c0ec2 and the DDL in **************** applied to production by hand on 2026-09-23 before this was pushed. Tests: MemberNotificationServiceTest (+4), NotificationSelfRaiseActorTest (4). Full suite: 1912 run, 0 failures, 1 skipped.
· kamo-shared-library
A notification can say whose doing it is
MemberNotification gains ACTOR_MEMBER_ID (INT8) and ACTOR_NAME (255): the member who caused it - who dealt you in, whose meeting is starting, who wrote to you - and their name exactly as the title or body spells it. kamo-internal draws that stretch of the text as the member's name, so hovering it opens their member card. Until now every name in the bell was plain text, because nothing recorded who it was. DDL, applied by hand to production on 2026-09-23 as the owner kamo (ddl-auto is none everywhere): ALTER TABLE public.member_notifications ADD COLUMN IF NOT EXISTS actor_member_id INT8; ALTER TABLE public.member_notifications ADD COLUMN IF NOT EXISTS actor_name VARCHAR(255); The same statements ship in emailservice as **************** EmailService is the only service that reads or writes this table.
· InitializerService
The Notes plan feature is "Notes & Checklists"
The Notes app holds checklists as well since 2026-09-23 and is called "Notes & Checklists" everywhere it is named. The pricing page's comparison chart takes its row names from commerce_subscription_features, so the NOTES row is renamed there too: NotesChecklistsRenameMigration (@Order 215) renames every NOTES row still carrying the old default name, and rewrites Sign Pink's description where it still reads the old sentence. A reworded row is left alone and a re-run changes nothing. Copy only: service_type and is_included, which decide what a plan entitles, are untouched. KamoCRMPricingLoader seeds the new name for catalogs created from here on. Applied by hand on 2026-09-23 with the same statements, since this service is not deployed.
· kamo-internal
A switched-to mailbox stays live, and the search box is polished
Live updates. The page asks EmailService to watch the mailbox it has open when it is not the member's own (/app/email/watch, re-sent after a reconnect, stopped on leaving the page), and the folder list and the tree reload on that mailbox's changes - new mail, and changes made from any other client or by anyone else - exactly as they already did for the member's own. The relay has a topic of its own, so the mail badge and the new-mail sound never hear of somebody else's mailbox; a change is only acted on for the mailbox actually on screen (lib/email/mailboxWatch). Polish, all found by driving the real components in Chrome: - The help dialog and the advanced form re-ran their opening effect on every page render (the page passes a fresh onClose), which pulled focus back to the close button / the "From" box out from under the member. They now run once per opening. The dialog also keeps Tab inside it and hands focus back to "How to search" when it closes. - The search box placeholder is "Search mail"; the long one was cut to "Search mail - try f" in the 320px column (dictionary 6401409c). - "How to search" sits at the start of its own line when the column is too narrow for it beside the scope toggle, instead of hanging off the right. - The scope toggle is one tab stop with arrow keys, like any radio group; the suggestion list announces the highlighted option (aria-activedescendant), keeps it scrolled into view, and marks its headings as presentation.
· kamo-internal
Audio lessons — upload a recording, and learners can really navigate it
Authors (/hr/training course editor) - "Upload file" on every slide, beside "Choose media": audio (MP3, M4A, AAC, WAV, OGG/Opus, FLAC), video or an image, through the imaging pipeline into HR Resources, attached the moment it is stored. The library picker knows audio too (badge, waveform thumbnail), and the course-cover picker now offers images only. - An audio slide gets its own editor: a required description (the accessible name), the lesson playing exactly as a learner meets it, chapters (add one at the playhead, edit times as m:ss or h:mm:ss, play from any, or paste a list in the usual "00:00 Title" shapes), and a transcript that follows along. A finished job never overwrites text typed while it ran (it is offered instead), and one found running when the slide is reopened is offered the same way. - The recording is measured once, on attach: length and waveform come from ConversionService and are stored on the slide (media_meta_json), so no learner waits for them. "Listen to the recording" is the audio wording of the play-through rule. Learners (the course player) - A waveform scrubber showing what has been played, what has actually been HEARD, the chapters, their bookmarks and an A–B repeat; click, drag or arrow through it, with a time-and-chapter preview on hover. - Chapters with per-chapter heard progress, previous/next chapter, and "repeat this chapter". - A transcript that follows the recording, jumps anywhere on click, and searches with next/previous through the matches; captions of the line being spoken under the waveform. - Bookmarks with notes, kept in the member's own preferences so they follow them between devices (never written before the saved list has loaded, which would replace it; a pending note is sent, not dropped, when the slide closes). - Speed from 0.5× to 2× (and the time left at that speed), ±10 s, volume, the member's chosen speaker, the operating system's media keys, and a keyboard map (Space/K, J/L, arrows, Shift+arrows or P/N, < >, 0–9, M, B, A, C, /, ?) with a help sheet. - It resumes where they stopped on this device, and offers the first part they have not heard. - The play-through requirement counts only stretches actually played: a seek (scrubber, skip, loop, devtools, or a tick that lands mid-seek) earns nothing, and neither does faster than 2×. Unlike the video rule it credits a background tab's long tick, because audio is what people play in one. Heard to within half a second of the end counts as the whole recording: the stored form rounds to 0.1 s and the duration does not, and 100% used to read as 99% forever. - The escape hatch for a file the browser cannot play appears once the recording's length has passed, as it does for video; shortcuts work wherever focus is inside the player's dialog. Fixed on the way, for video as much as audio - The play-through gate measured against the version's frozen duration, which publishing never wrote: every video version carries null, so watching could never meet the requirement. The gate now uses the element's own duration. - What a member had watched reached the server only when they pressed Next, so closing the course mid-lesson lost it. It is saved every 20 s, on pause and on close, with the percent the completion manifest reads (it was never sent, so every manifest recorded 0% coverage). - Slide media, question pictures and the lesson itself now load through the member's own assignment **************** instead of the imaging stream, which requires VIEW_DOCUMENTS — a right 32 of 149 members did not hold. - The editor keyed slide cards by uid, which every autosave replaces, so each save remounted every card — and would have stopped the audio an author was listening to while typing a chapter. An upload in flight follows its slide through a move or a delete. - The member media route answers with a sandboxing CSP and nosniff: a slide picture can be an SVG, which opened directly would otherwise run script in this origin. Needs DocsService (media endpoint, media_meta_json) and ConversionService (peaks, transcripts) deployed alongside; the English is on kamo-translation-dictionary a4c6f856.
· KlusterServices
A way to actually build the golden image
build-golden-image.sh said "Run by CI (see ../../.forgejo/workflows/)". No such workflow was ever written. It needs libguestfs, qemu-img and virt-customize, none of which are installed on the machine this repo is worked on, and there is no container runtime there either — so the one artefact the whole app depends on had no path to existing. That is how the first person to assign a computer found out. The build runs in the cluster instead, which is the one place already equipped for it: the nodes run KubeVirt and expose /dev/kvm through the devices.kubevirt.io/kvm device plugin, so libguestfs is hardware-accelerated rather than emulating for hours. The job writes into the golden PVC directly, because vm/datavolume-template clones with source.pvc and a CDI clone of a Filesystem volume copies its contents — so any claim holding disk.img is a valid source. That removes the HTTP hosting step, the upload credential and a second copy of a multi-gigabyte file. golden-datavolume.yaml stays for an image built somewhere else. Three things worth knowing from writing it: The scripts travel as ConfigMaps rather than a git clone, so the job needs no credential. They mount read-only with no execute bit, so they are copied to a writable path before virt-customize --run can execute them. The build happens in scratch and only the finished file is copied into the claim. The script writes a 20 GB working qcow2 and then qemu-img convert -c writes a second compressed one beside it; doing that in the golden claim would need it to be twice the size of the image it ends up holding. VERSION is overridable now. The in-cluster path has to know the version BEFORE the build starts, because the claim carries it in its name and must exist for the job to write into, so the wrapper resolves Canonical's serial once and uses that one value for both. A rebuild of an existing version is refused rather than written into: a second write into a populated golden PVC is the failure that reaches every tenant at once.
· ConversionService
Audio — a waveform thumbnail at upload, and peaks and transcripts on demand
An uploaded audio file fell through to the Docs PDF path, which cannot open one, so every audio upload ended "Conversion failed" with a blank library tile. Audio now takes its own branch, like video: streamed to scratch (never into heap), decoded ONCE into peaks, and its page-0 thumbnail drawn from those peaks in Java. Not ffmpeg's showwavespic, which holds every decoded frame until the end of the file: measured at 298 MB for a 30-minute MP3 and 546 MB for an hour, on an unbounded thread per upload in a pod sized for documents; the streamed decode peaks at 54 MB. FLAC, which the MIME table stores as a generic binary, is recognised by its extension. **************** - GET /peaks — the duration and a 1,600-bucket waveform (base64 of 0–255 peaks). ffmpeg's PCM is read as it streams and only one short per 50 ms window is kept, so an hour costs 72,000 shorts; the browser's decodeAudioData would hold over a gigabyte for the same hour. Stored beside the file as <datId>_peaks_v1.json, so it is computed once per content. - POST|GET /transcript — a timed WebVTT transcript from whisper-asr (vad_filter on), run as a job on a virtual thread with its state in <datId>_transcript_v1.json, which either replica can read (an in-memory job table would be invisible to the other pod). One job per pod at a time: whisper-asr is one CPU-bound replica shared with voicemail and meeting transcription. A running job rewrites its marker every minute from its own thread (joined before the result is written), so a job whose pod died reads as failed after three missed beats instead of locking the author out for a Whisper timeout; ?restart=true replaces a finished transcript. Measured against the live service: a 2 min 7 s recording took 25 s. Both require VIEW_DOCUMENTS and the imaging access level, and serve HR Resources only: voicemails, call recordings and chat voice notes each carry a per-record rule in DocsService that this service cannot see, so they stay a 404 until something that needs them brings that rule along. The build also picks up kamo-shared-library's audio MIME aliases: M4A, WAV, OGG and AAC uploads were being refused as a MIME mismatch.
· DocsService
Audio slides — media metadata, and learners play media through their assignment
An audio slide's navigation lives in media_meta_json (duration, waveform peaks, chapters), which the editor writes and the player reads back verbatim. It is saved (bounded to 256K and to a JSON object, and kept only while the slide has media), duplicated, frozen at publish and served on the player DTO. Publish also writes media_duration_ms from it — declared on the version from the start and never written — and the course estimate counts an audio lesson's real length. The fingerprint hashes an AUDIO slide's transcript and metadata, so correcting a transcript line or moving a chapter is a publishable change. Every other slide hashes exactly as before: widening the hash to all slides would have turned every published course with a video transcript "unpublished" the day this shipped. GET **************** serves a slide's (or a question's) media with Range support, authorized by the member's own assignment: the file must be in the version they were assigned, in HR Resources, in their organization, live and untrashed. The imaging stream it replaces for learners needs VIEW_DOCUMENTS, which 32 of the 149 members holding any right did not have — every one of them met a dead player. Because a learner is authorized by the file's presence in a course, saving a unit now refuses a file its author cannot open (422 naming the slide or question), and attaching any file at all needs VIEW_DOCUMENTS, the right the imaging stream itself demands; publishing re-runs both checks over the whole course, so a draft saved earlier or by a co-author is held to the same rule. Otherwise MANAGE_TRAINING alone could publish a file its holder cannot read into a course and assign it to themselves. The learner path also only ever serves a picture, a video or a recording — never a document a crafted save named. HR Resources is also narrower than every other byte rule (issued packages are SERVICE_APPLICATION, per-party documents LOAN or ACCOUNT_MEMBER_VAULT, and chat/voicemail/call media have their own collections). Reads are audited like /imaging/stream, and the Range answer is one shared helper (ImgRangeResponses) that /imaging/stream now uses too. Publish refuses an audio slide without a description in audio's own words. Needs kamo-shared-library "feat(imaging,training): audio uploads pass the MIME check, and slides carry media metadata" (media_meta_json on both slide entities; the DDL is applied).
· kamo-shared-library
Audio uploads pass the MIME check, and slides carry media metadata
Every audio upload but MP3 was refused as a MIME mismatch. An upload is accepted only when the browser's type and Tika's sniff resolve to the same ImageMimeType, and for audio the two sides never spell a format the same way. Measured against Tika 2.9.2: a WAV sniffs as audio/vnd.wave (browsers send audio/wav), Ogg as audio/vorbis or audio/opus (audio/ogg), ADTS AAC as audio/x-aac (audio/aac), and Windows and macOS send an .m4a as audio/x-m4a where Tika says audio/mp4. stringToImageMimeType now ignores case, surrounding space and parameters, and maps those aliases onto the constant they name. Nothing maps a container that can carry video (Matroska, WebM, MP4) to an audio constant, and FLAC stays a generic binary on both sides, as before. TrainingSlide and TrainingVersionSlide gain media_meta_json (TEXT): the duration, waveform peaks and chapter marks the training player navigates an audio lesson by — written by the editor, frozen with the version. DDL, applied to production by hand before this commit (InitializerService is not deployed): ALTER TABLE public.hr_training_slide ADD COLUMN IF NOT EXISTS media_meta_json TEXT; ALTER TABLE **************** ADD COLUMN IF NOT EXISTS media_meta_json TEXT;
· kamo-internal
A tool window's full title, however it is cut short
A title bar is one line, and a conversation, an email or ticket subject or a terminal's directory is regularly longer than the room it gets — the member could only ever read the cut-off start. Every place a window's title is drawn now opens a card with the whole of it: the title bar in the front row, the strips of windows standing behind, a floating or maximized window's bar, and the tabs above the maximized pane (where it replaces the browser's own unstyled `title` tooltip). Reached every way a member can ask: - hover: after a short rest, and at once when sliding on from another title; one card at a time - press: at once; taken down the moment the press becomes a window drag, and kept down until the pointer leaves or comes back afresh - tap: at once, held after the finger lifts until a tap lands elsewhere - keyboard: focusing a strip, a tab, or a floating window's bar; Escape closes - right-click: the window menu now names the window in full at the top and has a "Copy title" row Only for a title that is actually cut off (measured when it would open, including a sub-pixel overflow `scrollWidth` rounds away), and it follows the window as the row moves, updates when the title is renamed over the socket, and goes away once a maximize or a wider row lets the title fit. The card is a deeper shade of the org's own title-bar colour, lines the full title up under the cut-off one, is selectable, and copies the whole title. The card is portalled but React bubbles its events back to the title bar, so it stops the press, click, double-click, context menu and keys that would drag, raise, maximize or move the window underneath it.
· InitializerService
Put the app in the entitlement catalog, add-on only
**************** asserts every sold app is categorised and sits somewhere on the tier ladder, because an app the catalog does not describe lands nowhere on the pricing matrix and silence writes is_included=false on every tier. Hosted Computers was sold and described nowhere, so it broke both assertions the moment ServiceType 37 existed. Add-on only, and for a reason none of the other add-on-only apps share. MLOS, the personal loan system and Patient Records are negotiated because of what they carry; Hosted Computers is negotiated because of what it spends. A tier rung would hand every organization on that plan the right to assign KubeVirt VMs, and the scheduler bounds the fleet by REQUESTS rather than usage, so the orgs that used the entitlement first would take the nodes from the rest - at a published price, which is the part that makes it hard to walk back. What moves it onto a rung is more nodes, not a pricing change. The add-on follows the shape the loop already applies to the negotiated apps: no list price, so nothing renders one, and compatible only with CUSTOM plans. It is also not offered anywhere until the app reaches COMPLETED, which it has not. Catalog entry sits in Productivity next to the other things people work in. A category holding one app reads as an oversight to whoever opens the matrix.
· kamo-internal
Human/AI member accounts, phonetic names, job title and department requirements
Add Member / Add Team Member - A first "Person or AI" section: Human Intelligence (default) or Artificial Intelligence as two keyboard-operable choice cards. AI brings in the supervising team member (defaults to you when you are a team member) and the AI model; a team member's work location pre-fills as Remote. - Phonetic name beside the preferred name, with pronunciation tips and two play buttons that read the name as spelled and as written phonetically through the browser's speech engine. - The summary shows Intelligence and "Says it"; picking a job title or department shows what it expects (description + requirements); an existing account's notice says its name and pronunciation are kept. - Bulk add: a batch Intelligence setting (+ supervisor), a per-row override and a Phonetic column; CSV import auto-maps "pronunciation", "human or ai" and friends ("AI", "Bot", "Human"...). After creation - Members / Team Members grids: an Intelligence column, an All · Human · AI filter with counts, and quick search that finds "AI" and phonetic names. - The member card (every name hover): an AI pill and a "Says it" line with a play button; roster rows prime it with both. - Profile: the hero wears the AI pill and "Says it"; the phonetic name is edited beside the preferred name; the Membership card shows/edits Intelligence, supervisor and model (Manage Members only, never on your own profile); the Work room gains a live, read-only Role expectations card. Job titles and departments (/settings/essential) - The add/edit dialog gains an Expectations section: a description (4,000 characters, counter) and a requirements editor — category, text, Required/Preferred, reorder, remove, Enter adds the next, pasting a list splits it into rows, example chips, 50-row cap. - Tables show the description's first line and a Requirements column with a hover list; both pages refresh live (paused while the dialog is open) and show the server's own words on a refused save. Shared pieces: app/lib/members/intelligence, app/lib/speech (feature-detected, SSR-safe), IntelligenceBadge / IntelligenceChoice / PhoneticNameField / SayItButton, app/lib/orgRequirements, RequirementsEditor, RoleExpectations. Spec and plan under docs/superpowers. Needs kamo-shared-library + securityservice (same feature) and the dictionary keys pushed first.
· SecurityService
Human/AI members, phonetic names, role requirements; fix position saves wiping security
Members - Create, create-team-member and bulk-create accept `intelligenceType`, `namePhonetic` (new accounts only, like the name), and for AI accounts `aiSupervisorMemberId` + `aiModel`. The supervisor must be an active, human team member of the org and never the account itself; when none is named the creator stands in if they qualify (AiSupervisorPolicy). Every refusal names its field before anything is written. - PUT /members/{id}: `namePhonetic` is a user-identity field (same authority as the legal name). `intelligenceType`, `aiSupervisorMemberId`, `aiModel` need MANAGE_MEMBERS or an open god window, and are refused on your own membership (CANNOT_RELABEL_SELF) so an AI cannot relabel itself human. Switching to HUMAN clears supervisor and model. - GET /members/{id}, /members/{id}/card, the by-department roster and the account list rows carry `intelligenceType` and `namePhonetic` (the profile also gets supervisor id/name and model). - org-config's job titles and departments carry description + requirements for the create form. Job titles and departments - POST/PUT/DELETE now require CONFIGURE_SYSTEM (or an open god window) — the settings pages that write them already did; the server accepted any signed-in member of the org. Reads unchanged. - Description and requirements flow through the shared services (see kamo-shared-library). Fix: PUT /member-security/{id} replaced roles, direct rights and the session timeout even when the request did not name them. The profile's Position card sends only department / job title / official title / manager, so every position edit wiped the member's custom roles, direct rights and timeout override. Each is now replaced only when its key is present; the security editor sends all three. DDL: **************** (applied by hand as the owner before this push; zero schema-mismatch errors in security/email/media over the following 15 minutes).
· kamo-shared-library
Intelligence type, phonetic names, AI supervision, job title/department requirements
Members: `Member.intelligenceType` (HUMAN | ARTIFICIAL, STRING, defaults HUMAN on every construction path and in the column), `aiSupervisorMemberId` (plain id — members is the hottest table and is hard-deleted) and `aiModel`. Users: `namePhonetic`, beside the name rather than inside EmbNameFull, which leads, accounts, borrowers and sessions also embed. Job titles and departments: a `description` (TEXT, 4,000 characters) and ordered requirements in two child tables (`org_job_title_requirements`, `dept_requirements`: category, Required/Preferred, text ≤ 500, stable ids, ON DELETE CASCADE). `OrgRequirements` is the one rule set for both — validation before anything is written, diff-by-id so a kept requirement keeps its id, and an id from another owner is treated as new. Updates follow "absent means unchanged" for both new fields, so a caller that never sends them cannot wipe them; "" / [] clear. **************** now take the request DTO (only SecurityService calls them). DDL is hand-applied **************** applied 2026-09-22 03:01Z, before this push). A real SessionFactory over the whole library pins the new tables and columns to that DDL (RoleRequirementsMappingTest).
· kamo-internal
The nav option, the settings section, and the storage figure
A member's own Linux computer, opened from the top bar. The app (ServiceType 37) and its three rights landed in the shared library; this is the half people see. THE NAV OPTION APPEARS EXACTLY WHEN THERE IS SOMETHING BEHIND IT Gated on the server's answer to "which computers may this member open", not on a right and not on god. The dev-machine option had to be taught this: it was god-gated once, which put an icon in front of people with no account on the machine and produced Guacamole's generic error when they clicked it. So a manager holding ACCESS_SUBORDINATE_COMPUTERS and no computer of their own still gets the icon — there is something behind it — and an administrator holding MANAGE and nothing assigned does not. The list is the server's, in the server's order: own first, then subordinates', then the organization's. Nothing here filters or reorders, for the reason /network's listing is server-decided — a client-side filter is a list that has already been sent, and the order carries information the client never sees because it does not have the manager chain. Each row expands to the sizes from DESKTOP_RESOLUTIONS, reused rather than restated so a size added there lands on the correct side of the rule under "Fit my window" without anybody moving a boundary. Every row wears its state, because "asleep" is the normal resting state of one of these computers rather than a fault, and a row that said nothing would leave somebody who waits a minute thinking it had failed. openHostedComputer is more than a fetch and a redirect for the same reason: a launch has three answers and two of them are not a link. Asleep means a wake has started and takes 30-90 seconds while the guest reads its memory back off a replicated volume, so the tab SAYS that — a blank tab for a minute is indistinguishable from a broken one, which openKamoDesktop had to be taught as well. No login yet means Kamo never stored the password and the member confirms it once; that is the design working, not a failure, so the tab closes and a dialog asks instead of leaving a page of explanation nobody wanted. THREE SETTINGS TABS. One was asked for; the other two are why it is usable Computers is the one: assign, resize, change storage, sleep, remove, live on useOrgChanges. Capacity answers "why will this not start?", which otherwise has no answer anywhere in the product — and the answer is counter-intuitive, so the bars say REQUESTS rather than usage, because a machine can look half idle and have no room. Defaults stops an organization re-choosing a size and a disk for every person, and every box there can be blank, where blank means "follow the platform" and not zero. The resize dialog warns and the storage dialog says it will not, which is the pair worth having: a size change restarts the computer because a sleep image is bound to the memory size it was written at, and a storage change expands the volume online and disturbs nothing. STORAGE ON THE STATS PAGE, AS ITS OWN PANEL HostedComputerUsagePanel sits directly after Storage on Settings → My Account → Stats & Costs. Not a row in that table: its rows come from the billing snapshot sweep walking storage RECORDS, and a computer's disk has none — what is on it is known only to the guest, which reports its own filesystem figure. Longhorn's number would be wrong in a way that matters, because it counts everything ever written and never falls when somebody deletes a file. The swap file is excluded: it is the size of the computer's memory and exists so the machine can sleep. Two things my own checks caught and fixed: the nav gate returned `undefined` rather than `false` for an organization with no flag set (`undefined && x`), and the storage column passed `number | null` into an ICU placeholder — the ceiling and "it grows" are the same fact, so it now reads the ceiling once and lets TypeScript follow the narrowing. Verified: tsc clean of everything but four pre-existing ReplyQuote errors in chat and support, present at the same lines on origin/main. 320 nav and lib tests pass. eslint clean on every new file and no new warning in the edited ones. All three image-build guards pass — check-i18n-keys clean over 4268 files against the dictionary head, which was pushed first and verified by ancestry.
· kamo-internal
Checklists in the Notes app — prerequisites, stickies, home panes and the calendar
A note is memory; a checklist is work. /notes now has two halves — Notes and Checklists (?view=checklists) — and a checklist is its own record (/api/checklists, KBService) with tasks that can wait on other tasks. Checklists - A card per list on its own wall: progress edge and ring, tasks tickable on the card, schedule chip; rail filters for in progress / not started / done / scheduled / on home / favourites, search that reaches inside every task. - The editor: add tasks fast (Enter; Shift+Enter adds it as the next step; paste a list), click to edit, drag or Alt+arrows to reorder, delete with Undo, All/Ready/Waiting/Done filter, "do in order", untick all, clear done, duplicate as a fresh copy, copy as Markdown. - Prerequisites: link mode picks what a task waits on, greying out anything that would loop. A locked task shows its padlock and what it waits on; pressing it shakes the row, lights up the tasks in the way and offers to jump to them. Unticking something finished work depends on, or giving a finished task an open prerequisite, asks first and names what reopens. The dependency rail draws every link and lights the hovered task's. The server enforces the same rules, so nothing gets round them. - Unlocks, ticks and finishing a list are animated; the finish gets confetti and a tone in the surface where the last tick was made. Everywhere a note goes - Floating: checklists float as stickies in the same overlay, stacking order, saved layout and hex-head dock as notes (their own dock group). - Home: a pinned checklist is a launchpad pane beside pinned notes, tickable. - One live store feeds every surface, refreshed by the `checklists` org change, so a tick anywhere lands everywhere. Calendar - A checklist goes on the calendar as a real event (sourceType CHECKLIST), scheduled through the calendar's own event dialog from any surface, or by dragging it from the calendar's new checklist tray onto a day or an hour. - Checklist events show their progress in every view; the event dialog shows the list, tickable in place; overdue work pulses red. - The calendar proxy now publishes the `calendar` org change on every write: EmailService's own topics are per pod, so a tab on the other pod never heard. Design: ****************
· APIService
Forward /api/checklists to KBService
Checklists live in the Notes app and are served by KBService beside the notes themselves; without this route the whole feature answers 404 at the gateway.
· KBService
The checklist API, with prerequisites the server enforces
/api/checklists: list, get, create, update, delete, duplicate, reset, clear-done, sequence ("do in order"), clear links, and per task add, edit, tick/untick, set prerequisites, move and delete. Every mutation answers the whole checklist so every surface redraws from one answer. The rules live in ChecklistRules, pure and exhaustively tested, and hold one invariant: a done task never waits on an open one. A tick is refused while a prerequisite is open (409 PREREQUISITES_OPEN, naming them); an untick that done tasks depend on is refused unless cascade=true; a link that would loop is refused (409 CYCLE, with the loop); a link that would leave finished work waiting on open work needs reopen=true (409 REOPEN_REQUIRED). The same rules run in the browser for instant feedback, but this is the one that decides — a sticky, the calendar or a stale tab cannot get round it. Rights are the Notes app's: VIEW_NOTES to read, CREATE_NOTES, EDIT_NOTES and DELETE_NOTES to change. Mutations carry @RetryOnDbConflict and bump the checklist's version, so two tabs ticking at once serialize. create_checklists.sql is the DDL, idempotent, applied as the owner.
· kamo-shared-library
Checklist and ChecklistItem entities, repository and DTOs
Checklists are the Notes app's second record type: work to get through, whose tasks can wait on other tasks. Three tables — checklists, checklist_items and checklist_item_prerequisites — all keyed by UUID so no id is ever an int64 a browser could round. Task text is ciphertext under the member's note key. The checklist row carries its own created_at/updated_at (EmbRecordState's DATE_UPDATED is insert/update=false and never moves) and a @Version the service bumps on every mutation, so concurrent edits to one list collide instead of interleaving. DDL is applied by hand **************** nothing here creates a table.
· SecurityService
The browser-facing surface, and the second Guacamole gateway
SecurityService is the trust boundary for Hosted Computers. It holds the session, the rights and the org chart; ComputeService holds the Kubernetes credentials and no session at all. Nothing downstream re-checks a right and nothing here touches a cluster. THE ORG ID IS NEVER TAKEN FROM THE REQUEST. Every call stamps the caller's own resolved organization. A Hosted Computer is a whole machine on a network that sees only its own organization and the internet, so a handler that accepted an org id from a body would not be a leaky list — it would be a door into somebody else's desktop. The assign path additionally resolves the target member's email through the caller's own organization, which doubles as the membership check: without it an administrator could assign a computer to a member id belonging to another tenant and have the machine built in this org's namespace under that person's name. THREE DIFFERENT GATES, BECAUSE THERE ARE THREE DIFFERENT QUESTIONS. Managing needs MANAGE_HOSTED_COMPUTERS. Opening one needs the computer to be yours, or one of the two ACCESS rights to widen it — and whether the caller may open THAT computer is answered by re-asking the same visibility question the nav menu was built from, rather than by a second rule that could disagree. A member who was never shown a computer cannot reach it by guessing an id. Confirming a password needs no right at all: it is how a member's own computer gets a login. THE ORG CHART IS FETCHED ONLY WHEN A RIGHT NEEDS IT, and in one query. ACCESS_SUBORDINATE_COMPUTERS reaches the whole subtree below a member, so the edges come back as pairs and the traversal downstream walks them over a visited set — managerId is a free-form profile field, not a validated tree, so a cycle is one reorganisation away. A member with no rights pays one call, which matters because the nav asks this on every page load. THE TOKEN FORMAT IS NOW SHARED, NOT COPIED. GuacamoleTokenCodec is extracted from GuacamoleJsonAuth because there are two gateways: desktop.<domain> for platform admins and computers.<domain> for tenants, with different signing keys and different connection models. Two copies of that format would be one implementation and one liability, and the mistake the copy would eventually make is the `expires` mistake — it reads like a link TTL and is actually the session's reconnect window, and getting it wrong empties a live user's connection list silently. The floor is enforced by both callers. HostedComputerSso is a sibling rather than a branch. That gateway offers three fixed targets and puts all of them in every token; this one has one connection per computer, named after the computer, with a one-time credential minted per launch — so the member's own Kamo password never travels in a token. The platform desktop has to keep a decryptable copy of an account password precisely so Guacamole can auto-login; this does not, and that is the improvement. PasswordChangeController now pushes the new password to the member's computers. This is the ONLY moment the plaintext exists — Kamo stores a hash and keeps no reversible copy anywhere — and the push is best-effort by construction: a failure must never fail the change the member asked for, and a sleeping computer is marked stale so its next launch asks for a confirmation. Nothing is queued, because a queued password is a stored password with a delay. Suite: 2755 tests, 5 failures, and all five are the same ones pristine origin/main produces — the UnauthenticatedEndpointRatchet on PromotionCodeInternalController (added by 9578ca0 and never added to the baseline) and four **************** NPEs on appliedJobStamp. Verified by running both in a clean worktree at origin/main. My own controller passes the ratchet: every handler resolves a session and answers 401 without one. Needs the hc-guac-sso secret to mint links; until it exists the launch path says so plainly rather than handing back a link that lands on Guacamole's generic error screen.
· EmailService
A member can put a checklist on their calendar, once
Checklists live in KBService and the calendar owns only WHEN, so a scheduled checklist is an ordinary member event carrying sourceType CHECKLIST, the checklist UUID and a deep link into /notes. The link is derived server-side, never read from the body, and the event goes through the normal member create path so the calendar change publish still fires. A second active schedule of the same checklist is a 409 naming the first. GET **************** lists them for the chips on /notes. An edit still cannot change what an event is about. Checklist reminders keep the calendar kind and get a checklist icon.
· APIService
Forward the Hosted Computer agent path, and only that path
/api/hosted-computers/agent/** -> ComputeService. A Hosted Computer's network policy denies the cluster entirely — no database, no service, not even cluster DNS — so the only way for a tenant's virtual machine to tell Kamo anything is out to the internet and back through api.<domain> like any other client. Everything else about a computer is pushed in to it; this is the single inbound path. THE NARROWNESS IS THE POINT. Its siblings under /api/hosted-computers/ are the internal management API: assigning a computer, resizing one, deleting one, reading another tenant's capacity. This gateway's SecurityConfig is entirely commented out, so a forwarded path is public and the upstream service owns the auth — a wildcard here would publish a VM-creation API to the internet. That is the same mistake /api/bulktext/inbound/** exists to avoid, and the reason the agent surface was made a child of /agent rather than a sibling of the management routes. forward rather than forwardWebhook: unlike a carrier webhook there is no third-party signature over the raw bytes, and the credential is an ordinary Authorization: Bearer header — a per-computer secret rotated on every wake, which ComputeService checks against that one computer. The endpoint is write-only telemetry whose response carries nothing about the organization, so a credential lifted from a tenant's own desktop buys the ability to post that desktop's storage figure and nothing else.
· kamo-shared-library
One query for an organization's whole reporting graph
findMemberAndManagerIds returns every (memberId, managerId) edge in one organization, for Hosted Computers' ACCESS_SUBORDINATE_COMPUTERS right — which reaches the whole subtree below a member rather than only their direct reports. The alternative is findDirectReportMemberIds walked level by level, which is a query per level on a path the nav menu hits on every page load, and the depth is a property of the customer's org chart rather than anything this code controls. Returns the EDGES rather than a resolved subtree, because the traversal has to guard against a cycle: managerId is a free-form reference on a member's own profile, not a validated tree, so A reporting to B while B reports to A is one mis-click or one reorganisation away and a recursive query would not return. The caller walks these breadth-first over a visited set. Filters mirror findDirectReportMemberIds exactly, including the explicit LEFT JOIN on tm.user — an implicit path join is an INNER join in JPQL and would silently drop every report whose user row is missing, which that method's own comment records. Validated by LibraryQueryParseTest, which translates every @Query in the library against the real entity mapping. That matters here: Spring Data validates an annotated query when it builds the repository bean, so a bad path compiles, passes CI, ships, and then stops the consuming service from starting at all.
· kamo-internal
A social inbox's quote of an older message names its author with their card
The quote now takes its author from the server (@kamo/chat-core's quote.senderMemberId), which withholds a social contact's id since mediaservice ceee73e — a contact's words are stored under the connection owner, and the quote used to name the owner. So a rep's name in a quote of a message older than the loaded page opens their card, as a loaded one already did. A loaded contact message is refused here as well, whatever an older server says.
· kamo-internal
The card shows the member id to those who manage members
HR and support read a member's id off a tooltip on the HR rosters, which the member card replaced (the tooltip sat where the card opens). The card's details now carry a Member ID row, with the same copy button as the contact values, for the viewers who may open that member's profile — the rights that manage members, god, and the member themselves. Everyone else sees no id.
· kamo-shared-library
The app, three rights, tier/state enums and the entities
ServiceType.HOSTED_COMPUTERS at id 37, TESTING rather than COMPLETED. The stage is carrying weight here rather than describing progress: AppCatalogService resolves `available` as `stage == COMPLETED`, and an unavailable app is skipped by FeatureController's auto-provision loop. Every other COMPLETED app arrives switched on for all 25 organizations the day it ships; this one hands out virtual machines holding real RAM on a two-node cluster, so it has to be granted per organization by an operator who has looked at the capacity. Nothing is lost — the platform Apps & Features screen exists precisely to publish an app independently of its stage, so the row is waiting there under "Not Available". Three rights at 326-328, not 320-322: the local checkout was 30 commits behind and 320 is MANAGE_EMAIL_OPT_OUTS. Read the IDS in that file, never the line order — 318-325 sit hundreds of lines above the constant that looks last. All three are roots and none is a child of another, which is the decision worth recording rather than the count. MANAGE_HOSTED_COMPUTERS gates the settings section; the two ACCESS rights gate WHOSE computer appears in a member's nav menu. Parenting either under MANAGE would be wrong in the expensive direction — an IT lead who needs to open any machine would arrive holding the provisioning screen, and a manager granted a view of their team's computers would be able to resize them. Neither gates a member's OWN computer: that is not a right at all, because making it one allows a computer provisioned for somebody who cannot reach it. HostedComputerTier and HostedComputerState persist by NAME. These tables are new, so there is nothing to protect and no reason to inherit the reorder hazard AssocType and ServiceType have to live with. Two derivations in the tier are load-bearing and both fail QUIETLY without them: - minimumDiskGb() is guest memory + 25. A computer sleeps by writing its RAM into its own swap file, so a disk with no room for the image produces a computer that never sleeps — or writes a partial image and boots clean with the member's session gone. Neither reports anything, so the floor is enforced rather than suggested. - requestedMib() adds KubeVirt's launcher overhead, measured at ~324 MiB on desktop-1 and rounded to 400. Capacity must compare against what the POD requests: a check that omits it says a computer fits and then leaves its pod Pending indefinitely, which is indistinguishable from a computer that never appeared and logs nothing a member can see. **************** is what capacity planning subtracts, and ASLEEP is deliberately not in it — that is the whole feature. KubeVirt's own pause "freezes the process without releasing CPU resources, but memory remains allocated", so asleep here means suspend-to-disk: the VMI and its launcher pod are gone and the memory request is back with the scheduler. Storage is one nullable pair with a third meaning and no boolean. A ceiling above the current disk is "unlimited" as a member experiences it; an absent ceiling means the disk does not grow. There is no `unlimited` flag because a flag set with no ceiling is the configuration that quietly eats a cluster — the rule StorageAutoExpandSweep already enforces for files, for the reason it records. PhiModule.HOSTED_COMPUTERS is BLOCKED_NO_BAA, and the reason is a property of the thing rather than a gap in the paperwork. Every other module is a known surface with known data flows; this is a desktop somebody can install anything on and paste anything into, and Kamo cannot inventory it without watching the screen. The isolation is genuinely strong and that is not what a BAA asks for. Also mapped KAMO_UNIVERSE, CHESS and HOLDEM, which shipped with no PhiModule and left PhiServiceTypeMappingTest red. Not mine, but the test fails on the first gap it finds, so without them it cannot prove HOSTED_COMPUTERS is mapped either. They take GAMES, which their own parent takes. Suite: 3026 tests, and the 4 remaining failures are the same ones pristine origin/main produces — ReportVisibilityTest (PlatformRightType ordinal, untouched here), the two GROWTH_REQUEST_IMAGE storage domain tests, and SystemBugCountContractTest's NoSuchMethod. Verified by running them in a clean worktree at origin/main before changing anything. Tables are in **************** and have been applied to the live database.
· kamo-internal
A reply to an older message names its author with their card too
Moves @kamo/chat-core to 0.1.19, whose reply quote carries senderMemberId: the server's parentSenderMemberId when it sent one (null for a web-chat visitor, whose words are stored under the system member), else the loaded parent's member. Before, the console could read the author only off a loaded parent, so a reply to a message older than the loaded page quoted a plain name. - Colleague chats (MessageList) take the quote's author as it is. - Tickets apply the rule a bubble's face follows to it — the viewer's own side only, once the sides are known (isOwnSide) — which now holds for unloaded parents too. - Social inboxes keep their own rule (a loaded parent from one of the organization's reps): the server does not withhold a social contact's id, since contacts are stored under the system member. package-lock.json: the chat-core entry's version, resolved and integrity (checked against the downloaded tarball); its pinned dependencies (@kamo/net 0.1.2, @kamo/realtime 0.1.1) are unchanged.
· KlusterServices
Per-org VM islands, suspend-to-disk, and a white-labeled golden image
A member's own Linux computer, running as a KubeVirt VM in its organization's namespace and opened from the browser. Separate from desktop/ in every way that matters: that is one shared machine for platform admins, deliberately on the pod network so it can reach the database, NATS and MinIO. These are tenant machines whose defining property is the opposite. Three facts shaped this, and each contradicts the obvious reading: - Pausing a VM frees nothing. KubeVirt's docs: pausing "freezes the process without releasing CPU resources, but memory remains allocated". So sleeping is suspend-to-DISK — the guest writes its memory into its own swap and powers off, the launcher pod terminates, and the memory request goes back to the scheduler. That needs runStrategy RerunOnFailure, documented to let a guest shut itself down without being relaunched; under Always the sleep would be undone in seconds. - Headroom is bounded by scheduler REQUESTS, not physical RAM. A launcher pod that does not fit sits Pending forever, which looks exactly like a computer that never appears and logs nothing. So a wake is refused before it is attempted, and the refusal names the number. - Cilium reserved identities cannot be matched by a CIDR rule. **************** records that from an outage, where toEntities [host, remote-node] had to be added to ALLOW the node. Here the same fact is the defence inverted: no rule names them, and default-deny.yaml egressDenies them so no later edit can open them — a Cilium deny beats every allow, from any file. Isolation is asserted rather than reviewed. canary/verify-isolation.sh runs a pod carrying the same kamo.io/hc-org label a launcher pod carries, so it gets the same identity and the same policy, and probes both directions: the nodes, kube-apiserver, YugabyteDB, NATS, MinIO, kube-dns, the metadata address and another org's computers must all be unreachable; the internet and same-org computers must work. It also reports, without asserting, whether the upstream router hairpins — api.kamocrm.com resolves to 47.181.8.84 while Traefik's LoadBalancer IP is k1m1's own 192.168.4.22, so the agent's heartbeat is best-effort by construction and nothing depends on it. White-labeling needs no distro rebuild, which was checked surface by surface first. Every visible Xubuntu identity is config or a removable package. strip-distro-identity.sh removes them and pins them so apt cannot restore them — a shadowed file comes back on the next upgrade and the failure is a tenant seeing somebody else's logo weeks later. The golden image ships with no logo and no colours at all; the agent paints them per org from Behavior & Labeling, honouring the rule that a tenant never sees KamoCRM's images. The agent is stdlib-only and cannot call out, because the network policy means there is nobody to call. Config is pushed in, the sleep decision is local so it survives a Kamo outage, and hibernation readiness is verified (swap size, resume= AND resume_offset=, disk in /sys/power/state) before the idle timer is armed — a misconfigured hibernate does not raise, it boots clean and loses the session. 26 tests cover that arithmetic plus the branding input validation. **************** is applied by hand (ddl-auto is none and InitializerService is not deployed) and has been, against the live database as the owner. It needed a grant the existing recipe does not mention: a NEW schema has no ACL, so the global default privileges that cover tables left kamo_app with "permission denied for schema hosted_computer" on the first insert. Verified after. Every manifest validated with kubectl apply --dry-run=server.
· kamo-shared-library
Let a caller resolve a MediaObjImg by the Img id alone
DocsService's imaging endpoints (download/stream/list/delete) know an Img's id but have no reason to hold the MediaObj it was attached to — until they need to honour the chat-attachment revoke flag on it (MediaObjImg.isAccessRevoked) without owning any chat/media schema themselves. MediaObjImgRepository only supported the other direction (by obj id, ordered, for rendering one message's attachments). findByImg_Id closes the gap; plural because the unique constraint is on (OBJ_ID, IMG_ID), not on IMG_ID alone. New test: MediaObjImgRepositoryQueryTest, pinning the derived query's signature the way **************** already does for its sibling repository (this library has no runnable @DataJpaTest/H2 harness, so a derived no-@Query method is asserted by reflection rather than exercised). Full suite: 3013 tests, 4 pre-existing failures + 1 pre-existing error, all confirmed present on a clean origin/main checkout before this change and unrelated to it (PhiServiceTypeMappingTest: ServiceType.KAMO_UNIVERSE has no PhiModule; **************** **************** unclaimed by a StorageDomain; ReportVisibilityTest: an off-by-one report count; SystemBugCountContractTest: SystemBugRepository.findVisible signature drift).
· DaemonService
Fold the lead ledgers every 30 s and recount them nightly; the job trigger needs the cluster secret
The lead ledgers (securityservice create_lead_ledgers.sql, applied 2026-09-22) replace COUNT(*) over a 796k-lead pool and over lead_credits with maintained totals plus an append-only journal that row triggers write. This service keeps them folded and honest: - LeadLedgerFoldJob (every 30 s): one statement per batch deletes journal rows and adds them to the totals, so a reader sees the figure before or after, never half. Readers already add the journal to the total, so a missed tick costs read time, never correctness. At most 40 batches of 5,000 per tick. - LeadLedgerReconcileJob (03:40 nightly): recounts each org's pool and every member's available credits from the rows and compares them with total + journal in ONE statement (one snapshot). Differences are APPENDED as 'RECONCILE' deltas, never written over a total, so the recount cannot race the triggers or a fold. It also names any journal trigger that is missing or disabled. In a healthy system it finds nothing, so every correction is logged at WARN. Both are @DisallowConcurrentExecution and retry a statement YugabyteDB aborts with 40001 (each is atomic, so a retry cannot double-count). The SQL was replayed against YugabyteDB before this shipped (temp tables: triggers, reads, fold and recount, 60 checks). JobTriggerController: POST /api/daemon/trigger/{job} and GET /status answered anyone. SecurityConfig permits all, and the daemon-route IngressRoute puts this service on the public websecure entrypoint (Host daemon.internal.kamocrm.com; probed 200 anonymously), so any job — now including the recount — could be started from the internet, as many overlapping copies as the Quartz pool holds. Both now require X-Internal-Auth = mlos.internal-auth-secret (constant-time) and refuse everyone when that secret is unset. Nothing in the platform calls this endpoint; an operator still can, from inside the cluster. Tests: LeadLedgerMaintenanceTest (fold loop and cap, recount reporting, missing triggers, the retry rule, and the SQL shapes that make it safe beside live traffic), JobTriggerControllerAuthTest.

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