8942 Commits
Author SHA1 Message Date
Ruslan Konviser 84a527d857 fix(employee): honor "Include deleted" in the MikroORM pagination branch (#10347)
The employees page sends `withDeleted` from its "Include deleted" toggle.
The TypeORM branch of EmployeeService.pagination forwards it to
setFindOptions, but the MikroORM branch ignored it, so the global
soft-delete filter kept hiding deleted employees and the toggle had no
effect on MikroORM deployments.

Turn off SOFT_DELETABLE_FILTER when withDeleted is set, the same mapping
parseTypeORMFindToMikroOrm() already uses, and cover it with tests.
2026-10-01 21:59:33 +02:00
Cédric K. | Lord VB 497b9ecc86 fix(time-off): make "Include Archived" show archived requests alongside active ones (#10346)
* fix(time-off): make "Include Archived" show archived requests alongside active ones

The time off page sends `includeArchived` from its "Include Archived"
checkbox, but TimeOffRequestService.pagination filtered on
`isArchived = includeArchived` in both ORM branches. Checking the box
therefore showed only the archived requests instead of all of them.

Unchecked still hides archived requests (`isArchived = false`); checked
now adds no archived filter. Tests cover both states in both branches.

* fix(time-off): read "Include Archived" as the boolean the query DTO produces

The query DTO JSON-parses `where` values (convertNativeParameters), so
`includeArchived` reaches the service as a boolean, and
`isNotEmpty(false)` is false: an unchecked box skipped the filter and
showed archived requests. Compute the flag once, hiding archived requests
for `false` or `'false'`, and test both the boolean and string forms.
2026-10-01 21:58:58 +02:00
joel kalema d58dd759cb Feat/employees pages polish (#10345)
* feat(i18n): add strings for the appointment form, booking page and timezone dialog

* feat(appointments): group the appointment form into details, schedule and participants sections

* style(appointments): style the appointment form sections, options and fixed footer

* feat(employee-multi-select): fall back to initials when an employee image fails to load

* feat(employee-multi-select): render compact options with a small avatar and the name

* style(employee-multi-select): compact option rows with round avatars and a light selected state

* feat(public-layout): show the employee picker as a small centred booking card

* style(public-layout): style the booking card and give the employee select the filled input look

* feat(appointments): add readable city, region and offset helpers and search to the timezone dialog

* feat(appointments): redesign the timezone dialog with a header hint, close button and compact footer

* style(appointments): style the timezone dialog and its option rows

* feat(appointments): move the time zone next to the header actions and the calendar title to the left

* style(appointments): restyle the calendar toolbar, time axis and day headers with 12px body text

* style(calendar): give every FullCalendar a line-based grid, solid header row, tinted events and a cleaner month view

* style(appointments): drop the calendar rules now covered by the global calendar style

* feat(appointments): split the appointment form into details and schedule columns

* style(appointments): lay the appointment form out in two columns with tighter spacing

* style(appointments): turn the time zone Change link into a small outlined button

* style(candidates): give the interviews page the same single card surface as appointments

* style(schedules): give the schedules page the same single card surface as appointments

* style(schedules): make the recurring availability time pickers dark filled inputs

* style(schedules): keep the page title next to the back arrow

* feat(i18n): translate the appointment, time zone and booking strings into the other locales

* fix(employee-multi-select): retry an employee's photo when its URL changes

* fix(selectors): forward an accessible name to the inner ng-select of the employee and time pickers

* fix(appointments): name the break start picker and the timezone dialog close button

* fix(public-layout): name the employee field and keep Book disabled until the page is ready
2026-10-01 21:51:42 +02:00
Cédric K. | Lord VB bc76f57426 Merge pull request #10351 from Cedric921/fix/search-keyword-splitting
fix(search): ignore empty keywords in employee, candidate and time off name search
2026-10-01 21:51:01 +02:00
Cédric K. | Lord VB b38d26bb40 Merge pull request #10352 from Cedric921/fix/query-dto-numeric-text
fix(core): stop the query DTO turning numeric search text into booleans
2026-10-01 21:50:21 +02:00
Cédric K. | Lord VB 63ee0903fe Merge pull request #10354 from Cedric921/fix/invoice-date-search
fix(invoices): search by invoice / due date filters on the selected day
2026-10-01 21:48:49 +02:00
Cédric K. | Lord VB a888cfdb5a Merge pull request #10355 from Cedric921/fix/global-stats-mikro-soft-delete
fix(stats): exclude soft-deleted invoices and payments from global stats on MikroORM
2026-10-01 21:48:02 +02:00
Cédric K. | Lord VB 0a6ca8467a fix(tags): count tag usages with COUNT(DISTINCT) and fill tagTypeName on MikroORM (#10358)
* fix(tags): count tag usages with COUNT(DISTINCT) and fill tagTypeName on MikroORM

TagService.findTags LEFT JOINs all 25 tagged relations (plus many-to-many
custom fields) in one query and counted each with COUNT(...). The joins
multiply rows, so every counter was multiplied by the matches of the other
relations: a tag on 2 employees and 3 tasks reported 6 + 6 = 12 usages on
the Tags page instead of 5. Use COUNT(DISTINCT ...) for every counter.

The MikroORM branch also never set `tagTypeName`, which the Tags page shows
in its Type column ("—" for every tag). Set it from the populated tagType.
The MikroORM branch still returns no *_counter fields (usage shows 0);
that needs an aggregate query of its own.

* refactor(tags): build the usage joins and counters from one table; always load tagType

- Drive the 25 relation LEFT JOINs and COUNT(DISTINCT) counters from an
  exported TAG_USAGE_COUNTERS table instead of 50 near-identical lines
  (Sonar duplication on new code).
- MikroORM: always populate tagType so tagTypeName is set even when the
  caller requests no relations.
- Spec: assert each relation is joined and counted under its expected
  counter (and nothing else), plus a many-to-many custom field.

* test(tags): cover tagType loading and tagTypeName on the MikroORM branch
2026-10-01 12:49:40 +02:00
Cedric Karungu ca97460c8a fix(employee): honor "Include deleted" in the MikroORM pagination branch
The employees page sends `withDeleted` from its "Include deleted" toggle.
The TypeORM branch of EmployeeService.pagination forwards it to
setFindOptions, but the MikroORM branch ignored it, so the global
soft-delete filter kept hiding deleted employees and the toggle had no
effect on MikroORM deployments.

Turn off SOFT_DELETABLE_FILTER when withDeleted is set, the same mapping
parseTypeORMFindToMikroOrm() already uses, and cover it with tests.
2026-09-30 22:32:30 +02:00
Aditya MitraandRuslan Konviser b8dfc31a33 resolve loading after employee creation error (#10307)
* close loading after employee creation error

* fix the review

---------

Co-authored-by: Ruslan Konviser <evereq@gmail.com>
2026-09-30 17:59:47 +02:00
Aditya Mitra 6fb5a40886 store page size between page loads for employees (#10319)
* store page size in localstorage

* fix the reviews
2026-09-30 17:57:39 +02:00
Lord VB | Evernix 1f9132f5ca fix(candidate,equipment-sharing): apply the requested sort order in pagination (#10341)
* fix(candidate,equipment-sharing): apply the requested sort order in pagination

The candidates and equipment sharing tables sort on the server, but
CandidateService.pagination and EquipmentSharingService.pagination build
their own queries and ignored the requested order in both ORM branches,
so sorting by their date (and candidate status) columns had no effect.
Same bug as the employee (#10338) and time off (#10335) paginations.

Apply the order through the shared parseSortOrder() allowlist:
- candidates: appliedDate, hiredDate, rejectDate, status
- equipment sharing: shareRequestDay, shareStartDay, shareEndDay
  (TypeORM via addOrderBy on the hand-built query builder)

* test(candidate,equipment-sharing): cover the sort order forwarded by pagination in both ORM branches
2026-09-30 17:56:58 +02:00
Lord VB | Evernix c09ff94663 fix(time-off): apply the employee, description and policy filters with MikroORM (#10344)
* fix(time-off): apply the employee, description and policy filters with MikroORM

In the MikroORM branch of TimeOffRequestService.pagination, the text
filters read `where.user`, `where.description` and `where.policy` from the
local query object being built (which only holds tenant, organization,
status, dates...) instead of the client filter, so filtering the time off
table by employee name, description or policy name had no effect with
MikroORM. The TypeORM branch reads them from options.where correctly.

Read them from options.where, and cover the three filters with tests.

* test(time-off): assert both halves of the name + date range condition

* fix(time-off): driver-compatible text filters and no empty name keywords (MikroORM)

- MikroORM emits `$ilike` verbatim as ILIKE, which MySQL and SQLite reject.
  Add mikroOrmContains() in core/utils: `$ilike` on PostgreSQL, `$like`
  elsewhere (their LIKE is already case-insensitive), and use it for the
  employee name, description and policy filters.
- Split the employee name on any whitespace and drop empty keywords: with
  repeated spaces, split(' ') produced '' and a `%%` pattern matching
  every name.
- Tests for both, plus the helper.

* refactor(time-off): push both name conditions in one call (Sonar S7778)

* fix(core): detect the MikroORM driver from dbMikroOrmConnectionOptions for mikroOrmContains

getDBType() reads dbConnectionOptions (the TypeORM options, no driver)
and matches the driver with instanceof, while the MikroORM config sets
`driver` to the driver class in dbMikroOrmConnectionOptions. Under
MikroORM it therefore always answered PostgreSQL, so mikroOrmContains()
still produced $ilike on MySQL / SQLite.

Add isMikroOrmPostgres(), which reads dbMikroOrmConnectionOptions.driver
and matches the class or an instance, and use it as mikroOrmContains()'s
default. getDBType() itself is left unchanged: its other MikroORM callers
(custom entity fields, seeds) would change behaviour on MySQL / SQLite.
2026-09-30 17:55:47 +02:00
Lord VB | Evernix c42387d387 fix(employee): apply the requested sort order in employee pagination (#10338)
* fix(employee): apply the requested sort order in employee pagination

The employees table marks Income, Expenses, Bonus and Time Tracking as
sortable and sorts on the server (order[averageIncome]=ASC etc.), but
EmployeeService.pagination builds its own query and ignored options.order
in both the TypeORM and MikroORM branches, so those sorts had no effect.
Same bug as #1941 in the time off pagination.

Move the order sanitizing added for #1941 into a shared parseSortOrder()
in core/utils (allowed columns only, ASC/DESC only, client key order kept),
use it for employees and time off, and cover it with unit tests.

* fix(employee): access the required pagination options without optional chaining

options is a required parameter and is already destructured unguarded
above, so the optional chaining in the MikroORM branch was inconsistent
(DeepScan INSUFFICIENT_NULL_CHECK).

* fix(employee): parse the sort order inside each ORM branch

Reading options.order before the switch was an unguarded access ahead of
the TypeORM branch's `options && ...` checks (DeepScan
INSUFFICIENT_NULL_CHECK). Parse it in each branch, following that
branch's existing access style.

* refactor(employee): pass the parsed sort order inline

Avoid lexical declarations in case blocks (Sonar S6836) and use an
optional chain (S6582). An empty order map leaves the query unsorted,
so no conditional spread is needed.

* test(employee): cover the sort order forwarded by pagination in both ORM branches
2026-09-30 14:10:00 +02:00
Lord VB | Evernix 15fd2639c8 fix(time-off): apply the requested sort order in time off pagination (#10335)
* fix(time-off): apply the requested sort order in time off pagination

The Time Off table sorts on the server (ServerDataSource sends
order[start]=ASC etc.), but TimeOffRequestService.pagination builds its
own query and ignored options.order in both the TypeORM and MikroORM
branches, so sorting by Start, End or Request Date had no effect.

Pass the requested order to the ORM, keeping only the sortable date
columns (start, end, requestDate) with an ASC/DESC direction.

Closes #1941

* fix(time-off): only accept string sort directions

Read the direction only when the query value is a string instead of
stringifying any value (Sonar S6551: objects would become '[object Object]').

* fix(time-off): keep the client's sort key precedence

Iterate the requested order keys instead of the allowlist, so a
multi-column sort keeps the order the client asked for.
2026-09-29 20:07:20 +02:00
joel kalema a73de1d121 Feat/add employee dialog polish (#10336)
* feat(i18n): add strings for the add-employee dialog and form sections

* feat(user-forms): upload the profile photo through the shared avatar uploader

* feat(user-forms): group basic info fields into profile, account and employment sections

* feat(employees): track queued employees and the current entry in the add dialog

* feat(employees): redesign the add-employee dialog with labelled steps, review list and fixed footer

* style(employees): style the add-employee dialog header, steps, list and footer

* feat(tags): draw the picker checkbox and show selected tags as tinted labels

* style(tags): compact rows for the tag picker dropdown panel

* fix(employees): vertically centre the Show Deleted toggle in the header

* fix(employees): give Status and Screen Capture columns room from Name and Email

* fix(employees): address review feedback on the add-employee dialog

- mark template-only members protected
- translate the step bar's aria-label
- clear loading in a finally block so a failed create can be retried
- fit the step bar on narrow dialogs by naming only the current step

* fix(employees): resolve SonarCloud reliability issues in touched files

- mark fire-and-forget promises with void (typescript:S9383)
- drop async from delete(), which awaits nothing (typescript:S7503)

* fix(user-forms): hide SUPER_ADMIN from the role picker once the permission check resolves

excludeRoles() resolves after the role field may already have rendered its
options, and it mutated the excludes array in place, so the field kept
offering SUPER_ADMIN to users without that role.

- basic info form: assign a new excludes array instead of pushing
- role field: cache the fetched roles and re-filter them whenever excludes
  changes, clearing a selection that is no longer allowed

* fix(user-forms): keep a preselected role until the role list has loaded

applyExcludes() also runs from the excludes setter, which can fire before
getAll() returns. With no roles loaded every preselected role looked
disallowed and was cleared. Only check the selection once the roles have
loaded; renderRoles() re-runs the check at that point.
2026-09-29 20:06:44 +02:00
Sairaj-24 b00a36df4f fix(timer): toast a setting change only when the value changes (#10315)
* fix(timer): toast a setting change only when the value changes

Re-clicking the same Desktop Timer setting wrote it again and stacked another success alert. Skip the write and the toast when the stored value is unchanged.

Fixes #8479

* fix(timer): compare settings against the last saved value

SSL and auto-start edit the stored object before the equality check, so those saves were skipped.
2026-09-29 14:59:47 +02:00
Ruslan KonviserandClaude Opus 5.5 74d538b770 test(core): pin what the tenant-isolation guards actually do (#10328)
* test(core): pin what the tenant-isolation guards actually do

TenantBaseGuard and TenantPermissionGuard gate most controllers
(@UseGuards(TenantPermissionGuard, PermissionGuard)), yet no spec exercised
them: every other suite mocks or overrides them. This adds 39 behavioural
tests covering the tenant from the request context, the tenant-id header
path, the GET/DELETE query and data.findInput paths, the POST/PUT/PATCH body
paths, @Public, the super-admin switch, handler-over-controller permission
metadata, de-duplication, and the 5-minute permission cache (hit, miss,
per-role key).

Two current behaviours are pinned deliberately and documented in the spec,
not changed: a matching tenant-id header is the whole check (a body or
query tenantId naming another tenant passes the guard, so services must take
the tenant from context), and a permission verdict is cached for 5 minutes.

Mutation-checked: making the header or body comparison always pass,
ignoring the base guard's verdict, dropping the allowSuperAdminRole switch,
or changing the cache TTL each turns the suite red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(core): follow the house let/const rule and drop the lint findings in the guard spec

Review follow-up (Greptile). The context helper now declares the request
headers with let and rebuilds them instead of mutating a const, per the
repository's rule. Also clears the ESLint error (empty stub function) and
the nine no-explicit-any warnings the new spec added: typed casts through
unknown, and one typed handle on env.allowSuperAdminRole. Still 39/39, and
all five guard mutations are still caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:42:21 +02:00
joel kalema 4c0d30c4e1 Feat/task details polish (#10329)
* feat(record-view): add opt-in wide input to the record drawer

* feat(record-view): toggle the wide drawer class from the input

* style(record-view): size and pad the wide drawer for long-form content

* feat(tasks): render task descriptions from GitHub-flavoured markdown and safe HTML

* feat(tasks): add task view component deriving pills, people and description

* feat(tasks): lay out task view like an issue tracker with a collapsible details panel

* style(tasks): style task view details, avatars and markdown content

* feat(tasks): declare the task view component

* feat(tasks): show the task view in the wide drawer

* refactor(tasks): drop the record-view descriptor now served by the task view

* feat(record-view): add markdown and status field types, section variants and row icons

* feat(record-view): render panels, person chips, status and markdown in the shared view

* feat(tasks): describe the task view through the shared record view

* refactor(tasks): remove the separate task view component

* refactor(record-view): split markdown block rendering into per-block renderers

* refactor(record-view): derive avatar hue without bitwise truncation

* fix(record-view): keep fenced code intact when stripping comments and split list parsing

* fix(record-view): guard meta rows by permission and use inject() with class bindings

* fix(tasks): honour done statuses when flagging an overdue task

* fix(record-view): close code fences only on a matching fence and resolve Sonar findings in the markdown renderer

* style(record-view): merge the duplicate wide drawer selector

* fix(record-view): detect tilde fences, keep terminal # in headings and make placeholder restoration terminate

* fix(record-view): strip placeholder marks from reference URLs and restore placeholders in one pass
2026-09-29 13:34:28 +02:00
Ruslan KonviserandClaude Opus 5.5 c12fc11a50 fix(auth): make email verification work end to end and stop register dead-ends (#10332)
* fix(auth): make email verification work end to end and stop register dead-ends

Verification link and notice
- /auth/confirm-email no longer sits behind NoAuthGuard. Registering signs the
  user in, so the emailed link was opened by a signed-in user, redirected to the
  dashboard before the resolver ran, and never verified anything. The token is
  still required and still single use (the API clears it on success).
- A refused or expired link now shows its message instead of an endless spinner,
  and a successful one marks the signed-in user verified in the store.
- New "verify your email" notice with Resend (POST /auth/email/verify/resend-link,
  already throttled 3/min) in the main layout and on tenant onboarding. It shows
  only when GET /auth/email/verify/status (new, same feature flag, so 404 where
  verification is off) confirms the user is unverified.
- Settings > Billing: an unverified admin with no linked subscription is told a
  paid plan connects once the address is verified.

Email sending
- Templates fall back to English when the recipient's locale has none, instead
  of rendering an empty email (verification exists only in en/bg/he/ru).
- Send failures are logged with the provider code, SMTP reply code and command,
  every address masked; verification sends are now recorded in email_sent with
  status SENT/FAILED (subject only, never the link). A transport that fails
  verification throws instead of returning undefined.
- resend-link answers 503 when the provider refused the message, not "OK".
- The verification link encodes the address (plus-addressing survived as a space).
- A caller-supplied appEmailConfirmationUrl is honoured only on an origin this
  deployment serves (CLIENT_BASE_URL, the configured links, EMAIL_LINK_ALLOWED_ORIGINS;
  "*" disables the check).
- Auth emails carry X-PM-TrackLinks: None so Postmark stops storing tokens as clicks.
- {{appLink}} falls back to CLIENT_BASE_URL when APP_LINK is empty: every
  hosted deployment has APP_LINK empty, so welcome emails linked to localhost:4200.

Register
- A refused sign-up shows the API's 4xx message (e.g. "A subscription is
  required...") instead of "Something went wrong", and the 403 checkoutUrl is
  offered as a "Continue to checkout" button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(auth): confirm-email trusts the API, not the link, for who was verified

Review follow-ups on the confirm-email page:
- After a successful confirmation, re-read GET /auth/email/verify/status
  instead of comparing the link's email parameter with the signed-in user.
  The token decides which account was confirmed; the email parameter is not
  bound to it.
- A request that got no HTTP answer (status 0) is shown as a connection
  problem, not as an invalid link.
- ActivatedRoute, Store and AuthService come from inject().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(auth): apply a verification status only to the user it was asked for

If a different user signs in while the status or resend request is in flight,
the answer no longer updates the new user's verification state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-core): drop a resend answer once another user has signed in

The verification notice now tracks the user it speaks for. A different user
signing in cancels the pending resend and resets its state, and a late answer
for the previous user changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-core): a dismissed verification notice stays dismissed only for that user

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:32:46 +02:00
Ruslan KonviserandClaude Opus 5.5 c213fb3ea6 chore(cspell): ignore the malformed-session-id fixture word in the billing spec (#10334)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:48:24 +02:00
Ruslan KonviserandClaude Opus 5.5 ba62a02001 fix(billing): scope Stripe linking, paywall and billing pages to this deployment's product (#10331)
Product-scoped (hosted plan only) Stripe webhook linking, signup paywall, lazy tenant link and /billing routes; checkout-session proof at register/onboarding; BILLING_PRODUCT, BILLING_SIGNUP_PAYWALL and BILLING_WEBHOOK_LINKING (default off); 402 payment_method_required on a paid upgrade without a card. See the PR description for details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:34:26 +02:00
Lord VB | Evernix 367a01a66d fix(desktop): use a single ErrorHandler that forwards to Sentry and ErrorHandlerService (#10326)
* fix(desktop): use a single ErrorHandler that forwards to Sentry and ErrorHandlerService

The desktop, agent and desktop-timer apps registered two ErrorHandler
providers. Angular only keeps the last one, so either Sentry never saw
uncaught errors (desktop) or the app toast/logging handler was dead
(agent, desktop-timer).

Replace both with one factory provider that delegates to the Sentry
handler and then to ErrorHandlerService. Sentry console logging is
disabled since ErrorHandlerService already logs the error.

Closes #9106

* refactor(desktop-ui-lib): extract provideGlobalErrorHandler to remove duplicated provider

Move the combined ErrorHandler factory into desktop-ui-lib so the three
desktop apps share one implementation. The reporting handler (Sentry) is
passed in, so desktop-ui-lib does not need a Sentry dependency.

* fix(desktop-ui-lib): always run ErrorHandlerService and guard nullish errors in global handler

Run ErrorHandlerService in a finally block so a throwing reporting handler
cannot skip it, and wrap nullish values in an Error before passing them to
ErrorHandlerService. The value sent to the reporting handler is unchanged.
2026-09-28 19:54:07 +02:00
joel kalema a15f8536cd Feat/accounting dashboard polish (#10327)
* feat(i18n): add English strings for the accounting dashboard view

* feat(dashboard): redesign the accounting dashboard with KPIs, cash-flow chart and sortable breakdown

* fix(dashboard): keep employee chart legend items at the list font size

* feat(i18n): add Acholi strings for the accounting dashboard view

* feat(i18n): add Arabic and Hebrew strings for the accounting dashboard view

* feat(i18n): add Bulgarian strings for the accounting dashboard view

* feat(i18n): add German strings for the accounting dashboard view

* feat(i18n): add Spanish strings for the accounting dashboard view

* feat(i18n): add French strings for the accounting dashboard view

* feat(i18n): add Italian strings for the accounting dashboard view

* feat(i18n): add Dutch strings for the accounting dashboard view

* feat(i18n): add Polish strings for the accounting dashboard view

* feat(i18n): add Portuguese strings for the accounting dashboard view

* feat(i18n): add Russian strings for the accounting dashboard view

* feat(i18n): add Chinese strings for the accounting dashboard view

* fix(dashboard): address accounting dashboard review findings

- keep the signed income share for the label, clamp only the bar
- reject non-positive total income as a percentage denominator
- add a screen-reader table of the chart's daily values
- let long KPI amounts wrap instead of truncating
- format chart dates in the active UI language
- translate the accounting view strings in Hebrew and Acholi

* feat(dashboard): restore the cash-flow chart's day grid, point dots and full axis labels

- draw a vertical grid line per date, in the value grid's colour
- show a dot on every day for each series
- label the y-axis with full figures and the x-axis with full dates

* feat(dashboard): add a sort control to the earnings breakdown

- add a sort-by select and a direction toggle to the breakdown header
- sort by total expenses, highest first, by default
- start-align the money columns and trail their sort arrows
- narrow the employee column so the money columns get more room
- add the sort label and direction strings in every locale

* refactor(dashboard): inject the accounting view's theme service and currency pipe with inject()

* fix(dashboard): address accounting chart and header review findings

- keep a vertical grid line for every date; thin only the colliding labels
- format y-axis figures in the active UI language
- let the breakdown panel header and sort controls wrap on narrow screens

* fix(dashboard): read the x-axis tick font through CartesianScaleOptions
2026-09-28 19:53:11 +02:00
joel kalema 702a02bb28 Fix/documents actions column (#10323)
* fix(docs-ui): let the table row kebab open its actions menu

nbContextMenu's click trigger listens on document, so stopping the click on the button meant the menu never opened. The table's row-open handler already ignores clicks from buttons.

* docs(docs-ui): update the table double-click guard comment

* fix(docs-ui): let the card kebab open its actions menu

The card body now skips clicks, double clicks and Enter coming from its kebab instead of the kebab stopping propagation, which kept nbContextMenu from ever opening.

* fix(docs-ui): keep a tree node from activating on a kebab click

Override the tree's mouse click action so a click on the node menu button does not toggle the active node.

* fix(docs-ui): let the tree node kebab open its actions menu

Drop the stopPropagation that kept nbContextMenu (and the Shift+F10 path) from opening the node menu.

* feat(docs-ui): add column icons and column chooser strings

Per-column Eva icons and the DOCS.TABLE strings for the column count, the locked and auto-hidden hints, Show all and Reset to default.

* feat(docs-ui): add show-all, reset and visibility state to the column chooser

Visible-column count, whether any preference is stored, which columns the narrow-viewport defaults hid, and actions to show every column or drop the stored preferences.

* fix(docs-ui): open the column chooser and redesign it

nbTooltip and nbPopover on the same button shared one NbDynamicOverlay, so a click only ever showed the tooltip. The tooltip now sits on a wrapper.

The chooser becomes a list of switch rows with column icons, a locked Name row, an auto-hidden hint for narrow screens, a live count and Show all / Reset to default actions.

* feat(docs-ui): group the document actions menu and make it context-aware

Every item gets an icon and the menu is split into sections (open, create, organize, share, AI, destructive) with dividers only between non-empty ones. Delete is marked danger and the tree shows its F2 and Del shortcuts.

A FILE row offers Preview instead of a second, identical Open; Open on a folder or page says where it goes; Duplicate with children is dropped for a container the list reports as empty.

* feat(docs-ui): tag the table, card and tree action menus with a shared class

Pass DOCS_ACTION_MENU_CLASS through nbContextMenuClass so the three kebab menus can be styled as one.

* style(docs-ui): restyle the document actions menu

Compact rows with icons, rounded hover, section dividers, key-cap shortcut hints and a red Delete. Global on purpose: the menu renders in the CDK overlay, and the shell wraps every Documents route.

* feat(docs-ui): add the actions menu to folder cards

Folder cards had no kebab, so in card view a folder could only be drilled into. The kebab sits beside the folder card button in a wrapper, since a button may not contain another one.

* refactor(docs-ui): mark template-only members protected

Follows the Angular guideline for the column chooser members and the action menu class added in this branch.

* fix(docs-ui): keep tree key handling off the node menu button

The tree handles Enter and Space on body and calls preventDefault, which cancelled the menu button's own click. The button now stops their propagation without preventing the default. Also marks the tree's actionMenuClass protected.
2026-09-28 13:39:43 +02:00
Ruslan KonviserandClaude Opus 5.5 5c8d4e1c8c chore(cspell): declare or reword the words #10324 added
The develop push of #10324 (9a6f8787) failed "Check Spelling and Typos
with cspell" (run 36399734369): 13 issues in 5 files. PRs into develop
run no cspell, so this surfaced only after the merge.

- "Nx's" (4x) and "callables": reworded in the comments.
- "internmap" (a d3 dependency, npm package name): added to .cspell.json.
- "avascript" / "msdt" in the safe-url spec: file-level cspell:ignore,
  as the repo does elsewhere; they are the tail of the entity-encoded
  "javascript:" payloads and the ms-msdt: scheme the tests feed in.

Comment, dictionary and directive changes only; no code or config
behaviour changes. Local cspell 6.31.3 on the five files: 0 issues
(the pre-fix safe-url spec, as a control: 6 issues).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:04:48 +02:00
Ruslan KonviserandClaude Opus 5.5 89d3d51c98 test(docs-ui): check the absolute ceiling on one cold call, before the batches
A synchronous call cannot be interrupted (Jest's timeout included), so a
grossly slow isAllowedUrl is now reported after ONE call on the smaller
hostile input - the same single cold call the old 100/200 ms asserts
timed - instead of after the batches and the fourfold input have
multiplied the wait (CodeRabbit). Running each measurement in a child
process, as also suggested, is not done: a call that never returns
already ends in a failure (the job timeout), never a pass, and spawn/IPC
time would be billed to the measurements.

Known-dirty control (local): an injected O(n^2/200) scan in
isAllowedUrl makes the data: linearity test fail with a growth factor of
16.8 against the < 10 bound; the restored code passes (docs-ui 541/541).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:49:53 +02:00
Ruslan KonviserandClaude Opus 5.5 dc0897c29d test: address review findings on the unit-test harness
- docs-ui safe-url linearity tests: time each input size in batches of
  calls (doubled until a batch runs >= 50 ms, median of three) and take
  the growth ratio as measured, with no 1 ms floor that could understate
  growth for sub-millisecond samples (CodeRabbit). Keep a generous
  absolute ceiling (3 s per rejection of the smaller input, ~9x the
  slowest CI reading) so a uniformly slow validator cannot pass either.
- role-permission demo-mode suite: state its scope precisely. The reload
  only adds rows, so it never removes a deletion grant a tenant already
  holds; the seed and RolePermissionService keep demo tenants free of it,
  and PermissionGuard has no demo-mode rule (a product question, out of
  scope for this test-harness PR).
- Sonar: String.raw for the transformIgnorePatterns regex (pattern
  verified byte-identical), startsWith in jest.resolver.js, and no
  blanket eslint-disable in the new activepieces jest config (its
  template, integration-make-com, has none; eslint clean).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:26:56 +02:00
Ruslan KonviserandClaude Opus 5.5 e39e9323c6 test: finish the Angular stub specs; interop for callable CJS, d3 transforms
Local runs after this commit: ui-core 45/45 suites, ui-auth 4/4, gauzy
29/29, desktop-ui-lib 41/41, gauzy-server 2/2, and every plugin UI project
that was red (integration-ai/github/hubstaff/upwork-ui, job-employee/
matching/proposal/search-ui, jobs-ui) green.

- jest.interop.js now patches every callable CommonJS package the code
  imports both ways (moment, randomcolor) with `default` + `__esModule`, so
  both import styles resolve to the function under either esModuleInterop.
  apps/gauzy's spec tsconfig turns esModuleInterop on (as ui-core, docs-ui
  and videos-ui do): ui-core's line chart default-imports
  chartjs-plugin-annotation and Chart.register received undefined.
- jest.preset.js transforms d3-* / internmap (ESM-only, reached through
  @swimlane/ngx-charts).
- desktop-ui-lib specs: section forms get the FormGroup their parent passes,
  cell renderers their rowData, dialogs their data; Settings/Setup/
  ScreenCapture get the library's GAUZY_ENV and the time tracker the
  AuthStrategy/AuthService the desktop apps bootstrap; the task table a
  signed-in session. Two test-side overrides are documented in place:
  LanguageSelectorComponent binds [(ngModel)] without FormsModule (a dead
  binding in the app too), and the plugin source forms use <nb-hint> as a
  plain styled element (Nebular has no such component).
- apps/gauzy: page specs get the date-picker config their route sets,
  the timesheet layout / job pages their route data, the task dialog a
  selected organization (its unguarded async read crashed the Jest worker)
  and a stand-in for docs-ui's links panel (docs-ui's entry point loads a
  PDF viewer that uses import.meta, which CommonJS Jest cannot parse).
- integration-upwork-ui: two hand-written specs used jasmine spies (jest
  has none) and partial Router/TranslateService mocks the template could
  not render with; they use jest.fn() and the real services now.
- gauzy-server's AppComponent spec was the 2021 CLI scaffold (title
  'desktop-web-ui', a "Welcome" h1) and declared a standalone component; it
  now imports it and asserts what the shell does (router outlet, language
  bridge started on init), with the library entry mocked to its one token.
- jobs-ui: the layout spec has asserted a main landmark since it was
  written; the layout now renders <main role="main"> around its outlet (the
  app shell defines no other main landmark).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:25:57 +02:00
Ruslan KonviserandClaude Opus 5.5 d3d8687ac1 test: app-wide TestBed defaults for the Angular projects; fix their stub specs (WIP)
Once the Angular suites loaded, most failures were CLI-generated
`should create` stubs that had never run, failing on the first injection
(TranslateService, NbToastrService, NbDialogRef, ActivatedRoute, the
translate pipe, the default icon pack).

- jest.angular-defaults.ts (setupFilesAfterEnv, after each project's
  test-setup): a top-level beforeEach adds what the app's root module
  provides - TranslateModule.forRoot, the Nebular forRoot modules,
  NgxPermissionsModule.forRoot, the Tabler/eva icon pack, HttpClient
  testing, router, noop animations, a NbDialogRef stub, GAUZY_ENV - and a
  matchMedia stand-in (jsdom has none). configureTestingModule merges, so a
  spec's own providers still win. It imports no ui-core/core: that would
  cache real modules a spec later jest.mock()s.
- jest.preset.js maps dayjs/esm (imported by ngx-daterangepicker-material's
  .mjs bundle) to dayjs's CommonJS build.
- Specs of non-standalone components import the NgModule that declares
  them, provide what the host feature module provides (EmployeesService,
  AuthService, NbAuthModule.forRoot, PipesModule), and set the inputs /
  route data / date-picker config the app always supplies before render.
- Two stubs imported classes their files do not export
  (GauzyRangePickerComponent, ViewComponent) and declared `undefined`.
- desktop-ui-lib's test-setup installs an inert `window.electronAPI`
  preload bridge of the shape apps/agent exposes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:57:14 +02:00
Ruslan KonviserandClaude Opus 5.5 02c4289f5f test: load moment under both import styles; resolve ui-core's workspace imports from source
Iteration 1 of the hermetic run (36330457608, 83 pass / 14 fail) moved the
Angular suites past TestBed init and into two load-time failures:

- "(0, moment_1.default) is not a function" in ~50 suites across 11
  projects. ui-core imports `moment` as a default import, most other code
  as a namespace import; the bundlers accept both, the TypeScript CJS emit
  Jest runs cannot under a single esModuleInterop setting. jest.interop.js
  (a preset setupFiles entry, concatenated with any project's own) gives
  the loaded module `default` and `__esModule`, so both styles resolve to
  the moment function itself, as they do in the app build.
- "Cannot find module '@gauzy/ui-config'" in 23 ui-core suites: ui-core's
  tsconfig.json maps it to the BUILT dist copy, which a test run does not
  have, and Nx's Jest resolver falls back to the spec tsconfig's paths. The
  spec tsconfig now carries the same source mappings as tsconfig.lib.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:05:39 +02:00
Ruslan Konviser d0b2a8e080 Merge remote-tracking branch 'origin/develop' into fix/unit-tests-hermetic-harness 2026-09-27 20:53:10 +02:00
Ruslan KonviserandClaude Opus 5.5 ab886b135f fix(security): prove GitHub installation ownership before binding it (GHSA-4rwq) (#10318)
* fix(security): prove GitHub installation ownership before binding it (GHSA-4rwq)

POST /integration/github/install bound whatever installation_id the request
body carried, as long as the state nonce belonged to the caller's tenant. The
nonce proves which tenant started the flow, not which GitHub installation that
tenant may bind — and binding hands the tenant the App's token for every
repository in the installation. A tenant could bind another organization's
installation and read its private repositories.

The GitHub App now issues an OAuth code with the post-install redirect ("Request
user authorization (OAuth) during installation"). POST /install — the
authenticated request — exchanges it and binds only an installation the
authorizing GitHub user is entitled to in full:
- a personal installation: the user must be that account;
- an organization installation: the user must already reach every repository
  it covers (their count equals the App's own count, read before and after to
  close a create/delete race).
The code is signed with the nonce it arrived with, so a code leaked from a
post-install URL cannot be replayed against another tenant's nonce, and the
user token is revoked once checked. Without a code the install is refused with
a message naming the GitHub App setting to enable.

Also closes a sibling on the same surface: GithubMiddleware loaded an
integration's settings before authentication using ?tenantId= ahead of the
Tenant-Id header, while the tenant guard validates only the header when one is
sent. Own tenant in the header plus a victim's in the query let the repository,
metadata, issue and sync routes act on the victim's installation. The
middleware now records who owns the settings, and GithubIntegrationTenantGuard
refuses a mismatch after authentication.

The install popup now shows why a connection was refused instead of closing
after two seconds, and handles the update/request redirects GitHub sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(security): spend the install code at the callback, compare repository ids

Two P1 review findings on the first version of this fix, both correct:

- The post-install callback is public and signed ANY code with ANY live nonce,
  so a leaked, unused victim code plus a nonce from the attacker's own tenant
  still bound the victim's installation — the code binding proved nothing. The
  callback now exchanges the code the moment it arrives (spending it, so it
  cannot be replayed from a URL, history or log), checks entitlement there, and
  hands the browser only a signed, 10-minute proof bound to this flow's nonce
  and installation. The code never reaches the browser, and POST /install binds
  nothing without a valid proof. When there is no proof, install_check tells
  the web app why (no code / not entitled / unverifiable).
- Comparing repository COUNTS could be balanced by a member who creates
  throwaway repositories and deletes them between the reads while the
  repositories hidden from them remain. Entitlement for an organization
  installation is now a set comparison: every repository id the App can reach
  must be one the user can read (user ids read first, App ids after).

Also: installation ids beyond Number.MAX_SAFE_INTEGER are refused (Octokit
takes a number and would check a different installation than the one stored),
member install requests (setup_action=request) no longer hit a raw 400, and the
popup text is translatable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:26:32 +02:00
Ruslan Konviser 4c4e554e2b Merge remote-tracking branch 'origin/develop' into fix/sentry-quota-flood 2026-09-27 17:48:39 +02:00
Ruslan KonviserandClaude Opus 5.5 91d91ffca3 test: make the unit-test run hermetic and fix the Angular jest harness
The Unit Tests workflow has never been green (24 of 97 projects red at
develop eac7136562, run 36319732347). Causes addressed here:

- Env leak: Nx loads the committed .env.local (DEMO=true,
  WORKER_QUEUE_ENABLED=false, NODE_ENV=development) into every task.
  The test step now sets NX_LOAD_DOT_ENV_FILES=false, and the two specs
  that depend on a mode pin it themselves (role-permission counts pin
  environment.demo=false and gain a demo-mode suite asserting 209 per
  role; the worker spec clears the queue env before the constants load).
  .env.local is unchanged.
- Angular harness: nohoist gives each workspace its own @angular copy,
  so setupZoneTestEnv() initialised a different TestBed from the one the
  spec used. A workspace resolver (jest.resolver.js, wrapping Nx's) makes
  every @angular/* import in a Jest project resolve to one copy. The
  Angular projects now inherit the preset's transformIgnorePatterns,
  which gains the .mjs exception plus @datorama, @ngneat and lodash-es.
- ui-config's environment.ts is generated and gitignored; the workflow
  runs `yarn config:dev` before the tests, as the Playwright workflow does.
- Misconfigured targets: gauzy (jest.config.js -> .ts, Angular transform,
  setupFile moved into the config), integration-sim-ui (.ts -> .cts),
  integration-activepieces (config file added), mcp-auth (ran
  `node build/main.js --test`; now the jest executor like apps/mcp).
- Real failures: the openai "silence" case used a body with no `text`,
  which the shared helper rejects by contract; the toolbar spec read the
  tabIndex property, which is 0 on any button; the docs-ui linearity
  tests asserted wall-clock bounds, now a 4x-input growth ratio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:39:40 +02:00
Ruslan KonviserandClaude Opus 5.5 f7659fd4fb fix(sentry): route fatal logs like errors
SENTRY_LOG_LEVELS accepted 'fatal', but SentryService had no fatal override, so
Nest's ConsoleLogger.fatal printed the message and Sentry never saw it (also
before this branch). A fatal log now becomes an event whenever fatal or error is
captured, and is a breadcrumb otherwise. The level list is a Set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:29:08 +02:00
joel kalema 0fe8348260 Merge pull request #10313 from joel-kalema/fix/screenshots-gallery-viewer-clean
Fix/screenshots gallery viewer clean
2026-09-27 09:09:49 +02:00
Ruslan Konviser 47b0a55d3f Merge pull request #10311 from ever-co/fix/plugin-jest-esm-transform
test(plugins): make the videos, job-proposal and wakatime specs load and run
2026-09-27 00:07:36 +02:00
joel kalema e9e6bfd9fa Merge pull request #10308 from joel-kalema/feat/contacts-pages-polish
Feat/contacts pages polish
2026-09-26 20:17:45 +02:00
Ruslan KonviserandClaude Opus 5.5 c5513fc474 docs(core): say allowJs only affects repo-local .js in the idempotency README
Review follow-up (CodeRabbit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:00:54 +02:00
Ruslan KonviserandClaude Opus 5.5 69a293175a fix(sentry): only errors become Sentry events by default; stop logging health probes
The whole ever-co Sentry organisation (5,000 errors a month on the current
plan) has accepted no error after the first hours of each monthly reset:
2026-07-09, 08-09 and 09-09 are the only days with accepted errors in 90 days.
About 97% of what was accepted were info-level log lines, not errors.

- SentryService (the API's Nest logger) turned every log/warn/debug/verbose
  call into its own Sentry event and ignored the `logLevels: ['error']` the API
  passes. It now honours `logLevels`: listed levels become events, the rest
  become breadcrumbs on the next event. An unset or empty list keeps the old
  capture-everything behaviour. The API reads the list from SENTRY_LOG_LEVELS
  (default `error`), so capturing warnings or logs again is a config change.
- RequestContextMiddleware logged the start and end of every request,
  including the Kubernetes readiness probe on /api/health every 10 s on every
  pod: 2 events per probe, ~2,900 events an hour from the four Ever Teams API
  pods alone. /api/health and /api/health/* are no longer logged. This is
  decided by path only, since a client-set User-Agent must not be able to hide
  other requests.
- apps/api/src/sentry.ts printed the DSN at startup and ran the SDK in debug
  mode in production (`environment.production` is false in the published
  image), writing several SDK lines per request. Debug is now opt-in with
  SENTRY_DEBUG=true, and the DSN is no longer printed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:00:44 +02:00
Ruslan KonviserandClaude Opus 5.5 fed7700006 docs(jest): stop saying allowJs is what makes the ESM transform list work
Review follow-up (Greptile). core's and integration-zapier's
tsconfig.spec.json comments, and the idempotency README, still said allowJs
was required for transformIgnorePatterns to take effect. With the pinned
ts-jest (>= 29.3.2) that is no longer true for files under node_modules,
which is all the list covers. Reword the three comments; the allowJs
settings themselves are unchanged (identical parsed JSON).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:59:12 +02:00
Ruslan KonviserandClaude Opus 5.5 4eec30ca27 test(plugins): make the videos, job-proposal and wakatime specs load and run
Six service/controller specs in three plugins failed to load, so they ran
zero tests. Two defects were stacked, and the first hid the second.

1. Their testing modules load @gauzy/core, which imports uuid. The installed
   uuid (14.0.0) is ESM-only. The allow-list of ESM packages Jest must
   transform lived in packages/core/jest.config.ts, with a copy in
   integration-zapier, and not in the shared preset. Move it into
   jest.preset.js so inheriting projects get it, and let core inherit it
   (jest --showConfig resolves to the byte-identical pattern). zapier keeps
   its own copy, which replaces the preset's, and now points at the preset.
   No allowJs is needed: ts-jest >= 29.3.2 compiles node_modules .js to
   CommonJS regardless. All six specs pass with allowJs unset.

2. The specs are Nest CLI "should be defined" scaffolding whose testing
   modules never resolved the dependency graph of the class under test.
   Add stub providers for exactly what each class injects, and override
   the @UseGuards guards in the controller specs. Nest builds those in the
   module that owns the controller, but they are not constructor
   dependencies. The spec changes are additive only: no it/expect changed
   and nothing declared was removed.

Verified locally, before vs after on 1b2278d329:
- the three plugins go from 0 tests run to green (videos 3/3, 8 tests;
  job-proposal 2/2; wakatime 2/2)
- the other 31 plugins with specs are unchanged, suite for suite
- the 11 non-plugin projects that inherit the preset and have specs are
  unchanged
- core passes 179/179 suites, 2174 tests
- a mutation check (drop one repository stub) turns the spec red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:53:02 +02:00
Ruslan KonviserandClaude Opus 5.5 0da9a5d4fb fix(desktop): fix update prompt, auto-update delay and prerelease channel
The "new version available, download it?" dialog was registered with
autoUpdater.once(), so it could appear only once per app run. Since every
check now looks up the newest release again (#10305), a release published
later in the same run got no dialog. The once() listener was also used up
when the first version was found while automatic updates were off, so no
dialog appeared in that run even after they were turned back on. The
dialog now appears once for each new version. A version the user has
already answered in this run is not offered again, including the repeat
event that follows choosing Upgrade.

Only one update dialog is shown at a time. The dialog does not open while
it or the "ready to install" prompt is still showing; the next check
offers that version again. A "ready to install" prompt for a download
that finishes while the dialog is open waits until the dialog is
answered. An error showing either dialog is now logged instead of being
left as an unhandled rejection. The OS notification and the settings
page events are unchanged.

The settings page sends automatic_update_setting as { isEnabled, delay },
but the main process read automaticUpdateDelay, and AutomaticUpdate's
delay getter threw away any delay passed in and used the stored setting.
It worked only because the settings page happens to save the setting
first. The handler now accepts both names, handled on the receiving side
so existing settings pages keep working. The loop uses the delay it was
sent, falling back to the stored delay and then 1 hour. Delays that
setInterval cannot hold are ignored, since they would make it run every
millisecond.

Turning on the prerelease channel filtered the GitHub releases down to
prereleases only, so those users were never offered a newer stable
release. On the real release list they were held at v111.44.15, or even
v102.0.1, while v111.44.48 was out. The prerelease channel now means the
newest release of either kind, still preferring the newest one that
already has this platform's update file. Stable-only users are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:11:01 +02:00
Ruslan KonviserandClaude Opus 5.5 f32a4488bd fix(core): make role permissions unique per tenant, role and permission
Nothing stopped a role from holding the same permission twice. The
*RolePermissionsReload* migrations read which permissions a role lacks
and then insert them, so two API processes running them against one
database at the same time both inserted the same rows. Any install that
ran two instances through those migrations can hold millions of such
duplicate role_permission rows.

Add migration AddRolePermissionUniqueIndex1790000017000, which creates
the unique index IDX_role_permission_unique on (tenantId, roleId,
permission) for Postgres, MySQL and SQLite:

- It builds the index first. With no duplicates that build is the only
  pass over the table and nothing is rewritten. If the build fails on a
  duplicate key, the duplicates are deleted in one statement and the
  index is built again. Each group keeps the row that actually grants
  the permission (enabled, active, not archived, not soft-deleted),
  otherwise a row the app can still see, then an enabled row, then the
  oldest one, then the smallest id. So no role loses a permission it
  has. Rows with a NULL tenantId are left alone.
- On Postgres it runs inside the migration's transaction under a
  SHARE ROW EXCLUSIVE lock with a 5 s lock_timeout. Reads keep working,
  writers wait for the build (about 5 s per 2M rows), and a second
  process starting at the same time waits, then finds the index done.
  The build does not use CONCURRENTLY: a failed CONCURRENTLY build
  leaves an INVALID index, while this one just rolls back and runs
  again on the next start. An INVALID index already using this name is
  refused with instructions instead of being accepted as done.
- On MySQL the index is looked up by name before and after the build,
  so a retry after a crash or a lost race with another process does
  not fail.
- down() drops only the index.

Declare the same index on the RolePermission entity so synchronize and
migration:generate produce the same schema. Make the batched reload
insert skip rows another process inserted first (ON CONFLICT DO NOTHING
on Postgres and SQLite, a no-op ON DUPLICATE KEY UPDATE on MySQL).
Without that, the unique violation would be caught and logged, and
every tenant after the failing one would miss its new permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:10:49 +02:00
joel kalema f1655152d5 Merge pull request #10304 from joel-kalema/fix/sidebar-active-link-on-refresh
Fix/sidebar active link on refresh
2026-09-25 09:17:03 +02:00
Ruslan Konviser fb2184e41f Merge pull request #10305 from ever-co/fix/desktop-update-followups
fix(desktop): update-feed follow-ups: publish-channel CI guard, updater tag re-resolve, local-update note, stray channel
2026-09-25 00:53:23 +02:00
Ruslan KonviserandClaude Opus 5.5 52573417f6 test(desktop): avoid a word the CI spell-check dictionary rejects
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:48:39 +02:00
Ruslan KonviserandClaude Opus 5.5 028b5b1841 fix(desktop): resolve the GitHub update tag on every check, never a v-less feed URL
The GitHub update feed is github.com/<owner>/<repo>/releases/download/<tag>.
CdnUpdate.tagName() looks up <tag> through the unauthenticated releases API.

- Any failed lookup (offline, or the 60 requests/hour/IP rate limit, whose
  403 body is an object rather than a list) returned app.getVersion()
  without the "v" that release tags carry (111.44.45 instead of v111.44.45).
  That URL 404s until the app restarts. A failed lookup now keeps the last
  resolved tag and falls back to v<version>. A non-array response counts as
  a failure. The fallback is null-safe even when the prerelease setting
  itself cannot be read.
- The tag was only resolved at startup or on a strategy change, so the
  hourly automatic check and the manual check of a long-running app kept
  polling the release it started with. GithubCdn.checkUpdate() now
  re-resolves before every check. A tag resolved within the last minute is
  reused, so the startup and strategy-change paths still make one API call
  per check. The local strategy is untouched. download_update keeps the URL
  of the check that found the update.
- Because the lookup now runs before every check, it is bounded by a 10 s
  timeout, so a hung connection cannot stall a manual check.
- A release is published 15-60 minutes before its per-platform latest*.yml
  files are uploaded, and some releases never get one (desktop-timer
  v111.44.42 only has latest-x64-linux.yml). The lookup now prefers the
  newest matching release that already has this platform's update file, and
  otherwise falls back to the newest one. electron-updater does not
  downgrade (allowDowngrade stays false), so picking an older release only
  means "no update" instead of a 404.

Prerelease semantics are unchanged. An appSetting without a prerelease key
now means stable instead of matching nothing.

desktop-updater.spec.ts covers these paths (16 cases). tsc passes on the
changed files and the spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:42:17 +02:00
Ruslan KonviserandClaude Opus 5.5 e2797e2e9e fix(desktop-ui): name per-arch update manifests in the local update note
Since #9304 the desktop clients call autoUpdater.setFeedURL with
channel `latest-${process.arch}` on Windows and Linux (and `latest` on
macOS), and electron-updater's GenericProvider appends the platform
suffix to that channel. A folder chosen for the "Local Server" update
option is therefore queried for latest-x64.yml / latest-arm64.yml on
Windows, latest-x64-linux.yml / latest-arm64-linux-arm64.yml on Linux
and latest-mac.yml on macOS.

TIMER_TRACKER.SETTINGS.LOCAL_SERVER_NOTE still told users to provide
latest.yml, which the client never requests on any platform. A folder
prepared that way fails with ERR_UPDATER_CHANNEL_FILE_NOT_FOUND on
Windows and Linux.

Rewrite the note in all 13 locales to list the manifest per platform
and architecture and to mention the update files the manifest lists.
Only this value changes in each file; key order, indentation and line
endings are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:41:59 +02:00