மாற்று பதிவேடு
என்ன புதிது.
அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.
· DaemonService
Daily legal-package reminder tick
DaemonService owns the tick because DocsService cannot. DaemonService is strategy:Recreate — the old pod is gone before the new one starts, so a deploy window cannot double-fire. DocsService is RollingUpdate maxSurge:1, so a @Scheduled there provably can: for the length of every rollout there are two pods and both believe it is 13:00. For a job whose output is mail to every overdue employee on the platform, "usually once" is not a property. It also gives us @DisallowConcurrentExecution, JobTriggerController's manual trigger and a cron tunable without redeploying DocsService. Modelled on LeadEmailSweepJob, including the part that is load-bearing rather than stylistic: a default RestTemplate waits FOREVER, so a wedged DocsService would pin this thread and — under @DisallowConcurrentExecution — stop the sweep until the pod restarts, silently and indefinitely, looking exactly like "no reminders were due". The timeouts are explicit. Stamps internal.auth.secret (public-chat), the value DocsInternalAuthFilter validates — NOT the MLOS internal secret, which is a different value and 403s every call, the correction StewardNotifier and VoipGlobalSyncJob both needed. Daily, not hourly: the bucket is a function of the day, so an hourly tick is 24x the cross-org scanning for at most one extra send and 24x the blast radius. No configmap change — the docs base URL and the cron both carry working defaults.
· DocsService
Due-date reminder ladder and the batched, read-only sweep
The bucket is a pure function of (dueDate, today) and never of "when we last sent": UPCOMING_D{n} for the configured offsets, DUE_TODAY, then PAST_DUE_P{floor(daysOverdue / interval)}. Running the sweep five times on day 9 therefore yields one bucket, and the ledger claim is what deduplicates it — no state is remembered anywhere. The offsets are data; nothing in the class knows the number 3. past-due-max-periods defaults to 0 meaning UNLIMITED, because decision #4 says the past-due reminder recurs and a silent cap looks exactly like a working sweep while abandoning the member most in need of chasing. The cutoff is today + max(upcomingDays) + 1, not now(): the repository predicate is dueAt BEFORE, so a now() cutoff would find only overdue rows and the entire upcoming half of the ladder would never fire. One short read-only transaction per Slice, fully materialising each batch into plain records — Organization and Member are LAZY, so the fqdn, the label, the name and the address are resolved inside it — and the transaction is CLOSED before the first send. EmailService's send is blocking HTTP into blocking SMTP, and a batch held open pins a Hikari connection for the length of every round-trip in it, in a service also serving WOPI. The sweep performs ZERO writes. It cannot serialization-retry, cannot deadlock against the wizard, and paging is stable because it is not mutating the rows it is paging over. dryRun resolves and counts and claims nothing — a dry run that claimed would burn every bucket it inspected and silently suppress the real run. The recorder gains dispatchAllCounted beside the unchanged dispatchAll, because LegalReminderSweepResultDto already carries claimed and failed and a single sent count cannot fill them: on a re-run "claimed 0 of 400" is the correct, healthy answer and "claimed 400, sent 0" is an outage, and the daemon log has to be able to tell those apart.
· DocsService
Tell the member — assigned, voided and completed
ONE hr.package_assigned per member per package, never one per document: that is what suppressInvite exists for, and 8 documents x 200 members is 1,600 envelopes for 200 people. The send is one contiguous, transaction-free phase AFTER the assignment rows and the envelopes, because EmailService's send is blocking HTTP into blocking SMTP and interleaving 200 of those with envelope minting stretches a delivery HR is watching a progress state for. Deliver's repair pass and a repeat Deliver both rebuild the same notices and both are refused at the ledger claim rather than by an if — the repair pass exists precisely to re-walk members who were already assigned, so an in-code condition would have to encode "already assigned but possibly not yet emailed", which is the state the ledger already knows. hr.package_voided goes out only for the VOIDED outcome. SKIPPED_EXECUTED must not email — that member finished, and telling them their completed package was withdrawn is both wrong and alarming — and NOT_FOUND must not either, since the id was never this org's and the void service already refuses to resolve a name for it so it cannot become a cross-tenant oracle. Dispatch runs through LegalAfterCommit onto the delivery pool: a serialization retry that rolled the void back would otherwise have already told 200 people their package was cancelled, and there is no unsend. hr.package_completed is a receipt to the member carrying attestedAt and the manifest digest. Called out deliberately: the scope sentence lists assigned and voided, but a canonical template nothing sends is dead weight, and until the filed artifact is downloadable this receipt is the only record the member holds.
· DocsService
Outbound email client and a Redis notification ledger
Idempotency with NO schema change. The reminder bucket is a pure function of (dueDate, today) and never of "when we last sent", so the only durable state a notification needs is a per-(assignment, bucket, dueDate, channel) claim — and SET NX in the Redis DocsService already runs (WopiLockService, KSessionService) has exactly the semantics of the UNIQUE constraint the design asked a table for. It is atomic across the RollingUpdate replicas, the due date is IN the key so extending a due date legitimately restarts the ladder, and it leaves the sweep entirely READ-ONLY against CockroachDB: no write transaction is ever open while EmailService blocks on SMTP. Claim policy is at-most-once by default. An observed failure shortens the claim's TTL so the next tick retries; a crash between send and mark does NOT retry, because a missed reminder is recovered by the next bucket and a duplicate compliance email to every employee of every tenant is not recoverable at all. Redis being unreachable refuses the send rather than sending unclaimed. packageTitle and voidReason are HTML-escaped Java-side: TemplateRenderer is a flat regex replace with no escaping, so HR-authored prose otherwise reaches every employee's inbox as markup, sent from the org's own verified domain. Every varying sentence is pre-rendered as one whole-sentence variable because there are no conditionals and no pluralization either. Persistence is one seam (LegalNotificationRecorder) writing the Redis ledger plus one structured legal-notify line. The durable evidence table is REPORTED in the plan, not invented: a new shared-lib entity is a KamoInitializer run across every consumer.
· DocsService
DocsService's first inbound internal-auth filter
DocsService has no service-to-service authentication: ResourceServerConfig is anyRequest().permitAll() and OTKPreAuthFilter CONTINUES the chain when X-OTK is absent. Authorization is per-controller — an HR right on LegalPackageController, identity on LegalMemberController — and both need a session a daemon does not have. A mapping that fits neither funnel is therefore fully anonymous through APIService's /api/docs/** forward, so the filter ships BEFORE the endpoint it guards, not after. Copied from **************** including the two behaviours that matter: an unconfigured internal.auth.secret answers 503 rather than passing through (reading "no secret" as "no check" is how a control becomes a comment), and the comparison is MessageDigest.isEqual, which does not return early on the first differing byte the way String.equals does — against a freely callable endpoint that is a byte-at-a-time oracle for a cluster-wide secret. Ownership is segment-exact rather than a bare prefix, so /api/docs/legal/internal and everything under it is guarded while an unrelated future sibling is not 403'd by accident. The property is internal.auth.secret (public-chat), NOT mlos.internal-auth-secret.
· kamo-shared-library
Four canonical HR legal-package templates
hr.package_assigned, hr.package_reminder, hr.package_voided and hr.package_completed, as {json,html} pairs plus four CANONICAL_KEYS entries. The CONSTANT is not the templateKey and that seam is where the live QUOTE_SENT bug lives: the loader derives the FILENAME from the constant **************** and reads templateKey out of the JSON, while EmailTemplateService.sendToUser looks the org row up by the key the CALLER passed and, on a miss, seeds rows keyed by the JSON value and retries the same caller key. QUOTE_SENT -> quote-sent.json -> commerce.quote_sent therefore seeds a template no caller can reach. HrPackageTemplateTest pins all three legs for the four HR keys, and pins a file pair for EVERY canonical constant. Rendering is flat {{var}} regex substitution with no escaping, no conditionals and no pluralization, so every sentence that varies is pre-rendered Java-side as one variable (documentSummary, dueLine, duePhrase, reasonLine, receiptLine) and the tests refuse {{#if}}/{{/}}/{{^}}. Every body names {{teamMemberLabel}} and none contains the literal "Team Member": the org's label does not reach EmailService any other way — sendToUser injects only orgName, logoUrl and logoSimpleUrl. No entity, no repository, no enum, no schema. pom.xml stays 1.5.0.
· kamo-internal
Signature button with a live legal-package badge
Hand-written into the fixed left cluster between My Profile and Log out. NAV_REGISTRY cannot express a position between two fixed controls and its badgeKey field is dead code, so this is not a registry entry. Gated on getMemberType() === 'TEAM_MEMBER', never on checkRight(): checkRight returns true for every right under active god mode, and this button leads to the evidence of what a specific person personally agreed to. getMemberType() defaults to 'MEMBER', so the gate fails closed. Badge idiom copied from Chat Now — 99+ cap, color="error", invisible at zero. Icon-only with an aria-label, and navigation through <Link component={NextLink}> because check-client-navigation.mjs fails the build on window.location.href. The tooltip reuses hr.tabs.legalCompliance, the same key the member page heading already calls, rather than inventing one.
· kamo-internal
Live pending-package count over the EXISTING chat socket
LegalPackageCountContext consumes **************** rather than opening a fourteenth STOMP connection for one integer, and subscribes to **************** — the destination MediaService relays legal.packages.{memberId} onto. The REST seed is mandatory, not an optimisation: that subject is core NATS with no stream, so a frame published while the socket was down is gone. Seed on mount, on every reconnect, and on a 60s floor. With no nats block in the DocsService configmap — today's state — those three paths ARE the feature. Everything testable is a pure function in app/lib/legalPendingCount.ts; vitest runs in node and collects only .test.ts, so a context test would not run. parsePendingCount never throws (an exception in a stompjs handler takes the frame loop down and every other subscription on the shared client with it) and refuses a payload addressed to another member, as defence in depth behind the MediaService SUBSCRIBE guard. Ids are compared as strings and never coerced: getMemberIdString(), not the lossy getMemberId().
· MediaService
Relay the legal-package count and guard per-member SUBSCRIBEs
legal.packages.> -> **************** core NATS, no stream — the same shape SupportStompRelayController and PlatformStompRelayController already use. topicFor is a package-private static so the contract between DocsService and kamo-internal is testable without a broker, and it refuses a multi-token tail so the guard only ever sees single-token member ids. The guard is the other half, and it closes a real gap: only /topic/social/feed/{orgId} and /topic/leads/rt/{orgId} were checked, so every per-member topic this broker carries was subscribable by any authenticated member. Relaying a per-member compliance count onto an unguarded topic would have let any employee watch a named colleague's backlog. The decision moves into a pure StompSubscribeAuthz.allow so it can be tested at all — the interceptor is anonymous inside a @Configuration. Both existing org rules are pinned by test before the move, and the function stays an allowlist of guarded prefixes: eleven other topics run unguarded today and flipping the default would take chat, presence and unread down together. Comparison is String.valueOf on both sides. This service stores memberId as a Long and EmailService as a String; a guard copied across without that silently never matches.
· kamo-internal
Proxy the member's pending-package count
A static segment sibling to [uid] so App Router matches it first. PATH ONLY — forwardToApi owns the query string — and one upstream call, because the OTK is single-use.
· DocsService
Serve the caller's own pending-package count
GET **************** through memberGuarded() and the identity funnel. No memberId and no orgId parameter — both come from the session, because an id in a path is an id somebody can change. Mandatory, not optional: legal.packages.{memberId} is core NATS with no stream, so a frame published while a member's socket was down is gone. This is the only correct value after a reconnect. The response shape is IDENTICAL to the pushed frame, so the client parses both through one function and the seed cannot drift from the push. memberId is a string; count is a number.
· DocsService
Emit the badge count on every transition that moves it
Four sites: delivery (none -> PENDING), void (open -> VOIDED), redelivery (open -> SUPERSEDED) and finish (open -> EXECUTED). Each passes the status it read BEFORE the write and the one it wrote, and LegalPendingCountTransition decides — so the rule lives in one testable place instead of four. Two status writes deliberately do NOT publish. PENDING -> IN_PROGRESS is stamped every time a member opens a package and both states are OPEN, so it would put a STOMP frame (logged at INFO with full payload) on the wire per page view. The filer's EXECUTED <-> EXECUTION_INCOMPLETE cannot cross the boundary at all: its FILEABLE set is exactly those two. Finish into AWAITING_OTHERS publishes nothing by the same rule. The member signed, the instrument is not complete, and clearing the badge would say otherwise.
· DocsService
Publish the member's open-package count to core NATS
Connection.publish on legal.packages.{memberId}, NOT NatsMessageService: that service pins .expectedStream(CHAT_MESSAGES) in both publish and publishSync, so a JetStream publish on a legal subject is refused by the server and every count update would vanish silently. The count is read inside the caller's transaction (Hibernate auto-flushes before the count query, so the value is the post-commit value) and only the send is deferred through LegalAfterCommit. Reading from afterCommit would query a completed transaction; a rollback needs no handling because afterCommit never fires. Nothing here throws. Spring propagates an afterCommit exception to the caller, and a dead broker must not turn a Finish that already wrote an append-only attestation into a 500. natsConnection is required=false and is null in production today — DocsService has no nats block in its configmap — so the publisher degrades to a no-op and the REST seed carries the badge. memberId is a STRING in the payload. It is a unique_rowid() past Number.MAX_SAFE_INTEGER and the browser parses this with JSON.parse.
· kamo-internal
Give the executed-package repair an actual button
POST **************** existed upstream and was tested, but nothing in the product could reach it: no proxy route, no helper, no button. Every failure branch in LegalExecutedPackageFiler is designed around "HR repairs it" — a flatten that never landed, bytes that drifted since delivery, a subject below Team Member, a conversion mark that failed — and the Assigned Packages grid rendered all of them as an "Error" chip with nothing beside it to click. The action sits in the slot Download vacates, so a row shows exactly one of the two. canFileExecutedPackage mirrors the backend's FILEABLE list exactly, so no button can exist whose only outcome is a 409, and it keys on executedImgId being absent rather than on status alone — an EXECUTED row whose artifact never attached is the silent shape, the one case an operator would never think to retry. The upstream status and reason are preserved end to end rather than flattened into "Load failed": 409 here says WHICH repair is needed, and "that member's security level is below Team Member" is something HR fixes and files again while "a document is still missing" is not. PATH ONLY through forwardToApi, one upstream call for the single-use OTK, existing i18n keys only (retry/success/error, all three verified present and non-empty in all 22 locales), ids stay strings.
· kamo-login
Forward the login host so recovery can resolve the org
Password recovery now accepts a member's primary mailbox address, which only names an account within one org. This proxy reaches SecurityService over cluster DNS, so Host is the service name and the browser's host has to travel in X-Forwarded-Host — the only header the backend reads it from. `login` is a registered alias, so login.acme.com resolves to the acme.com org as-is; no rewrite to api.*, which is the APIService gateway's requirement and this route bypasses it.
· SecurityService
Accept a member's primary mailbox address as a login identifier
The address a member gives out is usually the org mailbox flagged Primary on their member settings, not whatever is on their user account — so typing it into the login form failed, and forgot-password for it silently did nothing. Both now fall back to it, after the account username/email has had its say, so account credentials still win and no existing login changes meaning. A mailbox address only names an account within one org, so the fallback is scoped to the org resolved from the login host; login already did that resolution, which now lives in OrgHostResolver so recovery can share it. The account query re-runs by resolved id, leaving every gate — disabled, locked, expired, membership active, password — exactly where it was. An address that names two accounts resolves to neither. The recover proxy in kamo-login has to forward the browser's host for any of this to reach the backend; EMAIL_NOT_VERIFIED now carries the userId, since the verification hand-off could no longer re-find the account from what was typed.
· kamo-internal
Download the executed package from the grid and the member's own page
Both surfaces go through the LEGAL endpoints, keyed on the assignment, not /api/imaging/download/{imgId}: that path additionally needs organization.isDocs and VIEW_DOCUMENTS and checks a session security level baked in at login, and design §8 is explicit that DocManager is the convenience copy and not the access path. executedImgId stays the presence signal — the action is ABSENT, not disabled, while nothing has been filed, because a Download button that 404s reads as a lost document. assignmentStatusKey stops mapping EXECUTION_INCOMPLETE to common.states.incomplete, which is semantically INVERTED in pt. It has been harmless because nothing in the platform has ever written that status; Phase 6 is the first thing that can, and a Portuguese member would have read "complete" over a package with no artifact behind it, on their own employment record. The file name is built from data and sanitised: a package title is free text an HR admin typed, and it reaches both a Content-Disposition header and the user's filesystem, where "/" makes a path and a quote terminates the header.
· kamo-internal
Draw non-image attachments with the document manager's file-type icons
A staged .docx or .xlsx was a grey MUI outline with the extension lettered underneath — a shape invented for the tray, and one that says less than the icon the same file gets the moment it lands in DocManager. Key react-file-icon by extension through defaultStyles exactly as DocManager does, so an attachment looks the same in a draft as the document it becomes, and an unrecognised extension falls back to the same generic sheet.
· DocsService
File after Finish, repair from HR, and download BY imgId
Filing is queued AFTER COMMIT and OFF the request thread. Finish is @Transactional in a service that also serves WOPI; holding a connection across N ESigService reads, a PDFBox merge and a MinIO write is how a pool of ten starves. Submitting from inside the transaction would also race the commit, which is the failure LegalAfterCommit was written for. A failure in the worker is not a failure of Finish: the attestation is written and the row falls to EXECUTION_INCOMPLETE. Downloads resolve by imgId through the legal endpoints, never through POST /api/imaging/list — that pages org-wide BEFORE filtering assocObjectId, so a member's own package silently vanishes past 100 rows at assoc 1, which the lead Files tab shares. And never through ImageService.downloadDocument either: that checks the CALLER's stale-able security level, and design §8 is explicit that the assignment record is the access path and DocManager the convenience copy. Three mappings, three authorizations: the member's is identity, HR's read is the HR read right, and HR's file action is the WRITE right because it uploads a document into someone's library. The file action is synchronous — an admin clicked it and "that member's security level is too low" is the answer they need, not a 200.
· DocsService
File the merged executed package into the member's own library
uploadDocument takes the SUBJECT'S Member, so Img.organization and Img.createdByMember derive from them and HR ends up having filed a document the member owns. Assoc is SERVICE_APPLICATION — ORDINAL 1, what /settings/member/{id}?tab=docs renders — with assocObjectId = the member id, NOT My Docs. accessLevel is the int 2 and never null; description is the package title; both hashes are computed here because uploadDocument recomputes them and rejects a mismatch. The ImgDat is marked converted. uploadDocument does no conversion and no thumbnails: without the flag the inline-PDF endpoint answers 202 "Conversion in progress" forever. Thumbnails are deliberately absent — the DocManager grid shows a file icon instead of a page preview for this row, and rasterising pages is ConversionService's pipeline, not this one. Filing is REFUSED when the subject's security level is below Team Member. ImagingController skips a too-high document silently and answers a clean empty list, so filing at level 2 for a member at MEMBER(1) would hand them nothing over their own employment agreement. It is refused when a signable document's flatten never landed and when an acknowledge-only document's bytes drifted: a package missing a signature page, or carrying bytes the member never read, is worse than no package. All three leave EXECUTION_INCOMPLETE, which is exactly what that status was declared for. Idempotent by executedImgId, transactions explicit, and every network and MinIO call outside all of them.
· kamo-internal
Say what is too big, instead of "Failed to send email"
A draft that outgrew the send endpoint's limits produced one flat line of red text after the whole payload had been uploaded — or, when the body itself was the problem, nothing until the send failed. Three things decide what a member does next: which piece is too big, how far over it is, and what to do about it, so the notice states the size, draws the overage, and names the fix. SendLimitNotice is a sibling of ComposerNotice and is built from the same tokens, but answers a different question: nothing was rejected and there is no file to point at — the message itself has grown too big. Its bar is the whole message and the dark tail is the part that does not fit, because byte counts alone don't say whether to drop one image or half the draft. Any real overage is drawn at a visible minimum; a tail thinner than a pixel would say the opposite of the sentence above it. The check runs before the upload starts. The server cannot answer usefully once it does: an oversized part is refused in the servlet container ahead of any handler, and on a large overage Tomcat drops the connection rather than swallow the rest of the body. Its 413 is still honoured as a backstop, with its numbers preferred over ours, for the case where the two sets of limits have drifted apart. The notice does not auto-dismiss — it is a blocking condition, not a status, and four seconds is not long enough to read it and act. Also carries the concurrent composer/attachment work in progress, at the user's request.
· kamo-internal
Gate each Human Resources tab on its own right
Overview needs VIEW_OVERVIEW, Attendance MANAGE_TIMECARDS, Legal & Compliance MANAGE_LEGAL_AND_COMPLIANCE, Training MANAGE_TRAINING and Resources MANAGE_RESOURCES. Each tab's gate is derived from that one name, so the gate a tab is hidden by and the one it is refused by cannot drift apart. The HR nav option — and with it the home launchpad card and the command-palette entry, which both render from the registry — now appears only when the HRS app is on for the org AND the member holds at least one of the five. It used to admit any of the eleven HR record rights, which offered the module to members every tab of it would refuse.
· DocsService
Enforce MANAGE_LEGAL_AND_COMPLIANCE on the package endpoints
LegalPackageAccess mirrors the Legal & Compliance tab gate, and that tab is now gated on one right of its own instead of VIEW_EMPLOYEES || EDIT_EMPLOYEES || MANAGE_HRS_SETTINGS. A server set narrower than the tab's leaves a 403 toast over an empty grid; wider, and the endpoint is reachable by someone the UI never offered it to. Read and write are the same right because the right says manage — which is why the SecurityService migration seeds it from the old write pair only, so nobody gained publish-and-void on upgrade. Viewing employees no longer dumps the roster through audience-preview.
· SecurityService
Carry existing HR holders onto the new per-tab rights
A right that gates something already reachable is a removal of access: new rights insert as NOT_SPECIFIED, so the /hr tabs would have vanished on deploy for every HR administrator whose role is not full-access. NEW_RIGHT_SEEDS declares each new right's lineage — the OR-group its tab was gated on before — and it is applied wherever a grant can live: at insert time for org roles and security-model templates (mirroring FORCE_GRANTED as FORCE_GRANTED), and via seedOverrideRights() for job titles, departments and member profiles. Those three tables hold a row only where somebody set one, so insert-where-absent would resurrect a cleared right on every boot. The marker is org_role_rights: a right with no row there has never been seen by this database, which is true exactly once. MANAGE_LEGAL_AND_COMPLIANCE deliberately seeds from the old WRITE pair only, so no read-only viewer was handed publish-and-void by the upgrade.
· kamo-shared-library
One security right per Human Resources tab
The /hr tabs were gated on whichever pre-existing HR right happened to be an OR-term in the front end: Attendance opened on VIEW_SCHEDULES, Training on VIEW_EMPLOYEES, Overview and Resources on any HR right at all. An org could not grant payroll without also handing over Legal & Compliance. VIEW_OVERVIEW(182), MANAGE_TIMECARDS(183), **************** MANAGE_TRAINING(185) and MANAGE_RESOURCES(186) gate a tab each. The record-level HR rights still govern what a tab may show and change.
· kamo-internal
Measure a draft against the send limits before uploading it
A body carrying inline base64 — HTML pasted from a web page, a forward's quoted content, a signature's images — can pass the per-part ceiling on its own. The send endpoint refuses it in the servlet container ahead of any handler, and on a large overage Tomcat drops the connection rather than swallow the rest of the body, so the composer had a failed upload and nothing to say about it beyond "Failed to send email". checkSendPayload measures the exact JSON that goes on the wire, plus the attachments and the multipart framing, against the same limits EmailService enforces. sendError now reads the MESSAGE_TOO_LARGE reply and the limits that applied, so a server whose limits have moved is still described accurately rather than against a stale constant.
· DocsService
The executed package certificate and the ordered merge
The certificate carries the package, the frozen version, the subject, the moment and address of execution, the attestation wording verbatim and both digests the WORM row carries — so the artifact and the ledger row can be checked against each other with no database in the room. Every string is sanitised to WinAnsi first. PDFBox's Standard-14 fonts encode WinAnsi only and showText THROWS on anything else; package titles, member names and authored wording are free text, so a Japanese title or a curly apostrophe pasted out of Word would otherwise be the reason a member's package could not be filed. Unencodable characters are substituted visibly, never dropped. Everything paginates: HR authors clauses of any length and a forty-document onboarding pack is ordinary. The merge refuses a gap. mergePdfs skips an unconvertible document and prepends a report of what it left out — right for an ad-hoc export, wrong here, because a package that quietly omits one document still looks like the executed instrument. Order is the order manifestSha256 hashed, so reordering would produce an artifact the attestation cannot vouch for.
· kamo-shared-library
Expose the content-addressed store so chat attachments can dedup
Chat attachments never went through the hashing pipeline — they wrote a private "<imgId>/<fileName>" object per upload, so the same file attached three times occupied three objects. They could not simply call uploadDocument: that flow validates client-supplied hashes, and an attachment arrives as a plain multipart with none. storeContentAddressed is the storage half on its own — hash, reuse-or-create the ImgDat, upload only if the content is new. Callers attach the returned dat to their own Img row, so an attachment stays individually access-controlled and revocable (the row is the permission; the dat is only the bytes) while the bytes are shared with every other attachment — and every document — of the same content.
· DocsService
The two byte sources for the executed artifact
LegalPinnedPdf extracts the frozen-digest rule out of the member read path so the wizard and the filed artifact read the SAME bytes through the SAME check — contentSha256 hashes the img_dats content-address triple, so the comparison costs no MinIO read and differs if and only if the bytes moved, and saveImgContent repoints img.setDat on every Docs save. Modifications are applied, because that is what the member was shown. **************** reads the FLATTENED PDF from the service that produced it. Nothing in ESigService changes: the internal endpoint already exists behind the same X-Internal-Auth + X-Org-Id pair. Re-flattening here would be a second set of signature-placement bugs over bytes ESigService has already committed to. 404 comes back as an empty Optional rather than an exception. EsignPublicService wraps flatten in try/catch { log.error }, so an envelope can sit SIGNED with a null signedBlobId forever — that is exactly what EXECUTION_INCOMPLETE names, and the caller refuses to merge an artifact missing a signature page.
· kamo-internal
Member legal-compliance page and the package execution wizard
**************** is gated on identity (memberID === getMemberIdString(), compared as STRINGS because getMemberId() is lossy past MAX_SAFE_INTEGER) and includes the early `return null` the member settings page omits — that page renders its tabs for the frame before its redirect lands, which here would paint another member's package list. Never checkRight(): it returns true for everything under god mode. The wizard is the only genuinely new UI: an outer per-document step axis in orderIndex order plus a terminal attestation step. It resumes at the first incomplete step rather than at zero, refuses to skip a document (the manifest asserts the member was presented every one, in order), renders the counter from data because common.table.of is an empty string in pl, and shows a persistent awaiting-other-parties panel instead of re-running Consent -> Sign for a signer who already signed. Signable documents render the bytes ESigService flattens; acknowledge-only documents render DocsService's version-pinned bytes. A 422 from submit is surfaced verbatim — it means the server's FieldCompletion disagreed with the ported isFieldComplete, which is a port bug and must not hide behind a generic toast. Finish does NOT treat 409 as success, against the plan's sketch: LegalPackageAttestationService returns 200 with the existing attestation for an already-finished assignment, so a 409 from that endpoint means NOT_OPEN / NO_WORDING / NOT_DELIVERED / NOT_SIGNED / UNVERIFIABLE — VOIDED and SUPERSEDED among them. It re-reads the assignment and only reports success when the row is really attested.

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